From 162063025cccf3006c33a096da845e808556da83 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Sat, 25 Apr 2020 09:38:04 +0200 Subject: [PATCH 01/48] Add missing form.js for field value update (:focus style) --- assets/js/app.js | 1 + assets/js/forms.js | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 assets/js/forms.js diff --git a/assets/js/app.js b/assets/js/app.js index fc7c539..ce75569 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -1,5 +1,6 @@ import './external_links'; import './message_icons'; +import './forms'; import '../sass/app.scss'; diff --git a/assets/js/forms.js b/assets/js/forms.js new file mode 100644 index 0000000..4161467 --- /dev/null +++ b/assets/js/forms.js @@ -0,0 +1,11 @@ +// For labels to update their state (css selectors based on the value attribute) +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('input').forEach(el => { + if (el.type !== 'checkbox') { + el.setAttribute('value', el.value); + el.addEventListener('change', () => { + el.setAttribute('value', el.value); + }); + } + }); +}); \ No newline at end of file From 8332c439ad2abef09681172a3c8aa3123098cea2 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Sat, 25 Apr 2020 09:38:25 +0200 Subject: [PATCH 02/48] Add validation attributes to select fields --- views/macros.njk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/views/macros.njk b/views/macros.njk index 6ba1583..034b77b 100644 --- a/views/macros.njk +++ b/views/macros.njk @@ -63,7 +63,7 @@ {% endfor %} {% elseif type == 'select' %} - {% for option in extraData %} {% endfor %} From e75a7536c77248afeb0a4b7b4248b3f1addc3bc0 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Sat, 25 Apr 2020 09:39:59 +0200 Subject: [PATCH 03/48] Add different appearance for disabled fields --- assets/sass/layout.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/assets/sass/layout.scss b/assets/sass/layout.scss index 0eb03a5..949d653 100644 --- a/assets/sass/layout.scss +++ b/assets/sass/layout.scss @@ -179,6 +179,15 @@ form { cursor: text; } + [disabled] { + opacity: 0.5; + + & ~ label { + opacity: 0.5; + cursor: default; + } + } + input, select, .input-group { border: 0; border-bottom: 2px solid #0008; From ea7840d683d30ef034703b52d36bc4a67532f706 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Sat, 25 Apr 2020 09:40:25 +0200 Subject: [PATCH 04/48] Add util class for feather icons that are in the end of a button --- assets/sass/layout.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/assets/sass/layout.scss b/assets/sass/layout.scss index 949d653..ba9c811 100644 --- a/assets/sass/layout.scss +++ b/assets/sass/layout.scss @@ -317,6 +317,11 @@ button, .button { margin-right: 8px; } + .feather.last { + margin-right: 0; + margin-left: 8px; + } + &, &.primary { color: $primaryForeground; background-color: $secondary; From 0f48bd55b6b0da4c16b3faf051176d8d7274c54e Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Sat, 25 Apr 2020 10:11:05 +0200 Subject: [PATCH 05/48] Add inline-fields class --- assets/sass/layout.scss | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/assets/sass/layout.scss b/assets/sass/layout.scss index ba9c811..d221be7 100644 --- a/assets/sass/layout.scss +++ b/assets/sass/layout.scss @@ -296,6 +296,22 @@ form { color: $error; } } + + .inline-fields { + display: flex; + flex-direction: row; + align-items: center; + margin: 16px auto; + + .form-field { + flex: 1; + margin: 0; + } + + & > :not(.form-field) { + padding: 32px 8px 8px 8px; + } + } } button, .button { From 50b01619ed2d072d929a7c72a56b18c7301f9017 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Sat, 25 Apr 2020 11:27:12 +0200 Subject: [PATCH 06/48] Separate field error into a macro and add support for custom error in inline fields --- assets/sass/layout.scss | 39 ++++++++++++++++++++++++--------------- views/macros.njk | 12 +++++++++--- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/assets/sass/layout.scss b/assets/sass/layout.scss index d221be7..e5643b5 100644 --- a/assets/sass/layout.scss +++ b/assets/sass/layout.scss @@ -281,20 +281,6 @@ form { } } } - - .error, .hint { - padding: 2px; - text-align: left; - font-size: 14px; - - .feather { - --icon-size: 14px; - } - } - - .error { - color: $error; - } } .inline-fields { @@ -308,9 +294,32 @@ form { margin: 0; } - & > :not(.form-field) { + > :not(.form-field) { padding: 32px 8px 8px 8px; } + + + { + .error, .hint { + margin-top: -16px; + margin-bottom: 16px; + } + } + } + + .form-field, .inline-fields + { + .error, .hint { + padding: 2px; + text-align: left; + font-size: 14px; + + .feather { + --icon-size: 14px; + } + } + + .error { + color: $error; + } } } diff --git a/views/macros.njk b/views/macros.njk index 034b77b..789700a 100644 --- a/views/macros.njk +++ b/views/macros.njk @@ -77,9 +77,7 @@ {% endif %} - {% if validation %} -
{{ validation.message }}
- {% endif %} + {{ fieldError(_locals, name) }} {% if hint %}
{{ hint }}
{% endif %} @@ -87,6 +85,14 @@ {% endif %} {% endmacro %} +{% macro fieldError(_locals, name) %} + {% set validation = _locals.validation() %} + {% set validation = validation[name] if validation[name] or null %} + {% if validation %} +
{{ validation.message }}
+ {% endif %} +{% endmacro %} + {% macro websocket(websocketUrl, listener, reconnectOnClose = 1, checkFunction = 0) %} - {% endblock %} - - - -
- {% block header %}{% endblock %} -
- -{% block _body %}{% endblock %} - -
{% block footer %}{% endblock %}
- - - \ No newline at end of file diff --git a/views/layouts/base.njk b/views/layouts/base.njk index f87c6aa..cb76570 100644 --- a/views/layouts/base.njk +++ b/views/layouts/base.njk @@ -1,4 +1,4 @@ -{% extends './barebone.njk' %} +{% extends 'layouts/barebone.njk' %} {% import 'macros.njk' as macros %} {% block _stylesheets %} diff --git a/views/macros.njk b/views/macros.njk deleted file mode 100644 index 789700a..0000000 --- a/views/macros.njk +++ /dev/null @@ -1,142 +0,0 @@ -{% macro message(type, content, raw=false, discreet=false) %} -
- - - {{ content|safe if raw else content }} - -
-{% endmacro %} - -{% macro messages(flash) %} - {% set flashed = flash() %} - {% set display = 0 %} - - {% for type, bag in flashed %} - {% if bag|length %} - {% set display = 1 %} - {% endif %} - {% endfor %} - - {% if display %} -
- {% for type, bag in flashed %} - {% for content in bag %} - {{ message(type, content) }} - {% endfor %} - {% endfor %} -
- {% endif %} -{% endmacro %} - -{% macro csrf(getCSRFToken) %} - -{% endmacro %} - -{% macro field(_locals, type, name, value, placeholder, hint, validation_attributes='', extraData='') %} - {% set validation = _locals.validation() %} - {% set validation = validation[name] if validation[name] or null %} - {% set previousFormData = _locals.previousFormData() %} - {% set value = previousFormData[name] or value or validation.value or '' %} - - {% if type == 'hidden' %} - {% if validation %} - {{ message('error', validation.message) }} - {% endif %} - - {% else %} -
- {% if type == 'duration' %} -
- {% for f in extraData %} -
- {% if previousFormData[name] %} - {% set v = value[f] %} - {% else %} - {% set v = (value % 60) if f == 's' else (((value - value % 60) / 60 % 60) if f == 'm' else ((value - value % 3600) / 3600 if f == 'h')) %} - {% endif %} - - -
- {% endfor %} -
- {% elseif type == 'select' %} - - - {% else %} - - {% endif %} - - - {{ fieldError(_locals, name) }} - {% if hint %} -
{{ hint }}
- {% endif %} -
- {% endif %} -{% endmacro %} - -{% macro fieldError(_locals, name) %} - {% set validation = _locals.validation() %} - {% set validation = validation[name] if validation[name] or null %} - {% if validation %} -
{{ validation.message }}
- {% endif %} -{% endmacro %} - -{% macro websocket(websocketUrl, listener, reconnectOnClose = 1, checkFunction = 0) %} - -{% endmacro %} - -{% macro paginate(pagination, routeName) %} - {% if pagination.hasPrevious() or pagination.hasNext() %} - - {% endif %} -{% endmacro %} \ No newline at end of file From 5f4bcae38cef09beed27a7eeeda376ad044b6162 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Sun, 12 Jul 2020 12:13:26 +0200 Subject: [PATCH 32/48] Rename ExampleApp.test.ts to more useful App.test.ts --- test/{ExampleApp.test.ts => App.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{ExampleApp.test.ts => App.test.ts} (100%) diff --git a/test/ExampleApp.test.ts b/test/App.test.ts similarity index 100% rename from test/ExampleApp.test.ts rename to test/App.test.ts From e7e5981af741c74c8c99e4cb5b4cecf3216d4a45 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Sun, 12 Jul 2020 12:15:22 +0200 Subject: [PATCH 33/48] Improve progress bar design --- assets/sass/layout.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/sass/layout.scss b/assets/sass/layout.scss index e91200e..a818460 100644 --- a/assets/sass/layout.scss +++ b/assets/sass/layout.scss @@ -652,6 +652,7 @@ button, .button { top: 0; width: var(--progress); height: 100%; + transition: width ease-out 150ms; background: $secondary; } From c87eec28b23461cfb3d109ecc417b01af751954f Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Wed, 15 Jul 2020 11:04:45 +0200 Subject: [PATCH 34/48] Make icon only menu layout default --- assets/sass/layout.scss | 94 ++++++++++++++++++++++++++++++++++++++--- views/layouts/base.njk | 2 +- 2 files changed, 89 insertions(+), 7 deletions(-) diff --git a/assets/sass/layout.scss b/assets/sass/layout.scss index a818460..9890bcc 100644 --- a/assets/sass/layout.scss +++ b/assets/sass/layout.scss @@ -63,21 +63,54 @@ header { li { list-style: none; - a, span { + a, button { + position: relative; + height: 64px; + margin: 0; + padding: 0 24px; + display: flex; flex-direction: row; align-items: center; - height: 64px; - padding: 0 24px; + + &:hover, &:active { + &:not(button) { + background-color: rgba(255, 255, 255, 0.07); + } + + .tip { + visibility: visible; + opacity: 1; + transition: opacity ease-out 100ms; + transition-delay: 150ms; + } + } .feather { --icon-size: 24px; - margin-right: 10px; } } - a:hover, a:active { - background-color: rgba(255, 255, 255, 0.07); + button { + margin: 8px; + padding: 24px; + height: 32px; + + .feather { + margin-right: 0; + } + + .tip { + text-transform: initial; + font-weight: initial; + } + } + + form { + display: flex; + justify-content: center; + align-items: center; + padding: 0; } } } @@ -129,6 +162,55 @@ header { &.open { transform: translateX(0%); } + + li { + a, button { + .tip { + display: block; + margin-left: 8px; + text-transform: inherit; + font-weight: inherit; + } + } + } + } + } + } + + @media (min-width: $menuLayoutSwitchTreshold) { + nav ul li { + a, button { + .tip { + visibility: hidden; + position: absolute; + display: block; + width: max-content; + height: 30px; + padding: 4px 8px; + line-height: 22px; + top: calc(100% + 8px); + left: 50%; + transform: translateX(-50%); + + text-align: center; + font-size: 18px; + color: $defaultTextColor; + opacity: 0; + transition: opacity ease-out 100ms, visibility step-end 150ms; + transition-delay: 0ms; + background-color: #000; + border-radius: 5px; + } + } + + &:last-child { + a, button { + .tip { + left: unset; + right: 4px; + transform: none; + } + } } } } diff --git a/views/layouts/base.njk b/views/layouts/base.njk index cb76570..f350e8e 100644 --- a/views/layouts/base.njk +++ b/views/layouts/base.njk @@ -16,7 +16,7 @@ {% endblock %} From cb9d28128fb716797f092f75ac1fdfddad54993d Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Mon, 20 Jul 2020 16:34:35 +0200 Subject: [PATCH 35/48] Update codebase to latest wms-core and add recovery email add form --- config/default.ts | 4 +- config/production.ts | 5 +- src/Aldap.ts | 101 ------------------------- src/App.ts | 47 +++++++++++- src/LDAPServerComponent.ts | 20 +---- src/controllers/AccountController.ts | 21 +++++ src/controllers/AuthController.ts | 25 +++--- src/controllers/MagicLinkActionType.ts | 3 + src/controllers/MagicLinkController.ts | 35 +++++++++ src/controllers/PreLaunchWall.ts | 51 ------------- src/migrations.ts | 13 ---- src/models/UserPassword.ts | 30 +++++--- src/models/Username.ts | 26 ++++--- views/account.njk | 31 ++++++++ views/layouts/base.njk | 22 ++++-- views/login.njk | 2 +- views/prelaunch-wall.njk | 19 ----- views/register.njk | 4 +- 18 files changed, 208 insertions(+), 251 deletions(-) delete mode 100644 src/Aldap.ts create mode 100644 src/controllers/AccountController.ts create mode 100644 src/controllers/MagicLinkActionType.ts create mode 100644 src/controllers/MagicLinkController.ts delete mode 100644 src/controllers/PreLaunchWall.ts delete mode 100644 src/migrations.ts create mode 100644 views/account.njk delete mode 100644 views/prelaunch-wall.njk diff --git a/config/default.ts b/config/default.ts index 863e1da..9ecdeb9 100644 --- a/config/default.ts +++ b/config/default.ts @@ -24,7 +24,8 @@ export default Object.assign(require("wms-core/config/default").default, { session: { secret: "very_secret_not_known", cookie: { - secure: false + secure: false, + maxAge: 30 * 24 * 3600 * 1000, } }, mail: { @@ -41,5 +42,4 @@ export default Object.assign(require("wms-core/config/default").default, { cache: false }, approval_mode: false, - 'prelaunch-password': '$argon2i$v=19$m=4096,t=3,p=1$V7njt+IBmIQ/epc7tuQcfA$ypJCNauYSPrjOhtb5UqTbRlqCHkEGikBApOrYmbdYC0', }); \ No newline at end of file diff --git a/config/production.ts b/config/production.ts index 36feb69..f509007 100644 --- a/config/production.ts +++ b/config/production.ts @@ -1,9 +1,8 @@ export default Object.assign(require("wms-core/config/production").default, { - 'prelaunch-password': 'CHANGE ME', log_level: "DEBUG", db_log_level: "ERROR", - public_url: "https://watch-my.stream", - public_websocket_url: "wss://watch-my.stream", + public_url: "https://aldap.toot.party", + public_websocket_url: "wss://aldap.toot.party", session: { cookie: { secure: true diff --git a/src/Aldap.ts b/src/Aldap.ts deleted file mode 100644 index cc7672c..0000000 --- a/src/Aldap.ts +++ /dev/null @@ -1,101 +0,0 @@ -import Application from "wms-core/Application"; -import {Type} from "wms-core/Utils"; -import Migration from "wms-core/db/Migration"; -import ExpressAppComponent from "wms-core/components/ExpressAppComponent"; -import NunjucksComponent from "wms-core/components/NunjucksComponent"; -import MysqlComponent from "wms-core/components/MysqlComponent"; -import LogRequestsComponent from "wms-core/components/LogRequestsComponent"; -import RedisComponent from "wms-core/components/RedisComponent"; -import ServeStaticDirectoryComponent from "wms-core/components/ServeStaticDirectoryComponent"; -import MaintenanceComponent from "wms-core/components/MaintenanceComponent"; -import MailComponent from "wms-core/components/MailComponent"; -import SessionComponent from "wms-core/components/SessionComponent"; -import RedirectBackComponent from "wms-core/components/RedirectBackComponent"; -import FormHelperComponent from "wms-core/components/FormHelperComponent"; -import CsrfProtectionComponent from "wms-core/components/CsrfProtectionComponent"; -import WebSocketServerComponent from "wms-core/components/WebSocketServerComponent"; -import HomeController from "./controllers/HomeController"; -import AuthController from "./controllers/AuthController"; -import AuthComponent from "wms-core/auth/AuthComponent"; -import AuthGuard from "wms-core/auth/AuthGuard"; -import {PasswordAuthProof} from "./models/UserPassword"; -import {MIGRATIONS} from "./migrations"; -import LDAPServerComponent from "./LDAPServerComponent"; -import PreLaunchWall from "./controllers/PreLaunchWall"; -import AutoUpdateComponent from "wms-core/components/AutoUpdateComponent"; - -export default class Aldap extends Application { - private readonly port: number; - - constructor(port: number) { - super(require('../package.json').version); - this.port = port; - } - - protected getMigrations(): Type[] { - return MIGRATIONS; - } - - protected async init(): Promise { - this.registerComponents(); - this.registerWebSocketListeners(); - this.registerControllers(); - } - - private registerComponents() { - const redisComponent = new RedisComponent(); - const mysqlComponent = new MysqlComponent(); - - const expressAppComponent = new ExpressAppComponent(this.port); - this.use(expressAppComponent); - this.use(new NunjucksComponent()); - this.use(new LogRequestsComponent()); - - // Static files - this.use(new ServeStaticDirectoryComponent('public')); - this.use(new ServeStaticDirectoryComponent('node_modules/feather-icons/dist', '/icons')); - - // Maintenance - this.use(new MaintenanceComponent(this, () => { - return redisComponent.canServe() && mysqlComponent.canServe(); - })); - this.use(new AutoUpdateComponent()); - - // Services - this.use(mysqlComponent); - this.use(new MailComponent()); - - // Session - this.use(redisComponent); - this.use(new SessionComponent(redisComponent)); - - // Utils - this.use(new RedirectBackComponent()); - this.use(new FormHelperComponent()); - - // Middlewares - this.use(new CsrfProtectionComponent()); - - // Auth - this.use(new AuthComponent(new class extends AuthGuard { - public async getProofForSession(session: Express.Session): Promise { - return PasswordAuthProof.getProofForSession(session); - } - })); - - // WebSocket server - this.use(new WebSocketServerComponent(this, expressAppComponent, redisComponent)); - - // LDAP server - this.use(new LDAPServerComponent()); - } - - private registerWebSocketListeners() { - } - - private registerControllers() { - this.use(new PreLaunchWall()); - this.use(new HomeController()); - this.use(new AuthController()); - } -} \ No newline at end of file diff --git a/src/App.ts b/src/App.ts index 713f51b..c3dfbae 100644 --- a/src/App.ts +++ b/src/App.ts @@ -1,8 +1,6 @@ import Application from "wms-core/Application"; import {Type} from "wms-core/Utils"; import Migration from "wms-core/db/Migration"; -import CreateMigrationsTable from "wms-core/migrations/CreateMigrationsTable"; -import CreateLogsTable from "wms-core/migrations/CreateLogsTable"; import ExpressAppComponent from "wms-core/components/ExpressAppComponent"; import NunjucksComponent from "wms-core/components/NunjucksComponent"; import MysqlComponent from "wms-core/components/MysqlComponent"; @@ -12,15 +10,30 @@ import ServeStaticDirectoryComponent from "wms-core/components/ServeStaticDirect import MaintenanceComponent from "wms-core/components/MaintenanceComponent"; import MailComponent from "wms-core/components/MailComponent"; import SessionComponent from "wms-core/components/SessionComponent"; -import RedirectBackComponent from "wms-core/components/RedirectBackComponent"; import FormHelperComponent from "wms-core/components/FormHelperComponent"; import CsrfProtectionComponent from "wms-core/components/CsrfProtectionComponent"; import WebSocketServerComponent from "wms-core/components/WebSocketServerComponent"; import HomeController from "./controllers/HomeController"; +import AuthController from "./controllers/AuthController"; +import AuthComponent from "wms-core/auth/AuthComponent"; +import AuthGuard from "wms-core/auth/AuthGuard"; +import {PasswordAuthProof} from "./models/UserPassword"; +import LDAPServerComponent from "./LDAPServerComponent"; import AutoUpdateComponent from "wms-core/components/AutoUpdateComponent"; +import AccountController from "./controllers/AccountController"; +import CreateMigrationsTable from "wms-core/migrations/CreateMigrationsTable"; +import CreateLogsTable from "wms-core/migrations/CreateLogsTable"; +import CreateUsersAndUserEmailsTable from "wms-core/auth/migrations/CreateUsersAndUserEmailsTable"; +import CreateUserPasswordsTable from "./migrations/CreateUserPasswordsTable"; +import CreateUsernamesTable from "./migrations/CreateUsernamesTable"; +import CreateMagicLinksTable from "wms-core/auth/migrations/CreateMagicLinksTable"; +import MailController from "wms-core/auth/MailController"; +import MagicLinkController from "./controllers/MagicLinkController"; +import MagicLinkWebSocketListener from "wms-core/auth/magic_link/MagicLinkWebSocketListener"; export default class App extends Application { private readonly port: number; + private magicLinkWebSocketListener?: MagicLinkWebSocketListener; constructor(port: number) { super(require('../package.json').version); @@ -31,6 +44,10 @@ export default class App extends Application { return [ CreateMigrationsTable, CreateLogsTable, + CreateUsersAndUserEmailsTable, + CreateUserPasswordsTable, + CreateUsernamesTable, + CreateMagicLinksTable, ]; } @@ -68,20 +85,42 @@ export default class App extends Application { this.use(new SessionComponent(redisComponent)); // Utils - this.use(new RedirectBackComponent()); this.use(new FormHelperComponent()); // Middlewares this.use(new CsrfProtectionComponent()); + // Auth + this.use(new AuthComponent(new class extends AuthGuard { + public async getProofForSession(session: Express.Session): Promise { + return PasswordAuthProof.getProofForSession(session); + } + })); + // WebSocket server this.use(new WebSocketServerComponent(this, expressAppComponent, redisComponent)); + + // LDAP server + this.use(new LDAPServerComponent()); } private registerWebSocketListeners() { + this.magicLinkWebSocketListener = new MagicLinkWebSocketListener(); + this.use(this.magicLinkWebSocketListener); } private registerControllers() { + // Priority routes / interrupting middlewares + this.use(new AuthController()); + this.use(new AccountController()); + this.use(new MagicLinkController(this.magicLinkWebSocketListener!)) + + // Core functionality + this.use(new MailController()); + + // Other functionnality + + // Semi-static routes this.use(new HomeController()); } } \ No newline at end of file diff --git a/src/LDAPServerComponent.ts b/src/LDAPServerComponent.ts index 3026faf..db042da 100644 --- a/src/LDAPServerComponent.ts +++ b/src/LDAPServerComponent.ts @@ -1,27 +1,18 @@ import ApplicationComponent from "wms-core/ApplicationComponent"; -import {Express, Router} from "express"; -import ldap, {InsufficientAccessRightsError, InvalidCredentialsError, Server} from "ldapjs"; +import {Express} from "express"; +import ldap, {InvalidCredentialsError, Server} from "ldapjs"; import Logger from "wms-core/Logger"; import Username from "./models/Username"; -import UserEmail from "wms-core/auth/models/UserEmail"; import {PasswordAuthProof} from "./models/UserPassword"; import Throttler from "wms-core/Throttler"; export default class LDAPServerComponent extends ApplicationComponent { private server?: Server; - public async start(app: Express, router: Router): Promise { + public async start(app: Express): Promise { this.server = ldap.createServer({ log: console }); - let authorize = (req: any, res: any, next: any) => { - Logger.debug(req); - - if (!req.connection.ldap.bindDN.equals('cn=root')) - return next(new InsufficientAccessRightsError()); - - return next(); - }; this.server.bind('ou=users,dc=toot,dc=party', async (req: any, res: any, next: any) => { const rdns = req.dn.toString().split(', ').map((rdn: string) => rdn.split('=')); let username; @@ -46,7 +37,7 @@ export default class LDAPServerComponent extends ApplicationComponent { const user = await Username.getUserFromUsername(username); if (user) { - const email = await UserEmail.getMainFromUser(user.id!); + const email = await user.mainEmail.get(); if (email) { const authProof = new PasswordAuthProof(email.email!); if (await authProof.authorize(req.credentials)) { @@ -68,7 +59,4 @@ export default class LDAPServerComponent extends ApplicationComponent { Logger.info(`LDAP server listening on ${this.server!.url}`); }); } - - public async stop(): Promise { - } } \ No newline at end of file diff --git a/src/controllers/AccountController.ts b/src/controllers/AccountController.ts new file mode 100644 index 0000000..bf2373e --- /dev/null +++ b/src/controllers/AccountController.ts @@ -0,0 +1,21 @@ +import Controller from "wms-core/Controller"; +import {REQUIRE_AUTH_MIDDLEWARE} from "wms-core/auth/AuthComponent"; +import {NextFunction, Request, Response} from "express"; + +export default class AccountController extends Controller { + routes(): void { + this.get('/account', this.getAccount, 'account', REQUIRE_AUTH_MIDDLEWARE); + this.post('/add-recovery-email', this.addRecoveryEmail, 'add-recovery-email', REQUIRE_AUTH_MIDDLEWARE); + } + + protected async getAccount(req: Request, res: Response, next: NextFunction): Promise { + res.render('account', { + emails: await req.models.user!.emails.get(), + }); + } + + protected async addRecoveryEmail(req: Request, res: Response, next: NextFunction): Promise { + req.flash('warn', 'Not implemented'); + res.redirectBack(); + } +} \ No newline at end of file diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index fc27cae..ee32630 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -1,26 +1,28 @@ import Controller from "wms-core/Controller"; import {REQUIRE_AUTH_MIDDLEWARE, REQUIRE_GUEST_MIDDLEWARE} from "wms-core/auth/AuthComponent"; -import {Request, Response} from "express"; +import {NextFunction, Request, Response} from "express"; import Validator from "wms-core/db/Validator"; import {EMAIL_REGEX} from "wms-core/db/Model"; import {PasswordAuthProof} from "../models/UserPassword"; import UserEmail from "wms-core/auth/models/UserEmail"; import Username, {USERNAME_REGEXP} from "../models/Username"; +import _AuthController from "wms-core/auth/AuthController"; +import {ServerError} from "wms-core/HttpError"; -export default class AuthController extends Controller { +export default class AuthController extends _AuthController { routes(): void { this.get('/login', this.getLogin, 'login', REQUIRE_GUEST_MIDDLEWARE); this.post('/login', this.postLogin, 'login', REQUIRE_GUEST_MIDDLEWARE); this.get('/register', this.getRegister, 'register', REQUIRE_GUEST_MIDDLEWARE); this.post('/register', this.postRegister, 'register', REQUIRE_GUEST_MIDDLEWARE); - this.get('/logout', this.getLogout, 'logout', REQUIRE_AUTH_MIDDLEWARE); + this.post('/logout', this.postLogout, 'logout', REQUIRE_AUTH_MIDDLEWARE); } - private async getLogin(req: Request, res: Response): Promise { + protected async getLogin(req: Request, res: Response): Promise { res.render('login'); } - private async postLogin(req: Request, res: Response): Promise { + protected async postLogin(req: Request, res: Response): Promise { await this.validate({ email: new Validator().defined().regexp(EMAIL_REGEX), password: new Validator().acceptUndefined(), @@ -41,11 +43,11 @@ export default class AuthController extends Controller { res.redirect(Controller.route('home')); } - private async getRegister(req: Request, res: Response): Promise { + protected async getRegister(req: Request, res: Response): Promise { res.render('register'); } - private async postRegister(req: Request, res: Response): Promise { + protected async postRegister(req: Request, res: Response): Promise { const validationMap: any = { username: new Validator().defined().between(3, 64).regexp(USERNAME_REGEXP).unique(Username), password: new Validator().defined().minLength(8), @@ -96,8 +98,11 @@ export default class AuthController extends Controller { res.redirect(Controller.route('home')); } - private async getLogout(req: Request, res: Response): Promise { - await req.authGuard.logout(req.session!); - res.redirect(Controller.route('home')); + protected async getCheckAuth(req: Request, res: Response, next: NextFunction): Promise { + throw new ServerError('Not implemented.'); + } + + protected async postAuth(req: Request, res: Response, next: NextFunction): Promise { + throw new ServerError('Not implemented.'); } } \ No newline at end of file diff --git a/src/controllers/MagicLinkActionType.ts b/src/controllers/MagicLinkActionType.ts new file mode 100644 index 0000000..eda96d5 --- /dev/null +++ b/src/controllers/MagicLinkActionType.ts @@ -0,0 +1,3 @@ +export enum MagicLinkActionType { + ADD_RECOVERY_EMAIL = 'Add a recovery email', +} \ No newline at end of file diff --git a/src/controllers/MagicLinkController.ts b/src/controllers/MagicLinkController.ts new file mode 100644 index 0000000..8035f9e --- /dev/null +++ b/src/controllers/MagicLinkController.ts @@ -0,0 +1,35 @@ +import _MagicLinkController from "wms-core/auth/magic_link/MagicLinkController"; +import MagicLink from "wms-core/auth/models/MagicLink"; +import {Request, Response} from "express"; +import MagicLinkWebSocketListener from "wms-core/auth/magic_link/MagicLinkWebSocketListener"; +import {MagicLinkActionType} from "./MagicLinkActionType"; +import Controller from "wms-core/Controller"; +import {BadOwnerMagicLink} from "wms-core/auth/magic_link/MagicLinkAuthController"; +import UserEmail from "wms-core/auth/models/UserEmail"; + +export default class MagicLinkController extends _MagicLinkController { + constructor(magicLinkWebSocketListener: MagicLinkWebSocketListener) { + super(magicLinkWebSocketListener); + } + + protected async performAction(magicLink: MagicLink, req: Request, res: Response): Promise { + switch (magicLink.getActionType()) { + case MagicLinkActionType.ADD_RECOVERY_EMAIL: + if (magicLink.getSessionID() !== req.sessionID!) throw new BadOwnerMagicLink(); + + const user = await req.authGuard.getUserForSession(req.session!); + if (!user || !(await magicLink.isOwnedBy(user.id!))) throw new BadOwnerMagicLink(); + + let userEmail; + await (userEmail = new UserEmail({ + user_id: user.id, + email: await magicLink.getEmail(), + main: false, + })).save(); + + req.flash('success', `Recovery email ${userEmail.email} successfully added.`); + res.redirect(Controller.route('home')); + break; + } + } +} \ No newline at end of file diff --git a/src/controllers/PreLaunchWall.ts b/src/controllers/PreLaunchWall.ts deleted file mode 100644 index 9fd2274..0000000 --- a/src/controllers/PreLaunchWall.ts +++ /dev/null @@ -1,51 +0,0 @@ -import Controller from "wms-core/Controller"; -import {Request, RequestHandler, Response} from "express"; -import {ForbiddenHttpError} from "wms-core/HttpError"; -import Validator from "wms-core/db/Validator"; -import argon2 from "argon2"; -import config from "config"; - -export default class PreLaunchWall extends Controller { - public getGlobalHandlers(): RequestHandler[] { - return [ - (req, res, next) => { - if (!req.session) throw new ForbiddenHttpError('page', req.url); - - if (!req.session.authorized) { - const route = Controller.route('prelaunch-wall'); - if (req.url !== route) { - res.redirect(route); - return; - } - } - - next(); - } - ]; - } - - routes(): void { - this.get('/prelaunch-wall', this.getWall, 'prelaunch-wall'); - this.post('/prelaunch-wall', this.postWall, 'prelaunch-wall'); - } - - private async getWall(req: Request, res: Response) { - res.render('prelaunch-wall'); - } - - private async postWall(req: Request, res: Response) { - await this.validate({ - password: new Validator().defined(), - }, req.body); - - if (await argon2.verify(config.get('prelaunch-password'), req.body.password)) { - req.session!.authorized = true; - req.flash('success', 'Authentication success!'); - res.redirect(Controller.route('home')); - return; - } - - req.flash('error', 'Invalid password.'); - res.redirectBack(); - } -} \ No newline at end of file diff --git a/src/migrations.ts b/src/migrations.ts deleted file mode 100644 index a911950..0000000 --- a/src/migrations.ts +++ /dev/null @@ -1,13 +0,0 @@ -import CreateMigrationsTable from "wms-core/migrations/CreateMigrationsTable"; -import CreateLogsTable from "wms-core/migrations/CreateLogsTable"; -import CreateUsersAndUserEmailsTable from "wms-core/auth/migrations/CreateUsersAndUserEmailsTable"; -import CreateUserPasswordsTable from "./migrations/CreateUserPasswordsTable"; -import CreateUsernamesTable from "./migrations/CreateUsernamesTable"; - -export const MIGRATIONS = [ - CreateMigrationsTable, - CreateLogsTable, - CreateUsersAndUserEmailsTable, - CreateUserPasswordsTable, - CreateUsernamesTable, -]; \ No newline at end of file diff --git a/src/models/UserPassword.ts b/src/models/UserPassword.ts index 20f122a..d03b58e 100644 --- a/src/models/UserPassword.ts +++ b/src/models/UserPassword.ts @@ -4,22 +4,26 @@ import User from "wms-core/auth/models/User"; import argon2 from "argon2"; import AuthProof from "wms-core/auth/AuthProof"; import {UserAlreadyExistsAuthError} from "wms-core/auth/AuthGuard"; +import UserEmail from "wms-core/auth/models/UserEmail"; export default class UserPassword extends Model { public static async getByEmail(email: string): Promise { - const user = await User.fromEmail(email); - if (!user) return null; - - const result = await this.models(this.select().where('user_id', user.id).first()); - return result && result.length > 0 ? result[0] : null; + const userEmail = await UserEmail.select('user_id') + .where('email', email) + .first(); + return userEmail ? await UserPassword.select().where('user_id', userEmail.user_id).first() : null; } - private user_id?: number; - private password?: string; + private user_id!: number; + private password!: string; - protected defineProperties(): void { - this.defineProperty('user_id', new Validator().defined().unique(this).exists(User, 'id')); - this.defineProperty('password', new Validator().defined()); + public constructor(props: any) { + super(props); + } + + protected init(): void { + this.addProperty('user_id', new Validator().defined().unique(this).exists(User, 'id')); + this.addProperty('password', new Validator().defined()); } public async setUser(userID: number): Promise { @@ -67,7 +71,11 @@ export class PasswordAuthProof implements AuthProof { } public async getUser(): Promise { - return await User.fromEmail(await this.getEmail()); + const userEmail = await UserEmail.select() + .where('email', await this.getEmail()) + .with('user') + .first(); + return userEmail ? await userEmail.user.get() : null; } public async isAuthorized(): Promise { diff --git a/src/models/Username.ts b/src/models/Username.ts index ffbcb6d..2e20e8e 100644 --- a/src/models/Username.ts +++ b/src/models/Username.ts @@ -1,32 +1,38 @@ import Model from "wms-core/db/Model"; import Validator from "wms-core/db/Validator"; import User from "wms-core/auth/models/User"; +import {OneModelRelation} from "wms-core/db/ModelRelation"; export const USERNAME_REGEXP = /^[0-9a-z_-]+$/; export default class Username extends Model { public static async fromUser(userID: number): Promise { - const models = await this.models(this.select().where('user_id', userID)); - return models && models.length > 0 ? models[0] : null; + return await this.select().where('user_id', userID).first(); } public static async getUserFromUsername(username: string): Promise { - const models = await this.models(this.select().where('username', username.toLowerCase())); - if (!models || models.length === 0) return null; - return await User.getById(models[0].user_id!); + let model = await this.select() + .with('user') + .where('username', username.toLowerCase()) + .first(); + return model ? await model.user.get() : null; } - private user_id?: number; - public username?: string; + private readonly user_id!: number; + public readonly user: OneModelRelation = new OneModelRelation(this, User, { + localKey: 'user_id', + foreignKey: 'id' + }); + public username!: string; constructor(data: any) { super(data); } - protected defineProperties(): void { - this.defineProperty('user_id', new Validator().defined().exists(User, 'id').unique(this)); - this.defineProperty('username', new Validator().defined().between(3, 64).regexp(USERNAME_REGEXP).unique(this)); + protected init(): void { + this.addProperty('user_id', new Validator().defined().exists(User, 'id').unique(this)); + this.addProperty('username', new Validator().defined().between(3, 64).regexp(USERNAME_REGEXP).unique(this)); } } \ No newline at end of file diff --git a/views/account.njk b/views/account.njk new file mode 100644 index 0000000..7ab5edb --- /dev/null +++ b/views/account.njk @@ -0,0 +1,31 @@ +{% extends 'layouts/base.njk' %} + +{% set title = 'ALDAP - Welcome to the toot.party auth center!' %} + +{% block body %} +
+
+

My account

+ +

Name: {{ user.name }}

+ +
+

Email addresses

+ + {% for email in emails %} + {% if email.main %} +

Main email: {{ email.email }}

+ {% else %} +

Recovery email: {{ email.email }}

+ {% endif %} + {% endfor %} + +
+ {{ macros.field(_locals, 'email', 'email', null, 'Choose a safe email address', 'An email we can use to identify you in case you lose access to your account', 'required') }} + + +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/views/layouts/base.njk b/views/layouts/base.njk index 131cda7..b9494ca 100644 --- a/views/layouts/base.njk +++ b/views/layouts/base.njk @@ -16,15 +16,21 @@ {% endblock %} diff --git a/views/login.njk b/views/login.njk index 1b45bac..f6a90d1 100644 --- a/views/login.njk +++ b/views/login.njk @@ -11,7 +11,7 @@ {{ macros.field(_locals, 'email', 'email', null, 'Your email address', null, 'required') }} {{ macros.field(_locals, 'password', 'password', null, 'Your password', null, 'required') }} - + {{ macros.csrf(getCSRFToken) }} diff --git a/views/prelaunch-wall.njk b/views/prelaunch-wall.njk deleted file mode 100644 index 99e8741..0000000 --- a/views/prelaunch-wall.njk +++ /dev/null @@ -1,19 +0,0 @@ -{% extends 'layouts/base.njk' %} - -{% set title = 'ALDAP - Early access' %} - -{% block body %} -
-
-

{{ title }}

- -
- {{ macros.field(_locals, 'password', 'password', null, 'Enter password') }} - - - - {{ macros.csrf(getCSRFToken) }} -
-
-
-{% endblock %} \ No newline at end of file diff --git a/views/register.njk b/views/register.njk index f62501d..5079713 100644 --- a/views/register.njk +++ b/views/register.njk @@ -26,7 +26,7 @@ {{ macros.field(_locals, 'select', 'domain', null, 'Choose your domain', null, 'disabled', ['toot.party']) }} {{ macros.fieldError(_locals, 'email') }} -
This cannot be changed later.
+
You won't be able to change this again.
@@ -42,7 +42,7 @@ {{ macros.field(_locals, 'checkbox', 'terms', null, 'I accept the terms of services', null, 'required') }} - + {{ macros.csrf(getCSRFToken) }} From 3b8dee7fdf0092f488c6f1355af09ff70db5f49f Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Sat, 25 Jul 2020 10:48:56 +0200 Subject: [PATCH 36/48] Fix some routing and configuration --- config/default.ts | 22 +++++++++++++--------- config/production.ts | 11 ++++++++--- src/controllers/AuthController.ts | 4 ++-- src/controllers/MagicLinkController.ts | 2 +- views/account.njk | 2 ++ views/layouts/base.njk | 6 +++++- views/login.njk | 2 +- 7 files changed, 32 insertions(+), 17 deletions(-) diff --git a/config/default.ts b/config/default.ts index 9ecdeb9..cb6a0d4 100644 --- a/config/default.ts +++ b/config/default.ts @@ -1,12 +1,13 @@ export default Object.assign(require("wms-core/config/default").default, { app: { name: 'ALDAP', - contact_email: 'contact@toot.party' + contact_email: 'contact@toot.party', }, log_level: "DEV", db_log_level: "ERROR", - public_url: "http://localhost:4899", + base_url: "http://localhost:4899", public_websocket_url: "ws://localhost:4899", + domain: 'localhost:4899', port: 4899, mysql: { connectionLimit: 10, @@ -14,19 +15,19 @@ export default Object.assign(require("wms-core/config/default").default, { user: "root", password: "", database: "aldap", - create_database_automatically: false + create_database_automatically: false, }, redis: { host: "127.0.0.1", port: 6379, - prefix: 'aldap' + prefix: 'aldap', }, session: { secret: "very_secret_not_known", cookie: { secure: false, maxAge: 30 * 24 * 3600 * 1000, - } + }, }, mail: { host: "127.0.0.1", @@ -35,11 +36,14 @@ export default Object.assign(require("wms-core/config/default").default, { username: "", password: "", allow_invalid_tls: true, - from: 'contact@example.net', - from_name: 'Example App', + from: 'contact@toot.party', + from_name: 'ALDAP - toot.party', }, view: { - cache: false + cache: false, + }, + magic_link: { + validity_period: 20, }, approval_mode: false, -}); \ No newline at end of file +}); diff --git a/config/production.ts b/config/production.ts index f509007..933ee09 100644 --- a/config/production.ts +++ b/config/production.ts @@ -1,8 +1,9 @@ export default Object.assign(require("wms-core/config/production").default, { log_level: "DEBUG", db_log_level: "ERROR", - public_url: "https://aldap.toot.party", + base_url: "https://aldap.toot.party", public_websocket_url: "wss://aldap.toot.party", + domain: 'aldap.toot.party', session: { cookie: { secure: true @@ -11,5 +12,9 @@ export default Object.assign(require("wms-core/config/production").default, { mail: { secure: true, allow_invalid_tls: false - } -}); \ No newline at end of file + }, + magic_link: { + validity_period: 900, + }, + approval_mode: true, +}); diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts index ee32630..0ba14f0 100644 --- a/src/controllers/AuthController.ts +++ b/src/controllers/AuthController.ts @@ -11,8 +11,8 @@ import {ServerError} from "wms-core/HttpError"; export default class AuthController extends _AuthController { routes(): void { - this.get('/login', this.getLogin, 'login', REQUIRE_GUEST_MIDDLEWARE); - this.post('/login', this.postLogin, 'login', REQUIRE_GUEST_MIDDLEWARE); + this.get('/login', this.getLogin, 'auth', REQUIRE_GUEST_MIDDLEWARE); + this.post('/login', this.postLogin, 'auth', REQUIRE_GUEST_MIDDLEWARE); this.get('/register', this.getRegister, 'register', REQUIRE_GUEST_MIDDLEWARE); this.post('/register', this.postRegister, 'register', REQUIRE_GUEST_MIDDLEWARE); this.post('/logout', this.postLogout, 'logout', REQUIRE_AUTH_MIDDLEWARE); diff --git a/src/controllers/MagicLinkController.ts b/src/controllers/MagicLinkController.ts index 8035f9e..7fa6019 100644 --- a/src/controllers/MagicLinkController.ts +++ b/src/controllers/MagicLinkController.ts @@ -28,7 +28,7 @@ export default class MagicLinkController extends _MagicLinkController { })).save(); req.flash('success', `Recovery email ${userEmail.email} successfully added.`); - res.redirect(Controller.route('home')); + res.redirect(Controller.route('account')); break; } } diff --git a/views/account.njk b/views/account.njk index 7ab5edb..3bff4bd 100644 --- a/views/account.njk +++ b/views/account.njk @@ -24,6 +24,8 @@ {{ macros.field(_locals, 'email', 'email', null, 'Choose a safe email address', 'An email we can use to identify you in case you lose access to your account', 'required') }} + + {{ macros.csrf(getCSRFToken) }}
diff --git a/views/layouts/base.njk b/views/layouts/base.njk index b9494ca..7192bfd 100644 --- a/views/layouts/base.njk +++ b/views/layouts/base.njk @@ -18,6 +18,10 @@