add mail type and validation

This commit is contained in:
Julian Krauser 2025-04-25 12:13:26 +02:00
parent 2e3d0a755c
commit ce9f621b8b
11 changed files with 193 additions and 19 deletions

30
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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<any> {
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<a
"Cross-Origin-Resource-Policy": "cross-origin",
"Cross-Origin-Embedder-Policy": "credentialless",
"Timing-Allow-Origin": "*",
"Content-Type": "image/png",
});
res.setHeader("Content-Type", "image/png");
res.send(image);
}

View file

@ -1,5 +1,7 @@
import { Request, Response } from "express";
import SettingHelper from "../helpers/settingsHelper";
import MailHelper from "../helpers/mailHelper";
import InternalException from "../exceptions/internalException";
/**
* @description Service is currently not configured
@ -88,7 +90,7 @@ export async function setAppIdentity(req: Request, res: Response): Promise<any>
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<any>
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<any> {
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);
}

View file

@ -60,6 +60,41 @@ export abstract class MsTypeConverter extends TypeConverter<ms.StringValue> {
}
}
export abstract class EmailTypeConverter extends TypeConverter<string> {
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 {}

View file

@ -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<boolean> {
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<boolean> {
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,

View file

@ -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"

View file

@ -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);
});

View file

@ -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);
}

View file

@ -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);
});

View file

@ -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 },