Initial files
This commit is contained in:
parent
8149ecac71
commit
b8c1ed1139
@ -1,71 +1 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-share-2"><circle cx="18" cy="5" r="3"></circle><circle cx="6" cy="12" r="3"></circle><circle cx="18" cy="19" r="3"></circle><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"></line><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"></line></svg>
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
version="1.1"
|
|
||||||
id="svg6"
|
|
||||||
sodipodi:docname="logo.svg"
|
|
||||||
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
|
|
||||||
inkscape:export-filename="/r/arisu/dev/streams/wms/assets/img/logox1024.png"
|
|
||||||
inkscape:export-xdpi="4096"
|
|
||||||
inkscape:export-ydpi="4096">
|
|
||||||
<metadata
|
|
||||||
id="metadata12">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs10" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="2560"
|
|
||||||
inkscape:window-height="1381"
|
|
||||||
id="namedview8"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="45.254834"
|
|
||||||
inkscape:cx="12.300687"
|
|
||||||
inkscape:cy="13.723964"
|
|
||||||
inkscape:window-x="1920"
|
|
||||||
inkscape:window-y="32"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg6">
|
|
||||||
<inkscape:grid
|
|
||||||
spacingx="0.5"
|
|
||||||
spacingy="0.5"
|
|
||||||
type="xygrid"
|
|
||||||
id="grid4542" />
|
|
||||||
</sodipodi:namedview>
|
|
||||||
<path
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#00c3b2;fill-opacity:1;fill-rule:nonzero;stroke:#00c3b2;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
|
||||||
d="m 4.6020263,4.6020261 c -4.08077722,4.0820101 -4.08077722,10.7139369 0,14.7959479 m 0,0 0.6656889,-0.665689 c -3.7210992,-3.722224 -3.7210992,-9.7423466 0,-13.4645699 L 4.6020263,4.6020261 m 14.7959477,0 -0.665689,0.665689 c 3.721099,3.7222233 3.721099,9.7423459 0,13.4645699 l 0.665689,0.665689 m 0,0 c 4.080777,-4.082011 4.080777,-10.7139378 0,-14.7959479 M 8.0959735,15.904026 c -2.1644289,-2.162022 -2.1644289,-5.655225 0,-7.8172472 m 0,0 L 7.4302847,7.419251 c -2.5246945,2.5218873 -2.5246945,6.628577 0,9.150465 l 0.6656888,-0.66569 M 15.904026,8.0959736 c 2.164429,2.1620214 2.164429,5.6552244 0,7.8172474 m 0,0 0.66569,0.667527 c 2.524693,-2.521887 2.524693,-6.6285761 0,-9.1504633 l -0.66569,0.6656889"
|
|
||||||
id="path4"
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
sodipodi:nodetypes="cccccccccccccccccccccccc" />
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 445 B |
@ -1,2 +1,19 @@
|
|||||||
export default Object.assign(require("wms-core/config/default").default, {
|
export default Object.assign(require("wms-core/config/default").default, {
|
||||||
|
domain: 'localhost',
|
||||||
|
base_url: 'http://localhost:4893',
|
||||||
|
public_websocket_url: 'ws://localhost:4893',
|
||||||
|
port: 4893,
|
||||||
|
mysql: {
|
||||||
|
connectionLimit: 10,
|
||||||
|
host: "localhost",
|
||||||
|
user: "root",
|
||||||
|
password: "",
|
||||||
|
database: "ilyli",
|
||||||
|
create_database_automatically: false
|
||||||
|
},
|
||||||
|
magic_link: {
|
||||||
|
validity_period: 20
|
||||||
|
},
|
||||||
|
gitlab_webhook_token: 'secret',
|
||||||
|
newlyGeneratedSlugSize: 3,
|
||||||
});
|
});
|
@ -1,2 +1,17 @@
|
|||||||
export default Object.assign(require("wms-core/config/production").default, {
|
export default Object.assign(require("wms-core/config/production").default, {
|
||||||
|
domain: 'ily.li',
|
||||||
|
base_url: 'https://ily.li',
|
||||||
|
public_websocket_url: 'wss://ily.li',
|
||||||
|
mysql: {
|
||||||
|
connectionLimit: 10,
|
||||||
|
host: "localhost",
|
||||||
|
user: "root",
|
||||||
|
password: "",
|
||||||
|
database: "ilyli",
|
||||||
|
},
|
||||||
|
magic_link: {
|
||||||
|
validity_period: 900
|
||||||
|
},
|
||||||
|
gitlab_webhook_token: 'CHANGEME',
|
||||||
|
newlyGeneratedSlugSize: 5,
|
||||||
});
|
});
|
@ -1,2 +1,9 @@
|
|||||||
export default Object.assign(require("wms-core/config/test").default, {
|
export default Object.assign(require("wms-core/config/test").default, {
|
||||||
|
mysql: {
|
||||||
|
database: "ilyli_test",
|
||||||
|
create_database_automatically: true
|
||||||
|
},
|
||||||
|
magic_link: {
|
||||||
|
validity_period: 2
|
||||||
|
},
|
||||||
});
|
});
|
@ -44,7 +44,9 @@
|
|||||||
"wms-core": "^0"
|
"wms-core": "^0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@types/express-fileupload": "^1.1.3",
|
||||||
"config": "^3.3.1",
|
"config": "^3.3.1",
|
||||||
"express": "^4.17.1"
|
"express": "^4.17.1",
|
||||||
|
"express-fileupload": "^1.1.7-alpha.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,11 +16,27 @@ import RedirectBackComponent from "wms-core/components/RedirectBackComponent";
|
|||||||
import FormHelperComponent from "wms-core/components/FormHelperComponent";
|
import FormHelperComponent from "wms-core/components/FormHelperComponent";
|
||||||
import CsrfProtectionComponent from "wms-core/components/CsrfProtectionComponent";
|
import CsrfProtectionComponent from "wms-core/components/CsrfProtectionComponent";
|
||||||
import WebSocketServerComponent from "wms-core/components/WebSocketServerComponent";
|
import WebSocketServerComponent from "wms-core/components/WebSocketServerComponent";
|
||||||
import HomeController from "./controllers/HomeController";
|
import AboutController from "./controllers/AboutController";
|
||||||
import AutoUpdateComponent from "wms-core/components/AutoUpdateComponent";
|
import AutoUpdateComponent from "wms-core/components/AutoUpdateComponent";
|
||||||
|
import AuthController from "./controllers/AuthController";
|
||||||
|
import MagicLinkWebSocketListener from "wms-core/auth/magic_link/MagicLinkWebSocketListener";
|
||||||
|
import MagicLinkController from "./controllers/MagicLinkController";
|
||||||
|
import MailController from "wms-core/auth/MailController";
|
||||||
|
import FileController from "./controllers/FileController";
|
||||||
|
import CreateUsersAndUserEmailsTable from "wms-core/auth/migrations/CreateUsersAndUserEmailsTable";
|
||||||
|
import CreateMagicLinksTable from "wms-core/auth/migrations/CreateMagicLinksTable";
|
||||||
|
import CreateAuthTokensTable from "./migrations/CreateAuthTokensTable";
|
||||||
|
import AuthComponent from "wms-core/auth/AuthComponent";
|
||||||
|
import AuthGuard from "wms-core/auth/AuthGuard";
|
||||||
|
import MagicLink from "wms-core/auth/models/MagicLink";
|
||||||
|
import AuthToken from "./models/AuthToken";
|
||||||
|
import {MagicLinkActionType} from "./controllers/MagicLinkActionType";
|
||||||
|
import {Request} from "express";
|
||||||
|
import CreateFilesTable from "./migrations/CreateFilesTable";
|
||||||
|
|
||||||
export default class ExampleApp extends Application {
|
export default class App extends Application {
|
||||||
private readonly port: number;
|
private readonly port: number;
|
||||||
|
private magicLinkWebSocketListener?: MagicLinkWebSocketListener;
|
||||||
|
|
||||||
constructor(port: number) {
|
constructor(port: number) {
|
||||||
super(require('../package.json').version);
|
super(require('../package.json').version);
|
||||||
@ -31,6 +47,10 @@ export default class ExampleApp extends Application {
|
|||||||
return [
|
return [
|
||||||
CreateMigrationsTable,
|
CreateMigrationsTable,
|
||||||
CreateLogsTable,
|
CreateLogsTable,
|
||||||
|
CreateUsersAndUserEmailsTable,
|
||||||
|
CreateMagicLinksTable,
|
||||||
|
CreateAuthTokensTable,
|
||||||
|
CreateFilesTable,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,6 +86,20 @@ export default class ExampleApp extends Application {
|
|||||||
// Session
|
// Session
|
||||||
this.use(redisComponent);
|
this.use(redisComponent);
|
||||||
this.use(new SessionComponent(redisComponent));
|
this.use(new SessionComponent(redisComponent));
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getProofForRequest(req: Request): Promise<MagicLink | AuthToken | null> {
|
||||||
|
const authorization = req.header('Authorization');
|
||||||
|
if (authorization) {
|
||||||
|
return await AuthToken.getBySecret(authorization);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.getProofForRequest(req);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
this.use(new RedirectBackComponent());
|
this.use(new RedirectBackComponent());
|
||||||
@ -79,9 +113,22 @@ export default class ExampleApp extends Application {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private registerWebSocketListeners() {
|
private registerWebSocketListeners() {
|
||||||
|
this.magicLinkWebSocketListener = new MagicLinkWebSocketListener();
|
||||||
|
this.use(this.magicLinkWebSocketListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerControllers() {
|
private registerControllers() {
|
||||||
this.use(new HomeController());
|
// Priority
|
||||||
|
this.use(new AuthController());
|
||||||
|
this.use(new MagicLinkController(this.magicLinkWebSocketListener!));
|
||||||
|
|
||||||
|
// Core functionality
|
||||||
|
this.use(new MailController());
|
||||||
|
|
||||||
|
// Other functionality
|
||||||
|
this.use(new FileController());
|
||||||
|
|
||||||
|
// Semi-static
|
||||||
|
this.use(new AboutController());
|
||||||
}
|
}
|
||||||
}
|
}
|
6
src/MagicLinkMail.ts
Normal file
6
src/MagicLinkMail.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import {MailTemplate} from "wms-core/Mail";
|
||||||
|
|
||||||
|
export const MAGIC_LINK_MAIL = new MailTemplate(
|
||||||
|
'magic_link',
|
||||||
|
data => data.type === 'register' ? 'Registration' : 'Login magic link'
|
||||||
|
);
|
@ -1,16 +1,11 @@
|
|||||||
import Controller from "wms-core/Controller";
|
import Controller from "wms-core/Controller";
|
||||||
import {Request, Response} from "express";
|
import {Request, Response} from "express";
|
||||||
|
|
||||||
export default class HomeController extends Controller {
|
export default class AboutController extends Controller {
|
||||||
routes(): void {
|
routes(): void {
|
||||||
this.get('/', this.getHome, 'home');
|
|
||||||
this.get('/about', this.getAbout, 'about');
|
this.get('/about', this.getAbout, 'about');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getHome(req: Request, res: Response) {
|
|
||||||
res.render('home');
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getAbout(req: Request, res: Response) {
|
private async getAbout(req: Request, res: Response) {
|
||||||
res.render('about');
|
res.render('about');
|
||||||
}
|
}
|
8
src/controllers/AuthController.ts
Normal file
8
src/controllers/AuthController.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import MagicLinkAuthController from "wms-core/auth/magic_link/MagicLinkAuthController";
|
||||||
|
import {MAGIC_LINK_MAIL} from "../MagicLinkMail";
|
||||||
|
|
||||||
|
export default class AuthController extends MagicLinkAuthController {
|
||||||
|
public constructor() {
|
||||||
|
super(MAGIC_LINK_MAIL);
|
||||||
|
}
|
||||||
|
}
|
96
src/controllers/FileController.ts
Normal file
96
src/controllers/FileController.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import Controller from "wms-core/Controller";
|
||||||
|
import {REQUIRE_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent";
|
||||||
|
import {Request, Response} from "express";
|
||||||
|
import {BadRequestError, NotFoundHttpError, ServerError} from "wms-core/HttpError";
|
||||||
|
import FileModel from "../models/FileModel";
|
||||||
|
import fileUpload from "express-fileupload";
|
||||||
|
import {cryptoRandomDictionary} from "wms-core/Utils";
|
||||||
|
import config from "config";
|
||||||
|
|
||||||
|
const FILE_UPLOAD_MIDDLEWARE = fileUpload({
|
||||||
|
tempFileDir: 'storage/tmp',
|
||||||
|
useTempFiles: true,
|
||||||
|
abortOnLimit: true,
|
||||||
|
createParentPath: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const SLUG_DICTIONARY = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
export default class FileController extends Controller {
|
||||||
|
routes(): void {
|
||||||
|
this.get('/f/:slug', this.downloadFile, 'get-file');
|
||||||
|
this.post('/f', this.postFile, 'post-file', REQUIRE_AUTH_MIDDLEWARE, FILE_UPLOAD_MIDDLEWARE);
|
||||||
|
this.put('/f/:slug', this.putFile, 'put-file', REQUIRE_AUTH_MIDDLEWARE, FILE_UPLOAD_MIDDLEWARE);
|
||||||
|
this.delete('/f/:slug', this.deleteFile, 'delete-file', REQUIRE_AUTH_MIDDLEWARE);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async downloadFile(req: Request, res: Response): Promise<void> {
|
||||||
|
const file = await FileModel.getBySlug(req.params.slug);
|
||||||
|
if (!file) throw new NotFoundHttpError('File', req.url);
|
||||||
|
|
||||||
|
switch (file.storage_type) {
|
||||||
|
case 'local':
|
||||||
|
res.download(file.storage_path, file.real_name);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ServerError(`This file cannot be served. Download protocol for ${file.storage_type} storage type not implemented.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async postFile(req: Request, res: Response): Promise<void> {
|
||||||
|
await this.handleFileUpload(await this.generateSlug(10), req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async putFile(req: Request, res: Response): Promise<void> {
|
||||||
|
const slug = req.params.slug;
|
||||||
|
if (!slug) throw new BadRequestError('Cannot put without a slug.', 'Either provide a slug or use POST method instead.', req.url);
|
||||||
|
|
||||||
|
await this.handleFileUpload(slug, req, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handleFileUpload(slug: string, req: Request, res: Response): Promise<void> {
|
||||||
|
if (!req.files || !req.files.upload) {
|
||||||
|
throw new BadRequestError('No file received.', 'You must upload exactly one (1) file.', req.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
let upload = req.files.upload;
|
||||||
|
if (Array.isArray(upload) && upload.length !== 1) {
|
||||||
|
throw new BadRequestError((req.files ? req.files.length : 0) + ' files received.', 'You must upload exactly one (1) file.', req.url);
|
||||||
|
}
|
||||||
|
if (Array.isArray(upload)) upload = upload[0];
|
||||||
|
|
||||||
|
const file = new FileModel({
|
||||||
|
slug: slug,
|
||||||
|
real_name: upload.name,
|
||||||
|
storage_type: 'local',
|
||||||
|
storage_path: 'storage/uploads/' + slug,
|
||||||
|
});
|
||||||
|
|
||||||
|
await file.save();
|
||||||
|
await upload.mv(file.storage_path);
|
||||||
|
|
||||||
|
res.format({
|
||||||
|
json: () => res.json({
|
||||||
|
url: file.getURL(),
|
||||||
|
}),
|
||||||
|
text: () => res.send(file.getURL()),
|
||||||
|
html: () => res.render('upload_success', {url: file.getURL()}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async deleteFile(req: Request, res: Response): Promise<void> {
|
||||||
|
const file = await FileModel.getBySlug(req.params.slug);
|
||||||
|
if (!file) throw new NotFoundHttpError('File', req.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async generateSlug(tries: number): Promise<string> {
|
||||||
|
let i = 0;
|
||||||
|
do {
|
||||||
|
const slug = cryptoRandomDictionary(config.get<number>('newlyGeneratedSlugSize'), SLUG_DICTIONARY);
|
||||||
|
if (!await FileModel.getBySlug(slug)) {
|
||||||
|
return slug;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
} while (i < tries)
|
||||||
|
throw new ServerError('Failed to generate slug; newly generated slug size should be increased by 1.');
|
||||||
|
}
|
||||||
|
}
|
17
src/controllers/MagicLinkActionType.ts
Normal file
17
src/controllers/MagicLinkActionType.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import MagicLink from "wms-core/auth/models/MagicLink";
|
||||||
|
|
||||||
|
export enum MagicLinkActionType {
|
||||||
|
LOGIN = 'Login',
|
||||||
|
REGISTER = 'Register',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActionMessage(magicLink: MagicLink): string {
|
||||||
|
switch (magicLink.getActionType()) {
|
||||||
|
case MagicLinkActionType.LOGIN:
|
||||||
|
return 'You have been authenticated.';
|
||||||
|
case MagicLinkActionType.REGISTER:
|
||||||
|
return 'You have been registered.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
27
src/controllers/MagicLinkController.ts
Normal file
27
src/controllers/MagicLinkController.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import _MagicLinkController from "wms-core/auth/magic_link/MagicLinkController";
|
||||||
|
import {Request, Response} from "express";
|
||||||
|
import Controller from "wms-core/Controller";
|
||||||
|
import MagicLinkWebSocketListener from "wms-core/auth/magic_link/MagicLinkWebSocketListener";
|
||||||
|
import MagicLink from "wms-core/auth/models/MagicLink";
|
||||||
|
import AuthController from "./AuthController";
|
||||||
|
import {MagicLinkActionType} from "./MagicLinkActionType";
|
||||||
|
|
||||||
|
export default class MagicLinkController extends _MagicLinkController {
|
||||||
|
constructor(magicLinkWebSocketListener: MagicLinkWebSocketListener) {
|
||||||
|
super(magicLinkWebSocketListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async performAction(magicLink: MagicLink, req: Request, res: Response): Promise<void> {
|
||||||
|
switch (magicLink.getActionType()) {
|
||||||
|
case MagicLinkActionType.LOGIN:
|
||||||
|
case MagicLinkActionType.REGISTER:
|
||||||
|
await AuthController.checkAndAuth(req, magicLink);
|
||||||
|
|
||||||
|
// Auth success
|
||||||
|
const user = await req.authGuard.getUserForSession(req.session!);
|
||||||
|
req.flash('success', `Authentication success. Welcome, ${user?.name}!`);
|
||||||
|
res.redirect(Controller.route('home'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import Logger from "wms-core/Logger";
|
import Logger from "wms-core/Logger";
|
||||||
import ExampleApp from "./ExampleApp";
|
import App from "./App";
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const app = new ExampleApp(4899);
|
const app = new App(4899);
|
||||||
await app.start();
|
await app.start();
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
Logger.error(err);
|
Logger.error(err);
|
||||||
|
20
src/migrations/CreateAuthTokensTable.ts
Normal file
20
src/migrations/CreateAuthTokensTable.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {Connection} from "mysql";
|
||||||
|
import Migration from "wms-core/db/Migration";
|
||||||
|
|
||||||
|
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(),' +
|
||||||
|
'ttl INT NOT NULL,' +
|
||||||
|
'PRIMARY KEY (id)' +
|
||||||
|
')', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async rollback(connection: Connection): Promise<void> {
|
||||||
|
await this.query('DROP TABLE IF EXISTS auth_tokens', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
20
src/migrations/CreateFilesTable.ts
Normal file
20
src/migrations/CreateFilesTable.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {Connection} from "mysql";
|
||||||
|
import Migration from "wms-core/db/Migration";
|
||||||
|
|
||||||
|
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,' +
|
||||||
|
'slug VARCHAR(259) UNIQUE NOT NULL,' +
|
||||||
|
'real_name VARCHAR(259) NOT NULL,' +
|
||||||
|
'storage_type VARCHAR(64) NOT NULL,' +
|
||||||
|
'storage_path VARCHAR(1745) NOT NULL,' +
|
||||||
|
'PRIMARY KEY (id)' +
|
||||||
|
')', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async rollback(connection: Connection): Promise<void> {
|
||||||
|
await this.query('DROP TABLE IF EXISTS files', connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
57
src/models/AuthToken.ts
Normal file
57
src/models/AuthToken.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
export default class AuthToken extends Model implements AuthProof {
|
||||||
|
public static async getBySecret(secret: string): Promise<AuthToken | null> {
|
||||||
|
const models = await this.models<AuthToken>(this.select().where('secret', secret).first());
|
||||||
|
return models.length > 0 ? models[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly user_id!: number;
|
||||||
|
protected readonly secret!: string;
|
||||||
|
protected created_at?: Date;
|
||||||
|
protected readonly ttl!: number;
|
||||||
|
|
||||||
|
|
||||||
|
protected defineProperties() {
|
||||||
|
this.defineProperty('user_id', new Validator().defined().exists(User, 'id'));
|
||||||
|
this.defineProperty('secret', new Validator().defined().between(32, 64));
|
||||||
|
this.defineProperty('created_at', new Validator());
|
||||||
|
this.defineProperty('ttl', new Validator().defined().min(1).max(5 * 365 * 24 * 3600)); // max 5 years
|
||||||
|
}
|
||||||
|
|
||||||
|
public getExpirationDate(): Date {
|
||||||
|
if (!this.created_at) return new Date();
|
||||||
|
|
||||||
|
return new Date(this.created_at.getTime() + this.ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getEmail(): Promise<string> {
|
||||||
|
let userEmail = await UserEmail.getMainFromUser(this.user_id);
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async isAuthorized(): Promise<boolean> {
|
||||||
|
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> {
|
||||||
|
await this.delete();
|
||||||
|
}
|
||||||
|
}
|
35
src/models/FileModel.ts
Normal file
35
src/models/FileModel.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import Model from "wms-core/db/Model";
|
||||||
|
import Validator from "wms-core/db/Validator";
|
||||||
|
import Controller from "wms-core/Controller";
|
||||||
|
import config from "config";
|
||||||
|
|
||||||
|
export default class FileModel extends Model {
|
||||||
|
public static get table(): string {
|
||||||
|
return 'files';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getBySlug(slug: string): Promise<FileModel | null> {
|
||||||
|
const models = await this.models<FileModel>(this.select().where('slug', slug).first());
|
||||||
|
return models.length > 0 ? models[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly slug!: string;
|
||||||
|
public readonly real_name!: string;
|
||||||
|
public readonly storage_type!: FileStorage;
|
||||||
|
public readonly storage_path!: string;
|
||||||
|
|
||||||
|
protected defineProperties() {
|
||||||
|
this.defineProperty('slug', new Validator().defined().minLength(1).maxLength(259).unique(this, 'slug'));
|
||||||
|
this.defineProperty('real_name', new Validator().defined().minLength(1).maxLength(259));
|
||||||
|
this.defineProperty('storage_type', new Validator().defined().maxLength(64));
|
||||||
|
this.defineProperty('storage_path', new Validator().defined().maxLength(1745));
|
||||||
|
}
|
||||||
|
|
||||||
|
public getURL(): string {
|
||||||
|
return config.get<string>('base_url') + Controller.route('get-file', {
|
||||||
|
slug: this.slug,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FileStorage = 'local';
|
0
storage/.gitkeep
Normal file
0
storage/.gitkeep
Normal file
@ -1,14 +1,14 @@
|
|||||||
{% extends 'layouts/base.njk' %}
|
{% extends 'layouts/base.njk' %}
|
||||||
|
|
||||||
{% set title = 'Example App - About us' %}
|
{% set title = 'ily.li - About' %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h1>Very interesting</h1>
|
<h1>Self-hosting files</h1>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>This is us</h2>
|
<h2>Details</h2>
|
||||||
<p class="center">And we like wms!</p>
|
<p class="center">Powered by wms-core</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -1,7 +0,0 @@
|
|||||||
{% extends 'layouts/base.njk' %}
|
|
||||||
|
|
||||||
{% set title = 'Example App - Hello world!' %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
<h1>Hello world!</h1>
|
|
||||||
{% endblock %}
|
|
115
views/mails/base_layout.mjml.njk
Normal file
115
views/mails/base_layout.mjml.njk
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
{% if not text %}
|
||||||
|
<mjml>
|
||||||
|
<mj-head>
|
||||||
|
<mj-title>{{ mail_subject }}</mj-title>
|
||||||
|
<mj-font
|
||||||
|
name="Nunito"
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Nunito"
|
||||||
|
/>
|
||||||
|
<mj-attributes>
|
||||||
|
<mj-all
|
||||||
|
color="#f0f0f0"
|
||||||
|
font-size="16px"
|
||||||
|
font-family="Nunito, Helvetica, sans-serif"
|
||||||
|
padding="0"
|
||||||
|
/>
|
||||||
|
<mj-section
|
||||||
|
full-width="full-width"
|
||||||
|
padding="16px 16px"
|
||||||
|
background-color="#1e2932"
|
||||||
|
/>
|
||||||
|
<mj-button
|
||||||
|
background-color="#00a99b"
|
||||||
|
text-transform="uppercase"
|
||||||
|
inner-padding="16px 32px"
|
||||||
|
border-radius="50px"
|
||||||
|
font-weight="700"
|
||||||
|
padding="32px"
|
||||||
|
/>
|
||||||
|
<mj-text
|
||||||
|
padding="8px 0px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<mj-class name="header"
|
||||||
|
align="center"
|
||||||
|
font-size="20px"
|
||||||
|
background-color="#1b252d"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<mj-class name="footer"
|
||||||
|
align="center"
|
||||||
|
background-color="#151d23"
|
||||||
|
font-size="14px"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<mj-class name="title"
|
||||||
|
align="center"
|
||||||
|
font-size="28px"
|
||||||
|
padding-bottom="24px"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<mj-class name="subtitle"
|
||||||
|
align="center"
|
||||||
|
font-size="22px"
|
||||||
|
padding-bottom="16px"
|
||||||
|
align="center"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<mj-class name="link"
|
||||||
|
color="#00766c"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<mj-class name="important-line"
|
||||||
|
font-size="20px"
|
||||||
|
padding="16px 0px"
|
||||||
|
/>
|
||||||
|
</mj-attributes>
|
||||||
|
<mj-style inline="inline">
|
||||||
|
.link {
|
||||||
|
color: #00766c !important;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
</mj-style>
|
||||||
|
<mj-style>
|
||||||
|
.link:hover {
|
||||||
|
color: #00a99b !important;
|
||||||
|
}
|
||||||
|
</mj-style>
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</mj-head>
|
||||||
|
<mj-body width="632px">
|
||||||
|
{% if mail_link %}
|
||||||
|
<mj-section background-color="#171f26" padding="2px 0px">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text align="center" font-size="12px">
|
||||||
|
Does this mail display improperly?
|
||||||
|
<a href="{{ mail_link }}" class="link">Open it in the browser</a>
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<mj-section mj-class="header">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="header">
|
||||||
|
ily.li
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
{% block body %}{% endblock %}
|
||||||
|
|
||||||
|
<mj-section mj-class="footer">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="footer">
|
||||||
|
All rights reserved. Contact us at
|
||||||
|
<a href="mailto:(contact email)" class="link">(contact email)</a>
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
</mj-body>
|
||||||
|
</mjml>
|
||||||
|
{% else %}
|
||||||
|
{% block text %}{% endblock %}
|
||||||
|
{% endif %}
|
66
views/mails/magic_link.mjml.njk
Normal file
66
views/mails/magic_link.mjml.njk
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
{% extends './base_layout.mjml.njk' %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<mj-section>
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="title">
|
||||||
|
{% if type == 'register' %}
|
||||||
|
Register an account on (app)
|
||||||
|
{% else %}
|
||||||
|
Log in to (app)
|
||||||
|
{% endif %}
|
||||||
|
</mj-text>
|
||||||
|
<mj-text>
|
||||||
|
{% if type == 'register' %}
|
||||||
|
Someone has requested an account registration for <strong>{{ mail_to }}</strong>. If it was not you,
|
||||||
|
please ignore this message.
|
||||||
|
{% else %}
|
||||||
|
Someone is attempting to log in to your account <strong>{{ mail_to }}</strong>.
|
||||||
|
{% endif %}
|
||||||
|
</mj-text>
|
||||||
|
|
||||||
|
{% if type == 'register' %}
|
||||||
|
<mj-button href="{{ link | safe }}">Finalize my account registration</mj-button>
|
||||||
|
{% else %}
|
||||||
|
<mj-text>If it is not you, <strong>DO NOT CLICK ON THIS BUTTON</strong>.</mj-text>
|
||||||
|
{% endif %}
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
|
||||||
|
{% if type == 'login' %}
|
||||||
|
<mj-section background-color="#1b252d">
|
||||||
|
<mj-column>
|
||||||
|
<mj-text mj-class="important-line" padding-bottom="0px">
|
||||||
|
IP: <strong>{{ ip }}</strong>
|
||||||
|
</mj-text>
|
||||||
|
<mj-text mj-class="important-line">
|
||||||
|
Location: <strong>{{ geo }}</strong>
|
||||||
|
</mj-text>
|
||||||
|
</mj-column>
|
||||||
|
<mj-column>
|
||||||
|
<mj-button href="{{ link | safe }}" padding="20px 0" background-color="#caa200">
|
||||||
|
Authorize log in
|
||||||
|
</mj-button>
|
||||||
|
</mj-column>
|
||||||
|
</mj-section>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block text %}
|
||||||
|
{% if type == 'register' %}
|
||||||
|
Hi!
|
||||||
|
Someone requested an account registration for {{ mail_to }}. If it was not you,
|
||||||
|
please ignore this message.
|
||||||
|
|
||||||
|
To finalize your account registration, please follow this link: {{ link|safe }}
|
||||||
|
{% else %}
|
||||||
|
Hi!
|
||||||
|
Someone is attempting to log in to your account {{ mail_to }}.
|
||||||
|
|
||||||
|
If it is not you, DO NOT FOLLOW THIS LINK.
|
||||||
|
|
||||||
|
IP: {{ ip }}
|
||||||
|
Location: {{ geo }}
|
||||||
|
To authorize this log in, please follow this link: {{ link|safe }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
Loading…
Reference in New Issue
Block a user