Merge pull request 'patches v1.5.1' (#99) from develop into main

Reviewed-on: #99
This commit is contained in:
Julian Krauser 2025-05-07 07:28:59 +00:00
commit a36ebbd43c
10 changed files with 65 additions and 84 deletions

View file

@ -8,6 +8,8 @@ Dieses Projekt, `ff-admin-server`, ist das Backend zur Verwaltung von Mitglieder
Eine Demo zusammen mit der `ff-admin` finden Sie unter [https://admin-demo.ff-admin.de](https://admin-demo.ff-admin.de). Eine Demo zusammen mit der `ff-admin` finden Sie unter [https://admin-demo.ff-admin.de](https://admin-demo.ff-admin.de).
Das Handbuch zur Anwendung finden sie unter [https://ff-admin.de/ff-admin-handbook](https://ff-admin.de/ff-admin-handbook).
## Installation ## Installation
Das Image exposed nur den Port 5000. Die Env-Variable SERVER_PORT kann nur im lokal ausführenden dev-Kontext verwendet werden. Das Image exposed nur den Port 5000. Die Env-Variable SERVER_PORT kann nur im lokal ausführenden dev-Kontext verwendet werden.
@ -25,26 +27,13 @@ services:
container_name: ff_member_administration_server container_name: ff_member_administration_server
restart: unless-stopped restart: unless-stopped
environment: environment:
- DB_TYPE=<mysql|sqlite|postgres> # default ist auf mysql gesetzt - DB_TYPE=<mysql|postgres> # default ist auf mysql gesetzt
- DB_HOST=ff-db - DB_HOST=ff-db
- DB_PORT=<number> # default ist auf 3306 gesetzt - DB_PORT=<number> # default ist auf 3306 gesetzt
- DB_NAME=ffadmin - DB_NAME=ffadmin
- DB_USERNAME=administration_backend - DB_USERNAME=administration_backend
- DB_PASSWORD=<dbuserpasswd> - DB_PASSWORD=<dbuserpasswd>
- JWT_SECRET=<tobemodified> - APPLICATION_SECRET=<tobemodified>
- JWT_EXPIRATION=<number[m|d] - bsp.:15m> # default ist auf 15m gesetzt
- REFRESH_EXPIRATION=<number[m|d] - bsp.:1d> # default ist auf 1d gesetzt
- PWA_REFRESH_EXPIRATION=<number[m|d] - bsp.:5d> # default ist auf 5d gesetzt
- MAIL_USERNAME=<mailadress|username>
- MAIL_PASSWORD=<password>
- MAIL_HOST=<url>
- MAIL_PORT=<port> # default ist auf 587 gesetzt
- MAIL_SECURE=<boolean> # default ist auf false gesetzt
- CLUB_NAME=<tobemodified> # default ist auf FF Admin gesetzt
- CLUB_WEBSITE=<tobemodified>
- BACKUP_INTERVAL=<number of days (min. 1)> # alle x Tage, sonst keine
- BACKUP_COPIES=<number of parallel copies> # Anzahl parallel bestehender Backups
- BACKUP_AUTO_RESTORE=<boolean> # default ist auf true gesetzt
- USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true - USE_SECURITY_STRICT_LIMIT = (true|false) # default ist true
- SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15 - SECURITY_STRICT_LIMIT_WINDOW = [0-9]*(y|d|h|m|s) # default ist 15
- SECURITY_STRICT_LIMIT_REQUEST_COUNT = strict_request_count # default ist 15 - SECURITY_STRICT_LIMIT_REQUEST_COUNT = strict_request_count # default ist 15
@ -91,8 +80,6 @@ networks:
Die Verwendung von postgres wird aufgrund des Verhaltens bei Datenbank-Update-Fehlern empfohlen. Die Verwendung von postgres wird aufgrund des Verhaltens bei Datenbank-Update-Fehlern empfohlen.
Die Verwendung von SQLite wird nur für die Entwicklung oder lokale Tests empfohlen.
Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten: Führen Sie dann den folgenden Befehl im Verzeichnis der compose-Datei aus, um den Container zu starten:
```sh ```sh

View file

@ -41,8 +41,6 @@ export async function login(req: Request, res: Response): Promise<any> {
let { id } = await UserService.getByUsername(username); let { id } = await UserService.getByUsername(username);
let { secret, routine } = await UserService.getUserSecretAndRoutine(id); let { secret, routine } = await UserService.getUserSecretAndRoutine(id);
console.log(secret, passedSecret);
let valid = false; let valid = false;
if (routine == LoginRoutineEnum.totp) { if (routine == LoginRoutineEnum.totp) {
valid = speakeasy.totp.verify({ valid = speakeasy.totp.verify({

View file

@ -52,7 +52,7 @@ import { TemplatesAndProtocolSort1742549956787 } from "./migrations/174254995678
import { QueryToUUID1742922178643 } from "./migrations/1742922178643-queryToUUID"; import { QueryToUUID1742922178643 } from "./migrations/1742922178643-queryToUUID";
import { NewsletterColumnType1744351418751 } from "./migrations/1744351418751-newsletterColumnType"; import { NewsletterColumnType1744351418751 } from "./migrations/1744351418751-newsletterColumnType";
import { QueryUpdatedAt1744795756230 } from "./migrations/1744795756230-QueryUpdatedAt"; import { QueryUpdatedAt1744795756230 } from "./migrations/1744795756230-QueryUpdatedAt";
import { SettingsFromEnv1745059495808 } from "./migrations/1745059495808-settingsFromEnv"; import { SettingsFromEnv1745059495807 } from "./migrations/1745059495807-settingsFromEnv";
import { MemberCreatedAt1746006549262 } from "./migrations/1746006549262-memberCreatedAt"; import { MemberCreatedAt1746006549262 } from "./migrations/1746006549262-memberCreatedAt";
import { UserLoginRoutine1746252454922 } from "./migrations/1746252454922-UserLoginRoutine"; import { UserLoginRoutine1746252454922 } from "./migrations/1746252454922-UserLoginRoutine";
import { SettingsFromEnv_SET1745059495808 } from "./migrations/1745059495808-settingsFromEnv_set"; import { SettingsFromEnv_SET1745059495808 } from "./migrations/1745059495808-settingsFromEnv_set";
@ -119,7 +119,7 @@ const dataSource = new DataSource({
QueryToUUID1742922178643, QueryToUUID1742922178643,
NewsletterColumnType1744351418751, NewsletterColumnType1744351418751,
QueryUpdatedAt1744795756230, QueryUpdatedAt1744795756230,
SettingsFromEnv1745059495808, SettingsFromEnv1745059495807,
SettingsFromEnv_SET1745059495808, SettingsFromEnv_SET1745059495808,
MemberCreatedAt1746006549262, MemberCreatedAt1746006549262,
UserLoginRoutine1746252454922, UserLoginRoutine1746252454922,

View file

@ -13,7 +13,7 @@ export abstract class CodingHelper {
try { try {
return CodingHelper.decrypt(key, val, true); return CodingHelper.decrypt(key, val, true);
} catch (error) { } catch (error) {
console.error("Decryption error:", error); console.error("Decryption error in database-read - can be ignored");
if (fallback == "<self>") return val; if (fallback == "<self>") return val;
else return fallback; else return fallback;
} }
@ -25,7 +25,7 @@ export abstract class CodingHelper {
try { try {
return CodingHelper.encrypt(key, valueToEncrypt, true); return CodingHelper.encrypt(key, valueToEncrypt, true);
} catch (error) { } catch (error) {
console.error("Encryption error:", error); console.error("Encryption error in database-read - can be ignored");
if (fallback == "<self>") return val; if (fallback == "<self>") return val;
return ""; return "";
} }

View file

@ -17,33 +17,31 @@ export default class PermissionHelper {
permissions: PermissionObject, permissions: PermissionObject,
type: PermissionType | "admin", type: PermissionType | "admin",
section: PermissionSection, section: PermissionSection,
module?: PermissionModule module: PermissionModule
) { ) {
if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false; if (type == "admin") return permissions?.admin ?? permissions?.adminByOwner ?? false;
if (permissions?.admin || permissions?.adminByOwner) return true; if (permissions?.admin || permissions?.adminByOwner) return true;
if ( if (
(!module &&
permissions[section] != undefined &&
(permissions[section]?.all == "*" || permissions[section]?.all?.includes(type))) ||
permissions[section]?.all == "*" || permissions[section]?.all == "*" ||
permissions[section]?.all?.includes(type) permissions[section]?.all?.includes(type) ||
permissions[section]?.[module] == "*" ||
permissions[section]?.[module]?.includes(type)
) )
return true; return true;
if (module && (permissions[section]?.[module] == "*" || permissions[section]?.[module]?.includes(type)))
return true;
return false; return false;
} }
static canSome( static canSome(
permissions: PermissionObject, permissions: PermissionObject,
checks: Array<{ checks: Array<{
requiredPermissions: PermissionType | "admin"; requiredPermission: PermissionType | "admin";
section: PermissionSection; section: PermissionSection;
module?: PermissionModule; module: PermissionModule;
}> }>
) { ) {
checks.reduce<boolean>((prev, curr) => { return checks.reduce<boolean>((prev, curr) => {
return prev || this.can(permissions, curr.requiredPermissions, curr.section, curr.module); return prev || this.can(permissions, curr.requiredPermission, curr.section, curr.module);
}, false); }, false);
} }
@ -66,12 +64,12 @@ export default class PermissionHelper {
static canSomeSection( static canSomeSection(
permissions: PermissionObject, permissions: PermissionObject,
checks: Array<{ checks: Array<{
requiredPermissions: PermissionType | "admin"; requiredPermission: PermissionType | "admin";
section: PermissionSection; section: PermissionSection;
}> }>
): boolean { ): boolean {
return checks.reduce<boolean>((prev, curr) => { return checks.reduce<boolean>((prev, curr) => {
return prev || this.can(permissions, curr.requiredPermissions, curr.section); return prev || this.canSection(permissions, curr.requiredPermission, curr.section);
}, false); }, false);
} }
@ -83,7 +81,7 @@ export default class PermissionHelper {
static passCheckMiddleware( static passCheckMiddleware(
requiredPermissions: PermissionType | "admin", requiredPermissions: PermissionType | "admin",
section: PermissionSection, section: PermissionSection,
module?: PermissionModule module: PermissionModule
): (req: Request, res: Response, next: Function) => void { ): (req: Request, res: Response, next: Function) => void {
return (req: Request, res: Response, next: Function) => { return (req: Request, res: Response, next: Function) => {
const permissions = req.permissions; const permissions = req.permissions;
@ -99,9 +97,9 @@ export default class PermissionHelper {
static passCheckSomeMiddleware( static passCheckSomeMiddleware(
checks: Array<{ checks: Array<{
requiredPermissions: PermissionType | "admin"; requiredPermission: PermissionType | "admin";
section: PermissionSection; section: PermissionSection;
module?: PermissionModule; module: PermissionModule;
}> }>
): (req: Request, res: Response, next: Function) => void { ): (req: Request, res: Response, next: Function) => void {
return (req: Request, res: Response, next: Function) => { return (req: Request, res: Response, next: Function) => {
@ -111,9 +109,7 @@ export default class PermissionHelper {
if (isOwner || this.canSome(permissions, checks)) { if (isOwner || this.canSome(permissions, checks)) {
next(); next();
} else { } else {
let permissionsToPass = checks.reduce<string>((prev, curr) => { let permissionsToPass = checks.map((c) => `${c.section}.${c.module}.${c.requiredPermission}`).join(" or ");
return prev + (prev != " or " ? "" : "") + `${curr.section}.${curr.module}.${curr.requiredPermissions}`;
}, "");
throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`); throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`);
} }
}; };
@ -136,7 +132,7 @@ export default class PermissionHelper {
} }
static sectionPassCheckSomeMiddleware( static sectionPassCheckSomeMiddleware(
checks: Array<{ requiredPermissions: PermissionType | "admin"; section: PermissionSection }> checks: Array<{ requiredPermission: PermissionType | "admin"; section: PermissionSection }>
): (req: Request, res: Response, next: Function) => void { ): (req: Request, res: Response, next: Function) => void {
return (req: Request, res: Response, next: Function) => { return (req: Request, res: Response, next: Function) => {
const permissions = req.permissions; const permissions = req.permissions;
@ -145,9 +141,7 @@ export default class PermissionHelper {
if (isOwner || this.canSomeSection(permissions, checks)) { if (isOwner || this.canSomeSection(permissions, checks)) {
next(); next();
} else { } else {
let permissionsToPass = checks.reduce<string>((prev, curr) => { let permissionsToPass = checks.map((c) => `${c.section}.${c.requiredPermission}`).join(" or ");
return prev + (prev != " or " ? "" : "") + `${curr.section}.${curr.requiredPermissions}`;
}, "");
throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`); throw new ForbiddenRequestException(`missing permission for ${permissionsToPass}`);
} }
}; };

View file

@ -3,8 +3,8 @@ import { setting_table } from "./baseSchemaTables/admin";
import SettingHelper from "../helpers/settingsHelper"; import SettingHelper from "../helpers/settingsHelper";
import ms from "ms"; import ms from "ms";
export class SettingsFromEnv1745059495808 implements MigrationInterface { export class SettingsFromEnv1745059495807 implements MigrationInterface {
name = "SettingsFromEnv1745059495808"; name = "SettingsFromEnv1745059495807";
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(setting_table, true, true, true); await queryRunner.createTable(setting_table, true, true, true);

View file

@ -20,7 +20,7 @@ export class SettingsFromEnv_SET1745059495808 implements MigrationInterface {
await SettingHelper.setSetting("mail.password", process.env.MAIL_PASSWORD); await SettingHelper.setSetting("mail.password", process.env.MAIL_PASSWORD);
await SettingHelper.setSetting("mail.host", process.env.MAIL_HOST); await SettingHelper.setSetting("mail.host", process.env.MAIL_HOST);
await SettingHelper.setSetting("mail.port", Number(process.env.MAIL_PORT ?? "578")); await SettingHelper.setSetting("mail.port", Number(process.env.MAIL_PORT ?? "578"));
await SettingHelper.setSetting("mail.secure", Boolean(process.env.MAIL_SECURE ?? "false")); await SettingHelper.setSetting("mail.secure", process.env.MAIL_SECURE == "true");
await SettingHelper.setSetting("backup.interval", Number(process.env.BACKUP_INTERVAL ?? "1")); await SettingHelper.setSetting("backup.interval", Number(process.env.BACKUP_INTERVAL ?? "1"));
await SettingHelper.setSetting("backup.copies", Number(process.env.BACKUP_COPIES ?? "7")); await SettingHelper.setSetting("backup.copies", Number(process.env.BACKUP_COPIES ?? "7"));
} }

View file

@ -33,64 +33,64 @@ var router = express.Router({ mergeParams: true });
router.use( router.use(
"/award", "/award",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "award" }, { requiredPermission: "read", section: "configuration", module: "award" },
{ requiredPermissions: "read", section: "club", module: "member" }, { requiredPermission: "read", section: "club", module: "member" },
]), ]),
award award
); );
router.use( router.use(
"/communicationtype", "/communicationtype",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "communication_type" }, { requiredPermission: "read", section: "configuration", module: "communication_type" },
{ requiredPermissions: "read", section: "club", module: "member" }, { requiredPermission: "read", section: "club", module: "member" },
]), ]),
communicationType communicationType
); );
router.use( router.use(
"/executiveposition", "/executiveposition",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "executive_position" }, { requiredPermission: "read", section: "configuration", module: "executive_position" },
{ requiredPermissions: "read", section: "club", module: "member" }, { requiredPermission: "read", section: "club", module: "member" },
]), ]),
executivePosition executivePosition
); );
router.use( router.use(
"/membershipstatus", "/membershipstatus",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "membership_status" }, { requiredPermission: "read", section: "configuration", module: "membership_status" },
{ requiredPermissions: "read", section: "club", module: "member" }, { requiredPermission: "read", section: "club", module: "member" },
]), ]),
membershipStatus membershipStatus
); );
router.use( router.use(
"/qualification", "/qualification",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "qualification" }, { requiredPermission: "read", section: "configuration", module: "qualification" },
{ requiredPermissions: "read", section: "club", module: "member" }, { requiredPermission: "read", section: "club", module: "member" },
]), ]),
qualification qualification
); );
router.use( router.use(
"/salutation", "/salutation",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "salutation" }, { requiredPermission: "read", section: "configuration", module: "salutation" },
{ requiredPermissions: "read", section: "club", module: "member" }, { requiredPermission: "read", section: "club", module: "member" },
]), ]),
salutation salutation
); );
router.use( router.use(
"/calendartype", "/calendartype",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "calendar_type" }, { requiredPermission: "read", section: "configuration", module: "calendar_type" },
{ requiredPermissions: "read", section: "club", module: "calendar" }, { requiredPermission: "read", section: "club", module: "calendar" },
]), ]),
calendarType calendarType
); );
router.use( router.use(
"/querystore", "/querystore",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "query_store" }, { requiredPermission: "read", section: "configuration", module: "query_store" },
{ requiredPermissions: "read", section: "club", module: "listprint" }, { requiredPermission: "read", section: "club", module: "listprint" },
]), ]),
queryStore queryStore
); );
@ -98,16 +98,16 @@ router.use("/template", PermissionHelper.passCheckMiddleware("read", "configurat
router.use( router.use(
"/templateusage", "/templateusage",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "template_usage" }, { requiredPermission: "read", section: "configuration", module: "template_usage" },
{ requiredPermissions: "read", section: "configuration", module: "template" }, { requiredPermission: "read", section: "configuration", module: "template" },
]), ]),
templateUsage templateUsage
); );
router.use( router.use(
"/newsletterconfig", "/newsletterconfig",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "configuration", module: "newsletter_config" }, { requiredPermission: "read", section: "configuration", module: "newsletter_config" },
{ requiredPermissions: "read", section: "configuration", module: "communication_type" }, { requiredPermission: "read", section: "configuration", module: "communication_type" },
]), ]),
newsletterConfig newsletterConfig
); );
@ -116,8 +116,8 @@ router.use("/member", PermissionHelper.passCheckMiddleware("read", "club", "memb
router.use( router.use(
"/protocol", "/protocol",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "club", module: "protocol" }, { requiredPermission: "read", section: "club", module: "protocol" },
{ requiredPermissions: "read", section: "club", module: "member" }, { requiredPermission: "read", section: "club", module: "member" },
]), ]),
protocol protocol
); );
@ -125,19 +125,19 @@ router.use("/calendar", PermissionHelper.passCheckMiddleware("read", "club", "ca
router.use( router.use(
"/querybuilder", "/querybuilder",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "club", module: "query" }, { requiredPermission: "read", section: "club", module: "query" },
{ requiredPermissions: "read", section: "configuration", module: "query_store" }, { requiredPermission: "read", section: "configuration", module: "query_store" },
]), ]),
queryBuilder queryBuilder
); );
router.use( router.use(
"/newsletter", "/newsletter",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "club", module: "newsletter" }, { requiredPermission: "read", section: "club", module: "newsletter" },
{ requiredPermissions: "read", section: "club", module: "member" }, { requiredPermission: "read", section: "club", module: "member" },
{ requiredPermissions: "read", section: "club", module: "calendar" }, { requiredPermission: "read", section: "club", module: "calendar" },
{ requiredPermissions: "read", section: "club", module: "query" }, { requiredPermission: "read", section: "club", module: "query" },
{ requiredPermissions: "read", section: "configuration", module: "query_store" }, { requiredPermission: "read", section: "configuration", module: "query_store" },
]), ]),
newsletter newsletter
); );
@ -147,8 +147,8 @@ router.use("/role", PermissionHelper.passCheckMiddleware("read", "management", "
router.use( router.use(
"/user", "/user",
PermissionHelper.passCheckSomeMiddleware([ PermissionHelper.passCheckSomeMiddleware([
{ requiredPermissions: "read", section: "management", module: "user" }, { requiredPermission: "read", section: "management", module: "user" },
{ requiredPermissions: "read", section: "management", module: "role" }, { requiredPermission: "read", section: "management", module: "role" },
]), ]),
user user
); );

View file

@ -34,15 +34,15 @@ router.post("/verify", async (req, res) => {
await verifyMyTotp(req, res); await verifyMyTotp(req, res);
}); });
router.post("/changepw", async (req, res) => { router.patch("/changepw", async (req, res) => {
await changeMyPassword(req, res); await changeMyPassword(req, res);
}); });
router.post("/changeToTOTP", async (req, res) => { router.patch("/changeToTOTP", async (req, res) => {
await changeToTOTP(req, res); await changeToTOTP(req, res);
}); });
router.post("/changeToPW", async (req, res) => { router.patch("/changeToPW", async (req, res) => {
await changeToPW(req, res); await changeToPW(req, res);
}); });

View file

@ -98,5 +98,7 @@ export const sectionsAndModules: SectionsAndModulesObject = {
"newsletter_config", "newsletter_config",
], ],
management: ["user", "role", "webapi", "backup", "setting"], management: ["user", "role", "webapi", "backup", "setting"],
additional: [], additional: [
//{ key: "val", name: "name", type: "number", emptyIfAdmin: true },
],
}; };