Merge branch 'develop'
This commit is contained in:
commit
09e9b968fe
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ily.li",
|
"name": "ily.li",
|
||||||
"version": "0.4.6",
|
"version": "0.5.0",
|
||||||
"description": "Self-hosted file pusher",
|
"description": "Self-hosted file pusher",
|
||||||
"repository": "git@gitlab.com:ArisuOngaku/ily.li.git",
|
"repository": "git@gitlab.com:ArisuOngaku/ily.li.git",
|
||||||
"author": "Alice Gaudon <alice@gaudon.pro>",
|
"author": "Alice Gaudon <alice@gaudon.pro>",
|
||||||
@ -22,13 +22,13 @@
|
|||||||
"@types/formidable": "^1.0.31",
|
"@types/formidable": "^1.0.31",
|
||||||
"@types/jest": "^26.0.4",
|
"@types/jest": "^26.0.4",
|
||||||
"@types/mysql": "^2.15.15",
|
"@types/mysql": "^2.15.15",
|
||||||
"@types/node": "^13.13.2",
|
"@types/node": "^14.0.27",
|
||||||
"@types/nodemailer": "^6.4.0",
|
"@types/nodemailer": "^6.4.0",
|
||||||
"@types/nunjucks": "^3.1.3",
|
"@types/nunjucks": "^3.1.3",
|
||||||
"@types/ws": "^7.2.6",
|
"@types/ws": "^7.2.6",
|
||||||
"babel-loader": "^8.1.0",
|
"babel-loader": "^8.1.0",
|
||||||
"concurrently": "^5.1.0",
|
"concurrently": "^5.1.0",
|
||||||
"css-loader": "^3.5.2",
|
"css-loader": "^4.2.1",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "^4.28.0",
|
||||||
"file-loader": "^6.0.0",
|
"file-loader": "^6.0.0",
|
||||||
"imagemin": "^7.0.1",
|
"imagemin": "^7.0.1",
|
||||||
@ -38,7 +38,7 @@
|
|||||||
"imagemin-svgo": "^8.0.0",
|
"imagemin-svgo": "^8.0.0",
|
||||||
"img-loader": "^3.0.1",
|
"img-loader": "^3.0.1",
|
||||||
"jest": "^26.1.0",
|
"jest": "^26.1.0",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.10.0",
|
||||||
"node-sass": "^4.14.0",
|
"node-sass": "^4.14.0",
|
||||||
"nodemon": "^2.0.3",
|
"nodemon": "^2.0.3",
|
||||||
"sass-loader": "^9.0.2",
|
"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 CreateFilesTable from "./migrations/CreateFilesTable";
|
||||||
import IncreaseFilesSizeField from "./migrations/IncreaseFilesSizeField";
|
import IncreaseFilesSizeField from "./migrations/IncreaseFilesSizeField";
|
||||||
import AddApprovedFieldToUsersTable from "wms-core/auth/migrations/AddApprovedFieldToUsersTable";
|
import AddApprovedFieldToUsersTable from "wms-core/auth/migrations/AddApprovedFieldToUsersTable";
|
||||||
import BackendController from "./controllers/BackendController";
|
|
||||||
import CreateUrlRedirectsTable from "./migrations/CreateUrlRedirectsTable";
|
import CreateUrlRedirectsTable from "./migrations/CreateUrlRedirectsTable";
|
||||||
import AuthTokenController from "./controllers/AuthTokenController";
|
import AuthTokenController from "./controllers/AuthTokenController";
|
||||||
import URLRedirectController from "./controllers/URLRedirectController";
|
import URLRedirectController from "./controllers/URLRedirectController";
|
||||||
import LinkController from "./controllers/LinkController";
|
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 {
|
export default class App extends Application {
|
||||||
private readonly port: number;
|
private readonly port: number;
|
||||||
@ -75,13 +76,18 @@ export default class App extends Application {
|
|||||||
|
|
||||||
const expressAppComponent = new ExpressAppComponent(this.port);
|
const expressAppComponent = new ExpressAppComponent(this.port);
|
||||||
this.use(expressAppComponent);
|
this.use(expressAppComponent);
|
||||||
this.use(new NunjucksComponent());
|
|
||||||
|
// Base
|
||||||
this.use(new LogRequestsComponent());
|
this.use(new LogRequestsComponent());
|
||||||
|
|
||||||
// Static files
|
// Static files
|
||||||
this.use(new ServeStaticDirectoryComponent('public'));
|
this.use(new ServeStaticDirectoryComponent('public'));
|
||||||
this.use(new ServeStaticDirectoryComponent('node_modules/feather-icons/dist', '/icons'));
|
this.use(new ServeStaticDirectoryComponent('node_modules/feather-icons/dist', '/icons'));
|
||||||
|
|
||||||
|
// Dynamic views and routes
|
||||||
|
this.use(new NunjucksComponent());
|
||||||
|
this.use(new RedirectBackComponent());
|
||||||
|
|
||||||
// Maintenance
|
// Maintenance
|
||||||
this.use(new MaintenanceComponent(this, () => {
|
this.use(new MaintenanceComponent(this, () => {
|
||||||
return redisComponent.canServe() && mysqlComponent.canServe();
|
return redisComponent.canServe() && mysqlComponent.canServe();
|
||||||
@ -95,6 +101,8 @@ export default class App extends Application {
|
|||||||
// Session
|
// Session
|
||||||
this.use(redisComponent);
|
this.use(redisComponent);
|
||||||
this.use(new SessionComponent(redisComponent));
|
this.use(new SessionComponent(redisComponent));
|
||||||
|
|
||||||
|
// Auth
|
||||||
this.use(new AuthComponent(new class extends AuthGuard<MagicLink | AuthToken> {
|
this.use(new AuthComponent(new class extends AuthGuard<MagicLink | AuthToken> {
|
||||||
public async getProofForSession(session: Express.Session): Promise<any | null> {
|
public async getProofForSession(session: Express.Session): Promise<any | null> {
|
||||||
return await MagicLink.bySessionID(session.id, [MagicLinkActionType.LOGIN, MagicLinkActionType.REGISTER]);
|
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 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 {
|
export default class AuthController extends MagicLinkAuthController {
|
||||||
public constructor() {
|
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);
|
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;
|
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,
|
user_id: req.models.user!.id,
|
||||||
slug: slug,
|
slug: slug,
|
||||||
real_name: upload.name,
|
real_name: upload.name,
|
||||||
@ -71,7 +71,7 @@ export default class FileController extends Controller {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await file.save();
|
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')];
|
const domain = req.body.url_domain || config.get<string[]>('allowed_url_domains')[config.get<number>('default_url_domain_for_files')];
|
||||||
res.format({
|
res.format({
|
||||||
@ -116,7 +116,7 @@ export default class FileController extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async deleteFile(file: FileModel): Promise<void> {
|
public static async deleteFile(file: FileModel): Promise<void> {
|
||||||
fs.unlinkSync(file.storage_path);
|
fs.unlinkSync(file.storage_path!);
|
||||||
await file.delete();
|
await file.delete();
|
||||||
Logger.info('Deleted', file.storage_path, `(${file.real_name})`);
|
Logger.info('Deleted', file.storage_path, `(${file.real_name})`);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Controller from "wms-core/Controller";
|
import Controller from "wms-core/Controller";
|
||||||
import {NextFunction, Request, Response} from "express";
|
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 config from "config";
|
||||||
import {REQUIRE_REQUEST_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
import {REQUIRE_REQUEST_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||||
import URLRedirect from "../models/URLRedirect";
|
import URLRedirect from "../models/URLRedirect";
|
||||||
@ -8,12 +8,14 @@ import URLRedirectController from "./URLRedirectController";
|
|||||||
import FileModel from "../models/FileModel";
|
import FileModel from "../models/FileModel";
|
||||||
import generateSlug from "../SlugGenerator";
|
import generateSlug from "../SlugGenerator";
|
||||||
import FileController, {FILE_UPLOAD_FORM_MIDDLEWARE} from "./FileController";
|
import FileController, {FILE_UPLOAD_FORM_MIDDLEWARE} from "./FileController";
|
||||||
|
import * as fs from "fs";
|
||||||
|
import {encodeRFC5987ValueChars} from "../Utils";
|
||||||
|
|
||||||
export default class LinkController extends Controller {
|
export default class LinkController extends Controller {
|
||||||
routes(): void {
|
routes(): void {
|
||||||
this.post('/', this.postFile, 'post-file', REQUIRE_REQUEST_AUTH_MIDDLEWARE, FILE_UPLOAD_FORM_MIDDLEWARE);
|
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.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.put('/:slug', this.putFile, 'put-file', REQUIRE_REQUEST_AUTH_MIDDLEWARE, FILE_UPLOAD_FORM_MIDDLEWARE);
|
||||||
|
|
||||||
this.post('/', URLRedirectController.addURL, 'post-url', REQUIRE_REQUEST_AUTH_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);
|
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)
|
console.log('get file', req.params.slug)
|
||||||
const file = await FileModel.getBySlug(req.params.slug);
|
const file = await FileModel.getBySlug(req.params.slug);
|
||||||
if (!file) return next();
|
if (!file) return next();
|
||||||
@ -33,9 +35,20 @@ export default class LinkController extends Controller {
|
|||||||
return next();
|
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) {
|
switch (file.storage_type) {
|
||||||
case 'local':
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ServerError(`This file cannot be served. Download protocol for ${file.storage_type} storage type not implemented.`);
|
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);
|
const url = await URLRedirect.getBySlug(req.params.slug);
|
||||||
if (!url) return next();
|
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> {
|
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.LOGIN:
|
||||||
case MagicLinkActionType.REGISTER:
|
case MagicLinkActionType.REGISTER:
|
||||||
await AuthController.checkAndAuth(req, res, magicLink);
|
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
|
// 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'));
|
res.redirect(req.query.redirect_uri?.toString() || Controller.route('home'));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -1,21 +1,28 @@
|
|||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
import Migration from "wms-core/db/Migration";
|
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 {
|
export default class CreateAuthTokensTable extends Migration {
|
||||||
public async install(connection: Connection): Promise<void> {
|
public async install(connection: Connection): Promise<void> {
|
||||||
await this.query('CREATE TABLE auth_tokens(' +
|
await this.query(`CREATE TABLE auth_tokens
|
||||||
'id INT NOT NULL AUTO_INCREMENT,' +
|
(
|
||||||
'user_id INT NOT NULL,' +
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
'secret VARCHAR(64) UNIQUE NOT NULL,' +
|
user_id INT NOT NULL,
|
||||||
'created_at DATETIME NOT NULL DEFAULT NOW(),' +
|
secret VARCHAR(64) UNIQUE NOT NULL,
|
||||||
'used_at DATETIME NOT NULL DEFAULT NOW(),' +
|
created_at DATETIME NOT NULL DEFAULT NOW(),
|
||||||
'ttl INT UNSIGNED NOT NULL,' +
|
used_at DATETIME NOT NULL DEFAULT NOW(),
|
||||||
'PRIMARY KEY (id)' +
|
ttl INT UNSIGNED NOT NULL,
|
||||||
')', connection);
|
PRIMARY KEY (id)
|
||||||
|
)`, connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(connection: Connection): Promise<void> {
|
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 {Connection} from "mysql";
|
||||||
import Migration from "wms-core/db/Migration";
|
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 {
|
export default class CreateFilesTable extends Migration {
|
||||||
public async install(connection: Connection): Promise<void> {
|
public async install(connection: Connection): Promise<void> {
|
||||||
await this.query('CREATE TABLE files(' +
|
await this.query(`CREATE TABLE files
|
||||||
'id INT NOT NULL AUTO_INCREMENT,' +
|
(
|
||||||
'user_id INT NOT NULL,' +
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
'slug VARCHAR(259) UNIQUE NOT NULL,' +
|
user_id INT NOT NULL,
|
||||||
'real_name VARCHAR(259) NOT NULL,' +
|
slug VARCHAR(259) UNIQUE NOT NULL,
|
||||||
'storage_type VARCHAR(64) NOT NULL,' +
|
real_name VARCHAR(259) NOT NULL,
|
||||||
'storage_path VARCHAR(1745) NOT NULL,' +
|
storage_type VARCHAR(64) NOT NULL,
|
||||||
'size INT UNSIGNED NOT NULL,' +
|
storage_path VARCHAR(1745) NOT NULL,
|
||||||
'created_at DATETIME NOT NULL DEFAULT NOW(),' +
|
size INT UNSIGNED NOT NULL,
|
||||||
'ttl INT UNSIGNED NOT NULL,' +
|
created_at DATETIME NOT NULL DEFAULT NOW(),
|
||||||
'PRIMARY KEY (id)' +
|
ttl INT UNSIGNED NOT NULL,
|
||||||
')', connection);
|
PRIMARY KEY (id)
|
||||||
|
)`, connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(connection: Connection): Promise<void> {
|
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 Migration from "wms-core/db/Migration";
|
||||||
import {Connection} from "mysql";
|
import {Connection} from "mysql";
|
||||||
|
import ModelFactory from "wms-core/db/ModelFactory";
|
||||||
|
import URLRedirect from "../models/URLRedirect";
|
||||||
|
|
||||||
export default class CreateUrlRedirectsTable extends Migration {
|
export default class CreateUrlRedirectsTable extends Migration {
|
||||||
public async install(connection: Connection): Promise<void> {
|
public async install(connection: Connection): Promise<void> {
|
||||||
await this.query('CREATE TABLE url_redirects(' +
|
await this.query(`CREATE TABLE url_redirects
|
||||||
'id INT NOT NULL AUTO_INCREMENT,' +
|
(
|
||||||
'user_id INT NOT NULL,' +
|
id INT NOT NULL AUTO_INCREMENT,
|
||||||
'slug VARCHAR(259) UNIQUE NOT NULL,' +
|
user_id INT NOT NULL,
|
||||||
'target_url VARCHAR(1745) NOT NULL,' +
|
slug VARCHAR(259) UNIQUE NOT NULL,
|
||||||
'created_at DATETIME NOT NULL DEFAULT NOW(),' +
|
target_url VARCHAR(1745) NOT NULL,
|
||||||
'PRIMARY KEY (id)' +
|
created_at DATETIME NOT NULL DEFAULT NOW(),
|
||||||
')', connection);
|
PRIMARY KEY (id)
|
||||||
|
)`, connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async rollback(connection: Connection): Promise<void> {
|
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 {
|
export default class IncreaseFilesSizeField extends Migration {
|
||||||
public async install(connection: Connection): Promise<void> {
|
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> {
|
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 Model from "wms-core/db/Model";
|
||||||
import AuthProof from "wms-core/auth/AuthProof";
|
import AuthProof from "wms-core/auth/AuthProof";
|
||||||
import UserEmail from "wms-core/auth/models/UserEmail";
|
|
||||||
import User from "wms-core/auth/models/User";
|
import User from "wms-core/auth/models/User";
|
||||||
import Validator from "wms-core/db/Validator";
|
|
||||||
import {cryptoRandomDictionary} from "wms-core/Utils";
|
import {cryptoRandomDictionary} from "wms-core/Utils";
|
||||||
|
|
||||||
export default class AuthToken extends Model implements AuthProof {
|
export default class AuthToken extends Model implements AuthProof<User> {
|
||||||
protected readonly user_id!: number;
|
public id?: number = undefined;
|
||||||
protected readonly secret!: string;
|
protected readonly user_id?: number = undefined;
|
||||||
protected created_at?: Date;
|
protected readonly secret?: string = undefined;
|
||||||
protected used_at?: Date;
|
protected created_at?: Date = undefined;
|
||||||
protected readonly ttl!: number;
|
protected used_at?: Date = undefined;
|
||||||
|
protected readonly ttl?: number = undefined;
|
||||||
|
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
@ -20,13 +19,10 @@ export default class AuthToken extends Model implements AuthProof {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected init() {
|
protected init() {
|
||||||
this.addProperty('user_id', new Validator().defined().exists(User, 'id'));
|
this.setValidation('user_id').defined().exists(User, 'id');
|
||||||
this.addProperty('secret', new Validator().defined().between(32, 64));
|
this.setValidation('secret').defined().between(32, 64);
|
||||||
this.addProperty('created_at', new Validator());
|
this.setValidation('ttl').defined().min(1).max(5 * 365 * 24 * 3600 /* 5 years */);
|
||||||
this.addProperty('used_at', new Validator());
|
|
||||||
this.addProperty('ttl', new Validator().defined().min(1).max(5 * 365 * 24 * 3600)); // max 5 years
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public use() {
|
public use() {
|
||||||
@ -40,16 +36,10 @@ export default class AuthToken extends Model implements AuthProof {
|
|||||||
public getExpirationDate(): Date {
|
public getExpirationDate(): Date {
|
||||||
if (!this.created_at) return new 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> {
|
public async getResource(): Promise<User | null> {
|
||||||
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> {
|
|
||||||
return await User.getById<User>(this.user_id);
|
return await User.getById<User>(this.user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,15 +47,11 @@ export default class AuthToken extends Model implements AuthProof {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async isOwnedBy(userId: number): Promise<boolean> {
|
|
||||||
return this.user_id === userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async isValid(): Promise<boolean> {
|
public async isValid(): Promise<boolean> {
|
||||||
return new Date().getTime() < this.getExpirationDate().getTime();
|
return new Date().getTime() < this.getExpirationDate().getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async revoke(session: Express.Session): Promise<void> {
|
public async revoke(): Promise<void> {
|
||||||
await this.delete();
|
await this.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import Model from "wms-core/db/Model";
|
import Model from "wms-core/db/Model";
|
||||||
import Validator from "wms-core/db/Validator";
|
|
||||||
import Controller from "wms-core/Controller";
|
import Controller from "wms-core/Controller";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import User from "wms-core/auth/models/User";
|
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));
|
return await this.paginate(req, perPage, this.select().where('user_id', user_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly user_id!: number;
|
public id?: number = undefined;
|
||||||
public readonly slug!: string;
|
public readonly user_id?: number = undefined;
|
||||||
public readonly real_name!: string;
|
public readonly slug?: string = undefined;
|
||||||
public readonly storage_type!: FileStorage;
|
public readonly real_name?: string = undefined;
|
||||||
public readonly storage_path!: string;
|
public readonly storage_type?: FileStorage = undefined;
|
||||||
public readonly size!: number;
|
public readonly storage_path?: string = undefined;
|
||||||
public created_at?: Date;
|
public readonly size?: number = undefined;
|
||||||
public readonly ttl!: number;
|
public created_at?: Date = undefined;
|
||||||
|
public readonly ttl?: number = undefined;
|
||||||
|
|
||||||
protected init() {
|
protected init() {
|
||||||
this.addProperty('user_id', new Validator().defined().exists(User, 'id'));
|
this.setValidation('user_id').defined().exists(User, 'id');
|
||||||
this.addProperty('slug', new Validator().defined().minLength(1).maxLength(259).unique(FileModel, 'slug').unique(URLRedirect, 'slug'));
|
this.setValidation('slug').defined().minLength(1).maxLength(259).unique(FileModel, 'slug').unique(URLRedirect, 'slug');
|
||||||
this.addProperty('real_name', new Validator().defined().minLength(1).maxLength(259));
|
this.setValidation('real_name').defined().minLength(1).maxLength(259);
|
||||||
this.addProperty('storage_type', new Validator().defined().maxLength(64));
|
this.setValidation('storage_type').defined().maxLength(64);
|
||||||
this.addProperty('storage_path', new Validator().defined().maxLength(1745));
|
this.setValidation('storage_path').defined().maxLength(1745);
|
||||||
this.addProperty('size', new Validator().defined().min(0));
|
this.setValidation('size').defined().min(0);
|
||||||
this.addProperty('created_at', new Validator());
|
this.setValidation('ttl').defined().min(0).max(4294967295);
|
||||||
this.addProperty('ttl', new Validator().defined().min(0).max(4294967295));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getURL(domain: string = config.get<string>('base_url')): string {
|
public getURL(domain: string = config.get<string>('base_url')): string {
|
||||||
return (/^https?:\/\//.test(domain) ? '' : 'https://') + domain + Controller.route('get-file', {
|
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.created_at) return new Date();
|
||||||
if (this.ttl === 0) return null;
|
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 {
|
public shouldBeDeleted(): boolean {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import Model from "wms-core/db/Model";
|
import Model from "wms-core/db/Model";
|
||||||
import Validator from "wms-core/db/Validator";
|
|
||||||
import User from "wms-core/auth/models/User";
|
import User from "wms-core/auth/models/User";
|
||||||
import FileModel from "./FileModel";
|
import FileModel from "./FileModel";
|
||||||
import {Request} from "express";
|
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));
|
return await this.paginate(req, perPage, this.select().where('user_id', user_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
public readonly user_id!: number;
|
public id?: number = undefined;
|
||||||
public readonly slug!: string;
|
public readonly user_id?: number = undefined;
|
||||||
public readonly target_url!: string;
|
public readonly slug?: string = undefined;
|
||||||
public created_at?: Date;
|
public readonly target_url?: string = undefined;
|
||||||
|
public created_at?: Date = undefined;
|
||||||
|
|
||||||
constructor(data: any) {
|
constructor(data: any) {
|
||||||
super(data);
|
super(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected init(): void {
|
protected init(): void {
|
||||||
this.addProperty('user_id', new Validator().defined().exists(User, 'id'));
|
this.setValidation('user_id').defined().exists(User, 'id');
|
||||||
this.addProperty('slug', new Validator().defined().minLength(1).maxLength(259).unique(URLRedirect, 'slug').unique(FileModel, 'slug'));
|
this.setValidation('slug').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.setValidation('target_url').defined().maxLength(1745).regexp(/^https?:\/\/.{3,259}?\/?/i);
|
||||||
this.addProperty('created_at', new Validator());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getURL(domain: string = config.get<string>('base_url')): string {
|
public getURL(domain: string = config.get<string>('base_url')): string {
|
||||||
return (/^https?:\/\//.test(domain) ? '' : 'https://') + domain + Controller.route('get-url', {
|
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