From ce9f621b8bef91758e8de691242baf600e72992c Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Fri, 25 Apr 2025 12:13:26 +0200 Subject: [PATCH] add mail type and validation --- package-lock.json | 30 +++++++++++++++++++ package.json | 2 ++ src/controller/publicController.ts | 8 ++--- src/controller/setupController.ts | 46 +++++++++++++++++++++++++++- src/helpers/convertHelper.ts | 36 ++++++++++++++++++++++ src/helpers/mailHelper.ts | 48 +++++++++++++++++++++++++++++- src/helpers/settingsHelper.ts | 19 +++++++----- src/index.ts | 2 +- src/middleware/multer.ts | 4 +-- src/routes/setup.ts | 12 +++++++- src/type/settingTypes.ts | 5 +++- 11 files changed, 193 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index ab0d1a9..f4314bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "cors": "^2.8.5", "crypto": "^1.0.1", "dotenv": "^16.4.5", + "email-check": "^1.1.0", "express": "^5.1.0", "express-rate-limit": "^7.5.0", "express-validator": "^7.2.1", @@ -45,6 +46,7 @@ }, "devDependencies": { "@types/cors": "^2.8.14", + "@types/email-check": "^1.1.3", "@types/express": "^5.0.1", "@types/ip": "^1.1.3", "@types/jsonwebtoken": "^9.0.6", @@ -806,6 +808,13 @@ "@types/node": "*" } }, + "node_modules/@types/email-check": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/email-check/-/email-check-1.1.3.tgz", + "integrity": "sha512-XgU2uxm8JjfK9e/CJg389b96XeLxJbUSCfe4hZxxwTu3XYT7A70punAWfpdppFHWPDl/qNtHC9vl3TmRHom+8w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/express": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.1.tgz", @@ -2167,6 +2176,18 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "license": "MIT" }, + "node_modules/email-check": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/email-check/-/email-check-1.1.0.tgz", + "integrity": "sha512-VoqdsHtP/Ct+Dsl9nJRlvVXhcHicWjmmp2KvLbyg+WovdUXihe8EbDKC5u+3SlBQIlh8RK1qFD5A4RCgTrW9Wg==", + "license": "MIT", + "dependencies": { + "js-promisify": "1.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -3143,6 +3164,15 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/js-promisify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/js-promisify/-/js-promisify-1.0.1.tgz", + "integrity": "sha512-/IBrGxYbrmRWA+rLtHVSiX7R92NuVqc84aSWXReEjwcj7NchYf+Wy/ShAapCmMM5ev0mvD2IhWmZIDk/7f/utQ==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index 02fbe5a..03e70f5 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "cors": "^2.8.5", "crypto": "^1.0.1", "dotenv": "^16.4.5", + "email-check": "^1.1.0", "express": "^5.1.0", "express-rate-limit": "^7.5.0", "express-validator": "^7.2.1", @@ -61,6 +62,7 @@ }, "devDependencies": { "@types/cors": "^2.8.14", + "@types/email-check": "^1.1.3", "@types/express": "^5.0.1", "@types/ip": "^1.1.3", "@types/jsonwebtoken": "^9.0.6", diff --git a/src/controller/publicController.ts b/src/controller/publicController.ts index a4a3483..b608b3b 100644 --- a/src/controller/publicController.ts +++ b/src/controller/publicController.ts @@ -165,9 +165,9 @@ export async function getApplicationFavicon(req: Request, res: Response): Promis "Cross-Origin-Resource-Policy": "cross-origin", "Cross-Origin-Embedder-Policy": "credentialless", "Timing-Allow-Origin": "*", + "Content-Type": "image/x-icon", }); - res.setHeader("Content-Type", "image/x-icon"); res.send(buffer); } @@ -178,8 +178,8 @@ export async function getApplicationFavicon(req: Request, res: Response): Promis * @returns {Promise<*>} */ export async function getApplicationIcon(req: Request, res: Response): Promise { - const width = parseInt((req.query.width as string) ?? ""); - const height = parseInt((req.query.height as string) ?? ""); + const width = parseInt((req.query.width as string) ?? "512"); + const height = parseInt((req.query.height as string) ?? "512"); let icon = FileSystemHelper.readAssetFile("icon.png", true); let setLogo = SettingHelper.getSetting("club.icon"); @@ -200,8 +200,8 @@ export async function getApplicationIcon(req: Request, res: Response): Promise await SettingHelper.resetSetting("app.custom_login_message"); } - if (show_link_to_calendar) { + if (show_link_to_calendar == false || show_link_to_calendar == true) { await SettingHelper.setSetting("app.show_link_to_calendar", show_link_to_calendar); } else { await SettingHelper.resetSetting("app.show_link_to_calendar"); @@ -96,3 +98,45 @@ export async function setAppIdentity(req: Request, res: Response): Promise res.sendStatus(204); } + +/** + * @description set app identity + * @param req {Request} Express req object + * @param res {Response} Express res object + * @returns {Promise<*>} + */ +export async function setMailConfig(req: Request, res: Response): Promise { + const mail = req.body.mail; + const username = req.body.username; + const password = req.body.password; + const host = req.body.host; + const port = req.body.port; + const secure = req.body.secure; + + let checkMail = await MailHelper.checkMail(mail); + + if (!checkMail) { + throw new InternalException("Mail is not valid"); + } + + let checkConfig = await MailHelper.verifyTransport({ + user: username, + password, + host, + port, + secure, + }); + + if (!checkConfig) { + throw new InternalException("Config is not valid"); + } + + await SettingHelper.setSetting("mail.email", mail); + await SettingHelper.setSetting("mail.username", username); + await SettingHelper.setSetting("mail.password", password); + await SettingHelper.setSetting("mail.host", host); + await SettingHelper.setSetting("mail.port", port); + await SettingHelper.setSetting("mail.secure", secure); + + res.sendStatus(204); +} diff --git a/src/helpers/convertHelper.ts b/src/helpers/convertHelper.ts index ec50a7b..8857834 100644 --- a/src/helpers/convertHelper.ts +++ b/src/helpers/convertHelper.ts @@ -60,6 +60,41 @@ export abstract class MsTypeConverter extends TypeConverter { } } +export abstract class EmailTypeConverter extends TypeConverter { + fromString(value: string): string { + return value; + } + toString(value: string): string { + return value; + } + validate(value: string): boolean { + var tester = + /^[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+\/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; + if (!value) return false; + + var emailParts = value.split("@"); + + if (emailParts.length !== 2) return false; + + var account = emailParts[0]; + var address = emailParts[1]; + + if (account.length > 64) return false; + else if (address.length > 255) return false; + + var domainParts = address.split("."); + + if ( + domainParts.some(function (part) { + return part.length > 63; + }) + ) + return false; + + return tester.test(value); + } +} + // Konkrete Implementierungen der Converter export class StringConverter extends StringTypeConverter {} export class LongStringConverter extends StringTypeConverter {} @@ -67,3 +102,4 @@ export class UrlConverter extends StringTypeConverter {} export class NumberConverter extends NumberTypeConverter {} export class BooleanConverter extends BooleanTypeConverter {} export class MsConverter extends MsTypeConverter {} +export class EmailConverter extends EmailTypeConverter {} diff --git a/src/helpers/mailHelper.ts b/src/helpers/mailHelper.ts index 1b0f728..b310bef 100644 --- a/src/helpers/mailHelper.ts +++ b/src/helpers/mailHelper.ts @@ -1,6 +1,7 @@ import { Transporter, createTransport, TransportOptions } from "nodemailer"; import { Attachment } from "nodemailer/lib/mailer"; import SettingHelper from "./settingsHelper"; +import emailCheck from "email-check"; export default abstract class MailHelper { private static transporter: Transporter; @@ -19,6 +20,51 @@ export default abstract class MailHelper { } as TransportOptions); } + static async verifyTransport({ + host, + port, + secure, + user, + password, + }: { + host: string; + port: number; + secure: boolean; + user: string; + password: string; + }): Promise { + let transport = createTransport({ + host, + port, + secure, + auth: { user, pass: password }, + }); + + return await transport + .verify() + .then(() => { + return true; + }) + .catch(() => { + return false; + }) + .finally(() => { + try { + transport?.close(); + } catch (error) {} + }); + } + + static async checkMail(mail: string): Promise { + return await emailCheck(mail) + .then((res) => { + return res; + }) + .catch((err) => { + return false; + }); + } + static initialize() { SettingHelper.onSettingTopicChanged("mail", () => { this.createTransport(); @@ -42,7 +88,7 @@ export default abstract class MailHelper { return new Promise((resolve, reject) => { this.transporter .sendMail({ - from: `"${SettingHelper.getSetting("club.name")}" <${SettingHelper.getSetting("mail.username")}>`, + from: `"${SettingHelper.getSetting("club.name")}" <${SettingHelper.getSetting("mail.email")}>`, to: target, subject, text: content, diff --git a/src/helpers/settingsHelper.ts b/src/helpers/settingsHelper.ts index 63fcf66..c8e27f6 100644 --- a/src/helpers/settingsHelper.ts +++ b/src/helpers/settingsHelper.ts @@ -5,6 +5,7 @@ import SettingService from "../service/management/settingService"; import { APPLICATION_SECRET } from "../env.defaults"; import { BooleanConverter, + EmailConverter, LongStringConverter, MsConverter, NumberConverter, @@ -27,6 +28,7 @@ export default abstract class SettingHelper { number: new NumberConverter(), boolean: new BooleanConverter(), ms: new MsConverter(), + email: new EmailConverter(), }; public static getAllSettings() { @@ -79,7 +81,7 @@ export default abstract class SettingHelper { finalValue = CodingHelper.encrypt(APPLICATION_SECRET, stringValue); } - this.settings[key] = finalValue; + this.settings[key] = stringValue; const [topic, settingKey] = key.split(".") as [SettingTopic, string]; await SettingCommandHandler.create({ @@ -144,13 +146,14 @@ export default abstract class SettingHelper { } let processedValue = valueToCheck; - if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) { - try { - processedValue = CodingHelper.decrypt(APPLICATION_SECRET, processedValue); - } catch (error) { - throw new Error(`Unable to decrypt value for ${key}: ${error.message}`); - } - } + // do not encypt data here - data is only crypted towards database + // if (typeof settingType.type === "string" && settingType.type.includes("/crypt")) { + // try { + // processedValue = CodingHelper.decrypt(APPLICATION_SECRET, processedValue); + // } catch (error) { + // throw new Error(`Unable to decrypt value for ${key}: ${error.message}`); + // } + // } const baseType = typeof settingType.type === "string" diff --git a/src/index.ts b/src/index.ts index 13de2ee..2e248f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ import { dataSource } from "./data-source"; import BackupHelper from "./helpers/backupHelper"; import SettingHelper from "./helpers/settingsHelper"; dataSource.initialize().then(async () => { - if (await dataSource.createQueryRunner().hasTable("user")) { + if (false && (await dataSource.createQueryRunner().hasTable("user"))) { await BackupHelper.autoRestoreBackup().catch((err) => { console.log(`${new Date().toISOString()}: failed auto-restoring database`, err); }); diff --git a/src/middleware/multer.ts b/src/middleware/multer.ts index fbd6822..c9bad44 100644 --- a/src/middleware/multer.ts +++ b/src/middleware/multer.ts @@ -8,9 +8,9 @@ export const clubImageStorage = multer.diskStorage({ const fileExtension = path.extname(file.originalname).toLowerCase(); if (file.fieldname === "icon") { - cb(null, "company-icon" + fileExtension); + cb(null, "admin-icon" + fileExtension); } else if (file.fieldname === "logo") { - cb(null, "company-logo" + fileExtension); + cb(null, "admin-logo" + fileExtension); } else { cb(null, file.originalname); } diff --git a/src/routes/setup.ts b/src/routes/setup.ts index 8f31a0b..2d69e07 100644 --- a/src/routes/setup.ts +++ b/src/routes/setup.ts @@ -1,5 +1,11 @@ import express from "express"; -import { isSetup, setAppIdentity, setClubIdentity, uploadClubImages } from "../controller/setupController"; +import { + isSetup, + setAppIdentity, + setClubIdentity, + setMailConfig, + uploadClubImages, +} from "../controller/setupController"; import { finishInvite, inviteUser, verifyInvite } from "../controller/inviteController"; import ParamaterPassCheckHelper from "../helpers/parameterPassCheckHelper"; import { clubImageUpload } from "../middleware/multer"; @@ -22,6 +28,10 @@ router.post("/app", async (req, res) => { await setAppIdentity(req, res); }); +router.post("/mail", async (req, res) => { + await setMailConfig(req, res); +}); + router.post("/verify", ParamaterPassCheckHelper.requiredIncludedMiddleware(["mail", "token"]), async (req, res) => { await verifyInvite(req, res); }); diff --git a/src/type/settingTypes.ts b/src/type/settingTypes.ts index 8f131ee..568bc64 100644 --- a/src/type/settingTypes.ts +++ b/src/type/settingTypes.ts @@ -13,6 +13,7 @@ export type SettingString = | "session.jwt_expiration" | "session.refresh_expiration" | "session.pwa_refresh_expiration" + | "mail.email" | "mail.username" | "mail.password" | "mail.host" @@ -21,7 +22,7 @@ export type SettingString = | "backup.interval" | "backup.copies"; -export type SettingTypeAtom = "longstring" | "string" | "ms" | "number" | "boolean" | "url"; +export type SettingTypeAtom = "longstring" | "string" | "ms" | "number" | "boolean" | "url" | "email"; export type SettingType = SettingTypeAtom | `${SettingTypeAtom}/crypt` | `${SettingTypeAtom}/rand`; export type SettingValueMapping = { @@ -36,6 +37,7 @@ export type SettingValueMapping = { "session.jwt_expiration": ms.StringValue; "session.refresh_expiration": ms.StringValue; "session.pwa_refresh_expiration": ms.StringValue; + "mail.email": string; "mail.username": string; "mail.password": string; "mail.host": string; @@ -69,6 +71,7 @@ export const settingsType: SettingsSchema = { "session.jwt_expiration": { type: "ms", default: "15m" }, "session.refresh_expiration": { type: "ms", default: "1d" }, "session.pwa_refresh_expiration": { type: "ms", default: "5d" }, + "mail.email": { type: "email", optional: false }, "mail.username": { type: "string", optional: false }, "mail.password": { type: "string/crypt", optional: false }, "mail.host": { type: "url", optional: false },