Merge branch 'develop'
This commit is contained in:
commit
09e9b968fe
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ily.li",
|
||||
"version": "0.4.6",
|
||||
"version": "0.5.0",
|
||||
"description": "Self-hosted file pusher",
|
||||
"repository": "git@gitlab.com:ArisuOngaku/ily.li.git",
|
||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||
@ -22,13 +22,13 @@
|
||||
"@types/formidable": "^1.0.31",
|
||||
"@types/jest": "^26.0.4",
|
||||
"@types/mysql": "^2.15.15",
|
||||
"@types/node": "^13.13.2",
|
||||
"@types/node": "^14.0.27",
|
||||
"@types/nodemailer": "^6.4.0",
|
||||
"@types/nunjucks": "^3.1.3",
|
||||
"@types/ws": "^7.2.6",
|
||||
"babel-loader": "^8.1.0",
|
||||
"concurrently": "^5.1.0",
|
||||
"css-loader": "^3.5.2",
|
||||
"css-loader": "^4.2.1",
|
||||
"feather-icons": "^4.28.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"imagemin": "^7.0.1",
|
||||
@ -38,7 +38,7 @@
|
||||
"imagemin-svgo": "^8.0.0",
|
||||
"img-loader": "^3.0.1",
|
||||
"jest": "^26.1.0",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"mini-css-extract-plugin": "^0.10.0",
|
||||
"node-sass": "^4.14.0",
|
||||
"nodemon": "^2.0.3",
|
||||
"sass-loader": "^9.0.2",
|
||||
|
12
src/App.ts
12
src/App.ts
@ -34,11 +34,12 @@ import {Request} from "express";
|
||||
import CreateFilesTable from "./migrations/CreateFilesTable";
|
||||
import IncreaseFilesSizeField from "./migrations/IncreaseFilesSizeField";
|
||||
import AddApprovedFieldToUsersTable from "wms-core/auth/migrations/AddApprovedFieldToUsersTable";
|
||||
import BackendController from "./controllers/BackendController";
|
||||
import CreateUrlRedirectsTable from "./migrations/CreateUrlRedirectsTable";
|
||||
import AuthTokenController from "./controllers/AuthTokenController";
|
||||
import URLRedirectController from "./controllers/URLRedirectController";
|
||||
import LinkController from "./controllers/LinkController";
|
||||
import BackendController from "wms-core/helpers/BackendController";
|
||||
import RedirectBackComponent from "wms-core/components/RedirectBackComponent";
|
||||
|
||||
export default class App extends Application {
|
||||
private readonly port: number;
|
||||
@ -75,13 +76,18 @@ export default class App extends Application {
|
||||
|
||||
const expressAppComponent = new ExpressAppComponent(this.port);
|
||||
this.use(expressAppComponent);
|
||||
this.use(new NunjucksComponent());
|
||||
|
||||
// Base
|
||||
this.use(new LogRequestsComponent());
|
||||
|
||||
// Static files
|
||||
this.use(new ServeStaticDirectoryComponent('public'));
|
||||
this.use(new ServeStaticDirectoryComponent('node_modules/feather-icons/dist', '/icons'));
|
||||
|
||||
// Dynamic views and routes
|
||||
this.use(new NunjucksComponent());
|
||||
this.use(new RedirectBackComponent());
|
||||
|
||||
// Maintenance
|
||||
this.use(new MaintenanceComponent(this, () => {
|
||||
return redisComponent.canServe() && mysqlComponent.canServe();
|
||||
@ -95,6 +101,8 @@ export default class App extends Application {
|
||||
// Session
|
||||
this.use(redisComponent);
|
||||
this.use(new SessionComponent(redisComponent));
|
||||
|
||||
// Auth
|
||||
this.use(new AuthComponent(new class extends AuthGuard<MagicLink | AuthToken> {
|
||||
public async getProofForSession(session: Express.Session): Promise<any | null> {
|
||||
return await MagicLink.bySessionID(session.id, [MagicLinkActionType.LOGIN, MagicLinkActionType.REGISTER]);
|
||||
|
@ -1,6 +0,0 @@
|
||||
import {MailTemplate} from "wms-core/Mail";
|
||||
|
||||
export const MAGIC_LINK_MAIL = new MailTemplate(
|
||||
'magic_link',
|
||||
data => data.type === 'register' ? 'Registration' : 'Login magic link'
|
||||
);
|
12
src/Mails.ts
12
src/Mails.ts
@ -1,12 +0,0 @@
|
||||
import {MailTemplate} from "wms-core/Mail";
|
||||
import config from "config";
|
||||
|
||||
export const ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
|
||||
'account_review_notice',
|
||||
data => `Your account was ${data.approved ? 'approved' : 'rejected'}.`
|
||||
);
|
||||
|
||||
export const PENDING_ACCOUNT_REVIEW_MAIL_TEMPLATE: MailTemplate = new MailTemplate(
|
||||
'pending_account_review',
|
||||
() => 'A new account is pending review on ' + config.get<string>('domain'),
|
||||
);
|
10
src/Utils.ts
Normal file
10
src/Utils.ts
Normal file
@ -0,0 +1,10 @@
|
||||
export function encodeRFC5987ValueChars(str: string) {
|
||||
return encodeURIComponent(str).
|
||||
// Note that although RFC3986 reserves "!", RFC5987 does not,
|
||||
// so we do not need to escape it
|
||||
replace(/['()*]/g, c => "%" + c.charCodeAt(0).toString(16)). // i.e., %27 %28 %29 %2a (Note that valid encoding of "*" is %2A
|
||||
// which necessitates calling toUpperCase() to properly encode)
|
||||
// The following are not required for percent-encoding per RFC5987,
|
||||
// so we can allow for a little better readability over the wire: |`^
|
||||
replace(/%(7C|60|5E)/g, (str, hex) => String.fromCharCode(parseInt(hex, 16)));
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import MagicLinkAuthController from "wms-core/auth/magic_link/MagicLinkAuthController";
|
||||
import {MAGIC_LINK_MAIL} from "../MagicLinkMail";
|
||||
import {MAGIC_LINK_MAIL} from "wms-core/Mails";
|
||||
|
||||
export default class AuthController extends MagicLinkAuthController {
|
||||
public constructor() {
|
||||
|
@ -1,69 +0,0 @@
|
||||
import Controller from "wms-core/Controller";
|
||||
import {REQUIRE_ADMIN_MIDDLEWARE, REQUIRE_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||
import {Request, Response} from "express";
|
||||
import User from "wms-core/auth/models/User";
|
||||
import {NotFoundHttpError} from "wms-core/HttpError";
|
||||
import {ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE} from "../Mails";
|
||||
import Mail from "wms-core/Mail";
|
||||
import config from "config";
|
||||
|
||||
export default class BackendController extends Controller {
|
||||
getRoutesPrefix(): string {
|
||||
return '/backend';
|
||||
}
|
||||
|
||||
routes(): void {
|
||||
this.get('/', this.getIndex, 'backend', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
|
||||
if (User.isApprovalMode()) {
|
||||
this.get('/accounts-approval', this.getAccountApproval, 'accounts-approval', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
|
||||
this.get('/accounts-approval/approve/:id', this.getApproveAccount, 'approve-account', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
|
||||
this.get('/accounts-approval/reject/:id', this.getRejectAccount, 'reject-account', REQUIRE_AUTH_MIDDLEWARE, REQUIRE_ADMIN_MIDDLEWARE);
|
||||
}
|
||||
}
|
||||
|
||||
public async getIndex(req: Request, res: Response): Promise<void> {
|
||||
res.render('backend/index', {
|
||||
approval_mode: User.isApprovalMode(),
|
||||
accounts_to_approve: User.isApprovalMode() ? await User.select().count() : 0,
|
||||
});
|
||||
}
|
||||
|
||||
public async getAccountApproval(req: Request, res: Response): Promise<void> {
|
||||
const accounts = await User.select().where('approved', 0).with('mainEmail').get();
|
||||
res.render('backend/accounts_approval', {
|
||||
accounts: User.isApprovalMode() ? accounts : 0,
|
||||
});
|
||||
}
|
||||
|
||||
public async getApproveAccount(req: Request, res: Response): Promise<void> {
|
||||
const account = await User.select().where('id', req.params.id).with('mainEmail').first();
|
||||
if (!account) throw new NotFoundHttpError('User', req.url);
|
||||
const email = await account.mainEmail.get();
|
||||
|
||||
account.approved = true;
|
||||
await account.save();
|
||||
|
||||
await new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
|
||||
approved: true,
|
||||
link: config.get<string>('base_url') + Controller.route('auth'),
|
||||
}).send(email!.email!);
|
||||
|
||||
req.flash('success', `Account successfully approved.`);
|
||||
res.redirectBack(Controller.route('accounts-approval'));
|
||||
}
|
||||
|
||||
public async getRejectAccount(req: Request, res: Response): Promise<void> {
|
||||
const account = await User.select().where('id', req.params.id).with('mainEmail').first();
|
||||
if (!account) throw new NotFoundHttpError('User', req.url);
|
||||
const email = await account.mainEmail.get();
|
||||
|
||||
await account.delete();
|
||||
|
||||
await new Mail(ACCOUNT_REVIEW_NOTICE_MAIL_TEMPLATE, {
|
||||
approved: false,
|
||||
}).send(email!.email!);
|
||||
|
||||
req.flash('success', `Account successfully deleted.`);
|
||||
res.redirectBack(Controller.route('accounts-approval'));
|
||||
}
|
||||
}
|
@ -60,7 +60,7 @@ export default class FileController extends Controller {
|
||||
if (req.body.ttl !== undefined) ttl = parseInt(req.body.ttl);
|
||||
else if (req.body.expire_after_days !== undefined) ttl = parseInt(req.body.expire_after_days) * 24 * 3600;
|
||||
|
||||
const file = new FileModel({
|
||||
const file = FileModel.create({
|
||||
user_id: req.models.user!.id,
|
||||
slug: slug,
|
||||
real_name: upload.name,
|
||||
@ -71,7 +71,7 @@ export default class FileController extends Controller {
|
||||
});
|
||||
|
||||
await file.save();
|
||||
fs.renameSync(upload.path, file.storage_path);
|
||||
fs.renameSync(upload.path, file.storage_path!);
|
||||
|
||||
const domain = req.body.url_domain || config.get<string[]>('allowed_url_domains')[config.get<number>('default_url_domain_for_files')];
|
||||
res.format({
|
||||
@ -116,7 +116,7 @@ export default class FileController extends Controller {
|
||||
}
|
||||
|
||||
public static async deleteFile(file: FileModel): Promise<void> {
|
||||
fs.unlinkSync(file.storage_path);
|
||||
fs.unlinkSync(file.storage_path!);
|
||||
await file.delete();
|
||||
Logger.info('Deleted', file.storage_path, `(${file.real_name})`);
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Controller from "wms-core/Controller";
|
||||
import {NextFunction, Request, Response} from "express";
|
||||
import {BadRequestError, NotFoundHttpError, ServerError} from "wms-core/HttpError";
|
||||
import {BadRequestError, ForbiddenHttpError, NotFoundHttpError, ServerError} from "wms-core/HttpError";
|
||||
import config from "config";
|
||||
import {REQUIRE_REQUEST_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||
import URLRedirect from "../models/URLRedirect";
|
||||
@ -8,12 +8,14 @@ import URLRedirectController from "./URLRedirectController";
|
||||
import FileModel from "../models/FileModel";
|
||||
import generateSlug from "../SlugGenerator";
|
||||
import FileController, {FILE_UPLOAD_FORM_MIDDLEWARE} from "./FileController";
|
||||
import * as fs from "fs";
|
||||
import {encodeRFC5987ValueChars} from "../Utils";
|
||||
|
||||
export default class LinkController extends Controller {
|
||||
routes(): void {
|
||||
this.post('/', this.postFile, 'post-file', REQUIRE_REQUEST_AUTH_MIDDLEWARE, FILE_UPLOAD_FORM_MIDDLEWARE);
|
||||
this.delete('/:slug', FileController.deleteFileRoute, 'delete-file', REQUIRE_REQUEST_AUTH_MIDDLEWARE);
|
||||
this.get('/:slug', this.downloadFile, 'get-file');
|
||||
this.get('/:slug', this.getFile, 'get-file');
|
||||
this.put('/:slug', this.putFile, 'put-file', REQUIRE_REQUEST_AUTH_MIDDLEWARE, FILE_UPLOAD_FORM_MIDDLEWARE);
|
||||
|
||||
this.post('/', URLRedirectController.addURL, 'post-url', REQUIRE_REQUEST_AUTH_MIDDLEWARE);
|
||||
@ -24,7 +26,7 @@ export default class LinkController extends Controller {
|
||||
this.get(/(.*)/, this.domainFilter);
|
||||
}
|
||||
|
||||
protected async downloadFile(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
protected async getFile(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
console.log('get file', req.params.slug)
|
||||
const file = await FileModel.getBySlug(req.params.slug);
|
||||
if (!file) return next();
|
||||
@ -33,9 +35,20 @@ export default class LinkController extends Controller {
|
||||
return next();
|
||||
}
|
||||
|
||||
// File type
|
||||
const fileName = file.real_name!;
|
||||
const parts = fileName.split('.');
|
||||
res.type(parts[parts.length - 1]);
|
||||
|
||||
// File name
|
||||
res.header('Content-Disposition', `inline; filename*=UTF-8''${encodeRFC5987ValueChars(fileName)}`);
|
||||
|
||||
switch (file.storage_type) {
|
||||
case 'local':
|
||||
res.download(file.storage_path, file.real_name);
|
||||
fs.readFile(file.storage_path!, (err, data) => {
|
||||
if (err) return next(err);
|
||||
res.send(data);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new ServerError(`This file cannot be served. Download protocol for ${file.storage_type} storage type not implemented.`);
|
||||
@ -60,7 +73,7 @@ export default class LinkController extends Controller {
|
||||
const url = await URLRedirect.getBySlug(req.params.slug);
|
||||
if (!url) return next();
|
||||
|
||||
res.redirect(url.target_url, 301);
|
||||
res.redirect(url.target_url!, 301);
|
||||
}
|
||||
|
||||
protected async deleteURL(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
|
@ -16,11 +16,12 @@ export default class MagicLinkController extends _MagicLinkController {
|
||||
case MagicLinkActionType.LOGIN:
|
||||
case MagicLinkActionType.REGISTER:
|
||||
await AuthController.checkAndAuth(req, res, magicLink);
|
||||
const proof = await req.authGuard.isAuthenticated(req.session!);
|
||||
const user = await proof?.getResource();
|
||||
|
||||
if (!res.headersSent) {
|
||||
if (!res.headersSent && user) {
|
||||
// Auth success
|
||||
const user = await req.authGuard.getUserForSession(req.session!);
|
||||
req.flash('success', `Authentication success. Welcome, ${user?.name}!`);
|
||||
req.flash('success', `Authentication success. Welcome, ${user.name}!`);
|
||||
res.redirect(req.query.redirect_uri?.toString() || Controller.route('home'));
|
||||
}
|
||||
break;
|
||||
|
@ -1,21 +1,28 @@
|
||||
import {Connection} from "mysql";
|
||||
import Migration from "wms-core/db/Migration";
|
||||
import ModelFactory from "wms-core/db/ModelFactory";
|
||||
import AuthToken from "../models/AuthToken";
|
||||
|
||||
export default class CreateAuthTokensTable extends Migration {
|
||||
public async install(connection: Connection): Promise<void> {
|
||||
await this.query('CREATE TABLE auth_tokens(' +
|
||||
'id INT NOT NULL AUTO_INCREMENT,' +
|
||||
'user_id INT NOT NULL,' +
|
||||
'secret VARCHAR(64) UNIQUE NOT NULL,' +
|
||||
'created_at DATETIME NOT NULL DEFAULT NOW(),' +
|
||||
'used_at DATETIME NOT NULL DEFAULT NOW(),' +
|
||||
'ttl INT UNSIGNED NOT NULL,' +
|
||||
'PRIMARY KEY (id)' +
|
||||
')', connection);
|
||||
await this.query(`CREATE TABLE auth_tokens
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
user_id INT NOT NULL,
|
||||
secret VARCHAR(64) UNIQUE NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT NOW(),
|
||||
used_at DATETIME NOT NULL DEFAULT NOW(),
|
||||
ttl INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)`, connection);
|
||||
}
|
||||
|
||||
public async rollback(connection: Connection): Promise<void> {
|
||||
await this.query('DROP TABLE IF EXISTS auth_tokens', connection);
|
||||
await this.query(`DROP TABLE IF EXISTS auth_tokens`, connection);
|
||||
}
|
||||
|
||||
public registerModels(): void {
|
||||
ModelFactory.register(AuthToken);
|
||||
}
|
||||
|
||||
}
|
@ -1,24 +1,30 @@
|
||||
import {Connection} from "mysql";
|
||||
import Migration from "wms-core/db/Migration";
|
||||
import ModelFactory from "wms-core/db/ModelFactory";
|
||||
import FileModel from "../models/FileModel";
|
||||
|
||||
export default class CreateFilesTable extends Migration {
|
||||
public async install(connection: Connection): Promise<void> {
|
||||
await this.query('CREATE TABLE files(' +
|
||||
'id INT NOT NULL AUTO_INCREMENT,' +
|
||||
'user_id INT NOT NULL,' +
|
||||
'slug VARCHAR(259) UNIQUE NOT NULL,' +
|
||||
'real_name VARCHAR(259) NOT NULL,' +
|
||||
'storage_type VARCHAR(64) NOT NULL,' +
|
||||
'storage_path VARCHAR(1745) NOT NULL,' +
|
||||
'size INT UNSIGNED NOT NULL,' +
|
||||
'created_at DATETIME NOT NULL DEFAULT NOW(),' +
|
||||
'ttl INT UNSIGNED NOT NULL,' +
|
||||
'PRIMARY KEY (id)' +
|
||||
')', connection);
|
||||
await this.query(`CREATE TABLE files
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
user_id INT NOT NULL,
|
||||
slug VARCHAR(259) UNIQUE NOT NULL,
|
||||
real_name VARCHAR(259) NOT NULL,
|
||||
storage_type VARCHAR(64) NOT NULL,
|
||||
storage_path VARCHAR(1745) NOT NULL,
|
||||
size INT UNSIGNED NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT NOW(),
|
||||
ttl INT UNSIGNED NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)`, connection);
|
||||
}
|
||||
|
||||
public async rollback(connection: Connection): Promise<void> {
|
||||
await this.query('DROP TABLE IF EXISTS files', connection);
|
||||
await this.query(`DROP TABLE IF EXISTS files`, connection);
|
||||
}
|
||||
|
||||
public registerModels(): void {
|
||||
ModelFactory.register(FileModel);
|
||||
}
|
||||
}
|
@ -1,20 +1,26 @@
|
||||
import Migration from "wms-core/db/Migration";
|
||||
import {Connection} from "mysql";
|
||||
import ModelFactory from "wms-core/db/ModelFactory";
|
||||
import URLRedirect from "../models/URLRedirect";
|
||||
|
||||
export default class CreateUrlRedirectsTable extends Migration {
|
||||
public async install(connection: Connection): Promise<void> {
|
||||
await this.query('CREATE TABLE url_redirects(' +
|
||||
'id INT NOT NULL AUTO_INCREMENT,' +
|
||||
'user_id INT NOT NULL,' +
|
||||
'slug VARCHAR(259) UNIQUE NOT NULL,' +
|
||||
'target_url VARCHAR(1745) NOT NULL,' +
|
||||
'created_at DATETIME NOT NULL DEFAULT NOW(),' +
|
||||
'PRIMARY KEY (id)' +
|
||||
')', connection);
|
||||
await this.query(`CREATE TABLE url_redirects
|
||||
(
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
user_id INT NOT NULL,
|
||||
slug VARCHAR(259) UNIQUE NOT NULL,
|
||||
target_url VARCHAR(1745) NOT NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (id)
|
||||
)`, connection);
|
||||
}
|
||||
|
||||
public async rollback(connection: Connection): Promise<void> {
|
||||
await this.query('DROP TABLE IF EXISTS url_redirects', connection);
|
||||
await this.query(`DROP TABLE IF EXISTS url_redirects`, connection);
|
||||
}
|
||||
|
||||
public registerModels(): void {
|
||||
ModelFactory.register(URLRedirect);
|
||||
}
|
||||
}
|
@ -3,10 +3,13 @@ import {Connection} from "mysql";
|
||||
|
||||
export default class IncreaseFilesSizeField extends Migration {
|
||||
public async install(connection: Connection): Promise<void> {
|
||||
await this.query('ALTER TABLE files MODIFY size BIGINT UNSIGNED', connection);
|
||||
await this.query(`ALTER TABLE files MODIFY size BIGINT UNSIGNED`, connection);
|
||||
}
|
||||
|
||||
public async rollback(connection: Connection): Promise<void> {
|
||||
await this.query('ALTER TABLE files MODIFY size INT UNSIGNED', connection);
|
||||
await this.query(`ALTER TABLE files MODIFY size INT UNSIGNED`, connection);
|
||||
}
|
||||
|
||||
public registerModels(): void {
|
||||
}
|
||||
}
|
@ -1,16 +1,15 @@
|
||||
import Model from "wms-core/db/Model";
|
||||
import AuthProof from "wms-core/auth/AuthProof";
|
||||
import UserEmail from "wms-core/auth/models/UserEmail";
|
||||
import User from "wms-core/auth/models/User";
|
||||
import Validator from "wms-core/db/Validator";
|
||||
import {cryptoRandomDictionary} from "wms-core/Utils";
|
||||
|
||||
export default class AuthToken extends Model implements AuthProof {
|
||||
protected readonly user_id!: number;
|
||||
protected readonly secret!: string;
|
||||
protected created_at?: Date;
|
||||
protected used_at?: Date;
|
||||
protected readonly ttl!: number;
|
||||
export default class AuthToken extends Model implements AuthProof<User> {
|
||||
public id?: number = undefined;
|
||||
protected readonly user_id?: number = undefined;
|
||||
protected readonly secret?: string = undefined;
|
||||
protected created_at?: Date = undefined;
|
||||
protected used_at?: Date = undefined;
|
||||
protected readonly ttl?: number = undefined;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
@ -20,13 +19,10 @@ export default class AuthToken extends Model implements AuthProof {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected init() {
|
||||
this.addProperty('user_id', new Validator().defined().exists(User, 'id'));
|
||||
this.addProperty('secret', new Validator().defined().between(32, 64));
|
||||
this.addProperty('created_at', new Validator());
|
||||
this.addProperty('used_at', new Validator());
|
||||
this.addProperty('ttl', new Validator().defined().min(1).max(5 * 365 * 24 * 3600)); // max 5 years
|
||||
this.setValidation('user_id').defined().exists(User, 'id');
|
||||
this.setValidation('secret').defined().between(32, 64);
|
||||
this.setValidation('ttl').defined().min(1).max(5 * 365 * 24 * 3600 /* 5 years */);
|
||||
}
|
||||
|
||||
public use() {
|
||||
@ -40,16 +36,10 @@ export default class AuthToken extends Model implements AuthProof {
|
||||
public getExpirationDate(): Date {
|
||||
if (!this.created_at) return new Date();
|
||||
|
||||
return new Date(this.created_at.getTime() + this.ttl * 1000);
|
||||
return new Date(this.created_at.getTime() + this.ttl! * 1000);
|
||||
}
|
||||
|
||||
public async getEmail(): Promise<string> {
|
||||
let userEmail = await UserEmail.select().where('user_id', this.user_id).first();
|
||||
if (!userEmail) throw new Error("Cannot find main user email for user " + this.user_id);
|
||||
return userEmail.email;
|
||||
}
|
||||
|
||||
public async getUser(): Promise<User | null> {
|
||||
public async getResource(): Promise<User | null> {
|
||||
return await User.getById<User>(this.user_id);
|
||||
}
|
||||
|
||||
@ -57,15 +47,11 @@ export default class AuthToken extends Model implements AuthProof {
|
||||
return true;
|
||||
}
|
||||
|
||||
public async isOwnedBy(userId: number): Promise<boolean> {
|
||||
return this.user_id === userId;
|
||||
}
|
||||
|
||||
public async isValid(): Promise<boolean> {
|
||||
return new Date().getTime() < this.getExpirationDate().getTime();
|
||||
}
|
||||
|
||||
public async revoke(session: Express.Session): Promise<void> {
|
||||
public async revoke(): Promise<void> {
|
||||
await this.delete();
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import Model from "wms-core/db/Model";
|
||||
import Validator from "wms-core/db/Validator";
|
||||
import Controller from "wms-core/Controller";
|
||||
import config from "config";
|
||||
import User from "wms-core/auth/models/User";
|
||||
@ -19,29 +18,29 @@ export default class FileModel extends Model {
|
||||
return await this.paginate(req, perPage, this.select().where('user_id', user_id));
|
||||
}
|
||||
|
||||
public readonly user_id!: number;
|
||||
public readonly slug!: string;
|
||||
public readonly real_name!: string;
|
||||
public readonly storage_type!: FileStorage;
|
||||
public readonly storage_path!: string;
|
||||
public readonly size!: number;
|
||||
public created_at?: Date;
|
||||
public readonly ttl!: number;
|
||||
public id?: number = undefined;
|
||||
public readonly user_id?: number = undefined;
|
||||
public readonly slug?: string = undefined;
|
||||
public readonly real_name?: string = undefined;
|
||||
public readonly storage_type?: FileStorage = undefined;
|
||||
public readonly storage_path?: string = undefined;
|
||||
public readonly size?: number = undefined;
|
||||
public created_at?: Date = undefined;
|
||||
public readonly ttl?: number = undefined;
|
||||
|
||||
protected init() {
|
||||
this.addProperty('user_id', new Validator().defined().exists(User, 'id'));
|
||||
this.addProperty('slug', new Validator().defined().minLength(1).maxLength(259).unique(FileModel, 'slug').unique(URLRedirect, 'slug'));
|
||||
this.addProperty('real_name', new Validator().defined().minLength(1).maxLength(259));
|
||||
this.addProperty('storage_type', new Validator().defined().maxLength(64));
|
||||
this.addProperty('storage_path', new Validator().defined().maxLength(1745));
|
||||
this.addProperty('size', new Validator().defined().min(0));
|
||||
this.addProperty('created_at', new Validator());
|
||||
this.addProperty('ttl', new Validator().defined().min(0).max(4294967295));
|
||||
this.setValidation('user_id').defined().exists(User, 'id');
|
||||
this.setValidation('slug').defined().minLength(1).maxLength(259).unique(FileModel, 'slug').unique(URLRedirect, 'slug');
|
||||
this.setValidation('real_name').defined().minLength(1).maxLength(259);
|
||||
this.setValidation('storage_type').defined().maxLength(64);
|
||||
this.setValidation('storage_path').defined().maxLength(1745);
|
||||
this.setValidation('size').defined().min(0);
|
||||
this.setValidation('ttl').defined().min(0).max(4294967295);
|
||||
}
|
||||
|
||||
public getURL(domain: string = config.get<string>('base_url')): string {
|
||||
return (/^https?:\/\//.test(domain) ? '' : 'https://') + domain + Controller.route('get-file', {
|
||||
slug: this.slug,
|
||||
slug: this.slug!,
|
||||
});
|
||||
}
|
||||
|
||||
@ -49,7 +48,7 @@ export default class FileModel extends Model {
|
||||
if (!this.created_at) return new Date();
|
||||
if (this.ttl === 0) return null;
|
||||
|
||||
return new Date(this.created_at.getTime() + this.ttl * 1000);
|
||||
return new Date(this.created_at.getTime() + this.ttl! * 1000);
|
||||
}
|
||||
|
||||
public shouldBeDeleted(): boolean {
|
||||
|
@ -1,5 +1,4 @@
|
||||
import Model from "wms-core/db/Model";
|
||||
import Validator from "wms-core/db/Validator";
|
||||
import User from "wms-core/auth/models/User";
|
||||
import FileModel from "./FileModel";
|
||||
import {Request} from "express";
|
||||
@ -19,26 +18,25 @@ export default class URLRedirect extends Model {
|
||||
return await this.paginate(req, perPage, this.select().where('user_id', user_id));
|
||||
}
|
||||
|
||||
public readonly user_id!: number;
|
||||
public readonly slug!: string;
|
||||
public readonly target_url!: string;
|
||||
public created_at?: Date;
|
||||
public id?: number = undefined;
|
||||
public readonly user_id?: number = undefined;
|
||||
public readonly slug?: string = undefined;
|
||||
public readonly target_url?: string = undefined;
|
||||
public created_at?: Date = undefined;
|
||||
|
||||
constructor(data: any) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
protected init(): void {
|
||||
this.addProperty('user_id', new Validator().defined().exists(User, 'id'));
|
||||
this.addProperty('slug', new Validator().defined().minLength(1).maxLength(259).unique(URLRedirect, 'slug').unique(FileModel, 'slug'));
|
||||
this.addProperty('target_url', new Validator().defined().maxLength(1745).regexp(/^https?:\/\/.{3,259}?\/?/i));
|
||||
this.addProperty('created_at', new Validator());
|
||||
this.setValidation('user_id').defined().exists(User, 'id');
|
||||
this.setValidation('slug').defined().minLength(1).maxLength(259).unique(URLRedirect, 'slug').unique(FileModel, 'slug');
|
||||
this.setValidation('target_url').defined().maxLength(1745).regexp(/^https?:\/\/.{3,259}?\/?/i);
|
||||
}
|
||||
|
||||
public getURL(domain: string = config.get<string>('base_url')): string {
|
||||
return (/^https?:\/\//.test(domain) ? '' : 'https://') + domain + Controller.route('get-url', {
|
||||
slug: this.slug,
|
||||
slug: this.slug!,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
{% extends 'layouts/base.njk' %}
|
||||
|
||||
{% set title = app.name + ' - Review accounts' %}
|
||||
|
||||
{% block body %}
|
||||
<h1>Accounts pending review</h1>
|
||||
|
||||
<div class="panel">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="shrink-col">#</th>
|
||||
<th>Name</th>
|
||||
<th>Main email</th>
|
||||
<th>Registered at</th>
|
||||
<th class="shrink-col">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in accounts %}
|
||||
<tr>
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.name }}</td>
|
||||
<td>{{ user.mainEmail.getOrFail().email }}</td>
|
||||
<td>{{ user.created_at.toISOString() }}</td>
|
||||
<td>
|
||||
<div class="max-content">
|
||||
<a href="{{ route('approve-account', user.id) }}"
|
||||
class="button success"><i data-feather="check"></i> Approve</a>
|
||||
|
||||
<a href="{{ route('reject-account', user.id) }}"
|
||||
onclick="return confirm(`This will irrevocably delete the ${user.main_email} account.`)"
|
||||
class="button danger"><i data-feather="x"></i> Reject</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,21 +0,0 @@
|
||||
{% extends 'layouts/base.njk' %}
|
||||
|
||||
{% set title = app.name + ' - Backend' %}
|
||||
|
||||
{% block body %}
|
||||
<h1>App administration</h1>
|
||||
|
||||
<div class="container">
|
||||
<div class="panel">
|
||||
<nav>
|
||||
<ul>
|
||||
{% if approval_mode %}
|
||||
<li>
|
||||
<a href="{{ route('accounts-approval') }}">Accounts approval ({{ accounts_to_approve }})</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue
Block a user