From ebe8a528a2ec962c0225bd5442971a9102c12833 Mon Sep 17 00:00:00 2001 From: JerryImMouse Date: Wed, 16 Apr 2025 20:03:36 +0500 Subject: [PATCH] feat: view models --- locales/en.json | 2 +- locales/ru.json | 2 +- src/types/models.d.ts | 43 +++++++++++ src/web/controllers/admin.ts | 40 ++++------ src/web/controllers/auth.ts | 59 +++++--------- src/web/helpers.ts | 58 ++++++++------ src/web/models.ts | 145 +++++++++++++++++++++++++++++++++++ views/admin_login.pug | 8 +- views/admin_panel.pug | 16 ++-- views/error.pug | 12 +-- views/layout.pug | 4 +- views/login.pug | 10 +-- views/success.pug | 4 +- 13 files changed, 285 insertions(+), 118 deletions(-) create mode 100644 src/types/models.d.ts create mode 100644 src/web/models.ts diff --git a/locales/en.json b/locales/en.json index f8a9a4d..d652d5e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -10,7 +10,7 @@ "error500": "Server error occured during authentication.", "invalid_code": "Invalid Code Provided", - "invalid_code_Details": "Try reauthorizing, probably you've spoiled discord provided code.", + "invalid_code_details": "Try reauthorizing, probably you've spoiled discord provided code.", "unable_exchange_code": "Unable to exchange code", "unable_exchange_code_details": "Try reauthorizing, probably you've spoiled discord provided code.", diff --git a/locales/ru.json b/locales/ru.json index 3fc90e4..88945a2 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -10,7 +10,7 @@ "error500": "Произошла серверная ошибка во время аутентификации.", "invalid_code": "Предоставлен неверный код", - "invalid_code_Details": "Попробуйте перезапустить процесс авторизации, возможно вы испортили код.", + "invalid_code_details": "Попробуйте перезапустить процесс авторизации, возможно вы испортили код.", "unable_exchange_code": "Невозможно обменяться кодом", "unable_exchange_code_details": "Попробуйте перезапустить процесс авторизации, возможно вы испортили код.", diff --git a/src/types/models.d.ts b/src/types/models.d.ts new file mode 100644 index 0000000..43bb251 --- /dev/null +++ b/src/types/models.d.ts @@ -0,0 +1,43 @@ +import { IAuthorizedRecord } from "./database"; + +export interface IBaseViewModel { + assetPrefix: string, + title: string, +} + +export interface ILoginViewModel extends IBaseViewModel { + description: string, + mainText: string, + authLink?: string, + authBtnText?: string, +} + +export interface ISuccessViewModel extends IBaseViewModel { + description: string, + mainText: string, +} + +export interface IErrorViewModel extends IBaseViewModel { + errorDescription: string, + errorTitle: string, + statusCode: number, + errorText: string, + errorId?: string, + logsLink?: string, +} + +export interface IAdminLoginViewModel extends IBaseViewModel { + loginTitle: string, + loginSubmitBtnText: string, + loginTokenName: string, +} + +export interface IAdminPanelViewModel extends IBaseViewModel { + records: IAuthorizedRecord[] | null, + panelDeleteBtnText: string, + panelSubmitBtnText: string, + currentPage: number, + panelTitle: string, + prevPage: string, + nextPage: string, +} \ No newline at end of file diff --git a/src/web/controllers/admin.ts b/src/web/controllers/admin.ts index eabd14c..e0d1187 100644 --- a/src/web/controllers/admin.ts +++ b/src/web/controllers/admin.ts @@ -10,6 +10,7 @@ import { LocaleExtendedRequest } from '../../types/web'; import { verifyJWT } from '../middlewares/admin'; import { IAuthorizedRecord } from '../../types/database'; import { Logger } from '../../logging'; +import { AdminLoginViewModel, AdminPanelViewModel } from '../models'; const config = Configration.get(); const locales = LocaleManager.get(); @@ -45,21 +46,14 @@ export class AdminController { const nextPageLink = `${config.pathBase}/admin/panel?search=${searchText}&page=${page+1}&loc=${locale ?? config.locale}`; const prevPageLink = `${config.pathBase}/admin/panel?search=${searchText}&page=${page-1 < 1 ? 1 : page-1}&loc=${locale ?? config.locale}`; - const panel_title = locales.loc('panel_title', locale); - const panel_submit_btn = locales.loc('panel_submit_btn', locale); - const panel_delete_btn = locales.loc('panel_delete_btn', locale); - - res.render('admin_panel', { - records, - title: "Admin", - panel_title, - panel_submit_btn, - panel_delete_btn, - next_page: nextPageLink, - prev_page: prevPageLink, - cur_page: page, - assetPrefix: config.pathBase - }); + new AdminPanelViewModel( + locales, + page, + prevPageLink, + nextPageLink, + records, + locale + ).respond(res); } public static async postDelete(req: Request, res: Response) { @@ -86,18 +80,10 @@ export class AdminController { } public static async getLogin(req: LocaleExtendedRequest, res: Response) { - // locales - const locale = req.locale!; - const login_title = locales.loc('login_title', locale); - const login_submit_btn = locales.loc('login_submit_btn', locale); - const login_token_name = locales.loc('login_token_name', locale); - - res.render('admin_login', { - login_title, - login_token_name, - login_submit_btn, - assetPrefix: config.pathBase - }); + new AdminLoginViewModel( + locales, + req.locale + ).respond(res.status(200)); } public static async postLogin(req: Request, res: Response) { diff --git a/src/web/controllers/auth.ts b/src/web/controllers/auth.ts index 3686f44..8f2c52a 100644 --- a/src/web/controllers/auth.ts +++ b/src/web/controllers/auth.ts @@ -8,6 +8,7 @@ import { getLocale } from '../middlewares/auth'; import { Configration } from '../../config'; import { randomUUID } from 'crypto'; import { validateUuid } from '../../validation/uuid'; +import { ErrorViewModel, LoginViewModel, SuccessViewModel } from '../models'; // database should be already initialized here const db = Database.getDbImpl(); @@ -18,7 +19,6 @@ const logger = Logger.get(); /// Here is the format /// getPATH_PATH_... - GET /auth/PATH/PATH/... export class AuthController { - public static collectToRouter() { const router = Router(); router.get('/login', getLocale, AuthController.getLogin); @@ -28,12 +28,7 @@ export class AuthController { public static async getLogin(req: LocaleExtendedRequest, res: Response) { let uid = req.query['uid']?.toString() ?? undefined; - const locale = req.locale!; - const auth_required = locales.loc('auth_required', locale); - const auth_required_details = uid ? locales.loc('auth_required_details_uid', locale) : locales.loc('auth_required_details', locale); - const auth_btn = locales.loc('auth_btn', locale); - let authLink: string | undefined; if (uid) { if (validateUuid(uid)) { @@ -41,13 +36,11 @@ export class AuthController { } } - res.render('login', { - title: "Login", - auth_required, - auth_required_details, - authLink, auth_btn, - assetPrefix: config.pathBase} - ); + new LoginViewModel( + locales, + authLink, + locale, + ).respond(res.status(200)); } public static async getLogin_Cb(req: LocaleExtendedRequest, res: Response) { @@ -55,20 +48,21 @@ export class AuthController { const query = WebHelpers.parseCodeQuery(req); if (!query) { - const title = locales.loc("invalid_code", locale); - const desc = locales.loc("invalid_code_details", locale); - - WebHelpers.respondErr(res, title, 400, desc, locale); + WebHelpers.respondErr(res, "invalid_code", 400, "invalid_code_details", locale); return; } + const decodedState = atob(query.state); const tokenStruct = await WebHelpers.exchangeCode(query.code, req); if (!tokenStruct) { - const title = locales.loc("unable_exchange_code", locale); - const desc = locales.loc("unable_exchange_code_details", locale); - - WebHelpers.respondErr(res, title, 400, desc, locale); + WebHelpers.respondErr( + res, + "unable_exchange_code", + 400, + "unable_exchange_code_details", + locale + ); return; } @@ -108,10 +102,7 @@ export class AuthController { await foundByUid.save(); // https://github.com/maximal/http-267 - const title = locales.loc('ok267', locale); - const desc = locales.loc('ok267_details', locale); - - WebHelpers.respondErr(res, title, 267, desc, locale); + WebHelpers.respondErr(res, "ok267", 267, "ok267_details", locale); logger.warn("Authenticating again, credentials left untouched", { uid: foundByUid.uid, duid: foundByUid.discord_uid @@ -127,10 +118,7 @@ export class AuthController { await found.save(); // https://github.com/maximal/http-267 - const title = locales.loc('ok267', locale); - const desc = locales.loc('ok267_details', locale); - - WebHelpers.respondErr(res, title, 267, desc, locale); + WebHelpers.respondErr(res, "ok267", 267, "ok267_details", locale); logger.warn("Authenticating again, credentials left untouched", { uid: found.uid, duid: found.discord_uid @@ -163,15 +151,10 @@ export class AuthController { await record.save(); } - const auth_success = locales.loc('auth_success', locale); - const auth_success_details = locales.loc('auth_success_details', locale); - - res.render('success', { - title: "Success", - auth_success, - auth_success_details, - assetPrefix: config.pathBase} - ); + new SuccessViewModel( + locales, + locale + ).respond(res.status(200)); logger.debug(`Successfully authenticated new record`, { discord_uid: identifyScopeData.id, diff --git a/src/web/helpers.ts b/src/web/helpers.ts index 9362067..2af6255 100644 --- a/src/web/helpers.ts +++ b/src/web/helpers.ts @@ -5,6 +5,7 @@ import { AuthorizedRecord } from '../database/generic'; import { DataParams, DiscordCodeQuery, DiscordCodeResponse, DiscordGuildMemberObject, DiscordPartialGuildObject, DiscordRawCodeQuery, DiscordUserObject, IdentifyQueryParams, LinkQueryParams, RolesQueryParams } from '../types/web'; import { IDatabase } from '../types/database'; import { LocaleManager } from '../locale'; +import { ErrorViewModel } from './models'; const locales = LocaleManager.get(); const config = Configration.get(); @@ -24,32 +25,41 @@ export class WebHelpers { /// Express helpers - public static respondErr(res: ExResponse, msg: string, code: number, help: string, locale: string | undefined = undefined) { - const errorText = locales.loc("error_title", locale ?? config.locale); - - res.status(code).render('error', { - title: 'Error', - statusCode: code, - errorTitle: msg, - errorDescription: help, - errorText, - assetPrefix: config.pathBase} - ); + public static respondErr( + res: ExResponse, + errTitleLocKey: string, + statusCode: number, + errDescLocKey: string, + locale: string | undefined = undefined + ) { + new ErrorViewModel( + locales, + errDescLocKey, + errTitleLocKey, + statusCode, + undefined, + undefined, + locale, + ).respond(res.status(statusCode)) } - public static respondErrWithLogs(res: ExResponse, msg: string, code: number, help: string, id: string, data: string, locale: string | undefined = undefined) { - const errorText = locales.loc("error_title", locale ?? config.locale); - - res.status(code).render('error', { - title: "Error", - statusCode: code, - errorId: id, - errorTitle: msg, - errorDescription: help, - errorText, - logsLink: `${config.pathBase}logs?b64=${btoa(data)}`, - assetPrefix: config.pathBase - }); + public static respondErrWithLogs( + res: ExResponse, + errTitleLocKey: string, + statusCode: number, + errDescLocKey: string, + errorId: string, + data: string, locale: string | undefined = undefined + ) { + new ErrorViewModel( + locales, + errDescLocKey, + errTitleLocKey, + statusCode, + `${config.pathBase}logs?b64=${btoa(data)}`, + errorId, + locale + ).respond(res.status(statusCode)) Logger.get().error("500 - Server Error", {err: data}); } diff --git a/src/web/models.ts b/src/web/models.ts new file mode 100644 index 0000000..84a8e92 --- /dev/null +++ b/src/web/models.ts @@ -0,0 +1,145 @@ +import { Response } from 'express'; +import { LocaleManager } from '../locale'; +import { IAdminLoginViewModel, IAdminPanelViewModel, IErrorViewModel, ILoginViewModel, ISuccessViewModel } from '../types/models'; +import { Configration } from '../config'; +import { IAuthorizedRecord } from '../types/database'; + +const config = Configration.get(); + +export class LoginViewModel { + static descriptionLocKey = "auth_required_details"; + static mainTextLocKey = "auth_required"; + static authBtnTextLocKey = "auth_btn"; + + static viewName = "login"; + + private _inner: ILoginViewModel; + + constructor(locMan: LocaleManager, authLink?: string, locale?: string) { + this._inner = { + description: locMan.loc(LoginViewModel.descriptionLocKey, locale), + authBtnText: locMan.loc(LoginViewModel.authBtnTextLocKey, locale), + mainText: locMan.loc(LoginViewModel.mainTextLocKey, locale), + assetPrefix: config.pathBase, + authLink: authLink, + title: "Login" + } + } + + public respond(res: Response) { + res.render(LoginViewModel.viewName, {model: this._inner}); + } +} + +export class SuccessViewModel { + private static descriptionLocKey = "auth_success_details"; + private static mainTextLocKey = "auth_success"; + + private static viewName = "success"; + + private _inner: ISuccessViewModel; + + constructor(locMan: LocaleManager, locale?: string) { + this._inner = { + description: locMan.loc(SuccessViewModel.descriptionLocKey, locale), + mainText: locMan.loc(SuccessViewModel.mainTextLocKey, locale), + assetPrefix: config.pathBase, + title: "Success", + } + } + + public respond(res: Response) { + res.render(SuccessViewModel.viewName, {model: this._inner}) + } +} + +export class ErrorViewModel { + private static errTextLocKey = "error_title"; + private static viewName = "error"; + + private _inner: IErrorViewModel; + + constructor( + locMan: LocaleManager, + errDescLocKey: string, + errTitleLocKey: string, + statusCode: number, + logsLink?: string, + errorId?: string, + locale?: string + ) { + this._inner = { + errorTitle: locMan.loc(ErrorViewModel.errTextLocKey, locale), + errorDescription: locMan.loc(errDescLocKey, locale), + errorText: locMan.loc(errTitleLocKey, locale), + assetPrefix: config.pathBase, + title: "Error", + statusCode, + logsLink, + errorId, + } + } + + public respond(res: Response) { + res.render(ErrorViewModel.viewName, {model: this._inner}); + } +} + +export class AdminLoginViewModel { + private static loginTitleLocKey = "login_title"; + private static loginSubmitBtnLocKey = "login_submit_btn"; + private static loginTokenNameLocKey = "login_token_name"; + + private static viewName = "admin_login"; + + private _inner: IAdminLoginViewModel; + + constructor(locMan: LocaleManager, locale?: string) { + this._inner = { + loginSubmitBtnText: locMan.loc(AdminLoginViewModel.loginSubmitBtnLocKey, locale), + loginTokenName: locMan.loc(AdminLoginViewModel.loginTokenNameLocKey, locale), + loginTitle: locMan.loc(AdminLoginViewModel.loginTitleLocKey, locale), + assetPrefix: config.pathBase, + title: "Login", + } + } + + public respond(res: Response) { + res.render(AdminLoginViewModel.viewName, {model: this._inner}); + } +} + +export class AdminPanelViewModel { + private static panelDeleteBtnTextLocKey = "panel_delete_btn"; + private static panelSubmitBtnTextLocKey = "panel_submit_btn"; + private static panelTitleLocKey = "panel_title"; + + private static viewName = "admin_panel"; + + private _inner: IAdminPanelViewModel; + + constructor( + locMan: LocaleManager, + currentPage: number, + prevPage: string, + nextPage: string, + records: IAuthorizedRecord[] | null, + locale?: string, + ) { + this._inner = { + panelDeleteBtnText: locMan.loc(AdminPanelViewModel.panelDeleteBtnTextLocKey, locale), + panelSubmitBtnText: locMan.loc(AdminPanelViewModel.panelSubmitBtnTextLocKey, locale), + panelTitle: locMan.loc(AdminPanelViewModel.panelTitleLocKey, locale), + assetPrefix: config.pathBase, + title: "Panel", + currentPage, + prevPage, + nextPage, + records, + } + } + + public respond(res: Response) { + res.render(AdminPanelViewModel.viewName, {model: this._inner}); + } +} \ No newline at end of file diff --git a/views/admin_login.pug b/views/admin_login.pug index a8b218b..5bf8aeb 100644 --- a/views/admin_login.pug +++ b/views/admin_login.pug @@ -1,10 +1,10 @@ extends layout.pug block header - link(rel="stylesheet", href=assetPrefix + "css/admin.css") + link(rel="stylesheet", href=model.assetPrefix + "css/admin.css") block content - h2.h2= login_title + h2.h2= model.loginTitle form(action="login", method="post").form - input(id="api_token" name="token" type="password" placeholder= login_token_name required).input.center-self - input(type="submit" value= login_submit_btn style="margin-top: 10px;").center.button + input(id="api_token" name="token" type="password" placeholder= model.loginTokenName required).input.center-self + input(type="submit" value= model.loginSubmitBtnText style="margin-top: 10px;").center.button diff --git a/views/admin_panel.pug b/views/admin_panel.pug index 1c6802a..05346d9 100644 --- a/views/admin_panel.pug +++ b/views/admin_panel.pug @@ -1,24 +1,24 @@ extends layout.pug block header - link(rel="stylesheet", href=assetPrefix + "css/panel.css") + link(rel="stylesheet", href=model.assetPrefix + "css/panel.css") block content - h2.h2= panel_title + h2.h2= model.panelTitle form.search-form(action="panel" method='get') input(type="text" name="search" placeholder="discord_id or user_id").input.center-self.search-input - input(type="submit" value= panel_submit_btn).button.center.submit-btn + input(type="submit" value= model.panelSubmitBtnText).button.center.submit-btn div(class="records_cont") - each record in records + each record in model.records .record-card p.p= "Discord UID: " + record.discord_uid p.p= "User ID: " + record.uid p.p= "Expires In: " + record.expires p.p= "Updated At: " + record.updated_at form(action="panel/delete?discord_uid=" + record.discord_uid method='post') - input(type="submit" value= panel_delete_btn).button.control-btn + input(type="submit" value= model.panelDeleteBtnText).button.control-btn .pages - a(href= prev_page).page_link= "<" - p.p= cur_page - a(href= next_page).page_link= ">" \ No newline at end of file + a(href= model.prevPage).page_link= "<" + p.p= currentPage + a(href= nextPage).page_link= ">" \ No newline at end of file diff --git a/views/error.pug b/views/error.pug index 58d83d7..242809f 100644 --- a/views/error.pug +++ b/views/error.pug @@ -1,10 +1,10 @@ extends layout.pug block content - h2.h2.center= errorText - p.p.center= errorTitle + " - " + statusCode - p.p.center= errorDescription - if logsLink - p.p.center= "Your error ID: " + errorId - a(href= logsLink style="font-size: 1.1em;").center.button= "Error Logs" + h2.h2.center= model.errorText + p.p.center= model.errorTitle + " - " + model.statusCode + p.p.center= model.errorDescription + if model.logsLink + p.p.center= "Your error ID: " + model.errorId + a(href= model.logsLink style="font-size: 1.1em;").center.button= "Logs" \ No newline at end of file diff --git a/views/layout.pug b/views/layout.pug index 3ce56ef..3b49a75 100644 --- a/views/layout.pug +++ b/views/layout.pug @@ -1,8 +1,8 @@ doctype html html head - title= 'TypeAuthd | ' + title - link(rel='stylesheet' href= assetPrefix + 'css/style.css') + title= 'TypeAuthd | ' + model.title + link(rel='stylesheet' href= model.assetPrefix + 'css/style.css') meta(name="viewport", content="width=device-width, initial-scale=1.0") meta(charset="UTF-8") //- Had to do this because of pathBase diff --git a/views/login.pug b/views/login.pug index 52522bb..2f679b0 100644 --- a/views/login.pug +++ b/views/login.pug @@ -1,9 +1,9 @@ extends layout.pug block content - h2.h2.center= auth_required - if authLink - p.p.center= auth_required_details - a(href= authLink style="font-size: 1.1em;").center.button= auth_btn + h2.h2.center= model.mainText + if model.authLink + p.p.center= model.description + a(href= model.authLink style="font-size: 1.1em;").center.button= model.authBtnText else - p.p.center= auth_required_details \ No newline at end of file + p.p.center= model.description \ No newline at end of file diff --git a/views/success.pug b/views/success.pug index 26c1241..22640c7 100644 --- a/views/success.pug +++ b/views/success.pug @@ -1,5 +1,5 @@ extends layout.pug block content - h2.h2.center= auth_success - p.p.center= auth_success_details \ No newline at end of file + h2.h2.center= model.mainText + p.p.center= model.description \ No newline at end of file