feat: marketplace modules and isolated container management

This commit is contained in:
ayde64
2026-10-07 23:18:02 +02:00
parent 564f7def1c
commit 7b121fa908
68 changed files with 2560 additions and 699 deletions

View File

@@ -17,7 +17,9 @@ export interface SecurityConfig {
readonly sessionTtlMinutes: number;
readonly cookieSecure: boolean;
readonly behindProxy: boolean;
/** @deprecated Account lockout was removed to prevent attacker-triggered account denial. */
readonly loginMaxAttempts: number;
/** @deprecated Account lockout was removed to prevent attacker-triggered account denial. */
readonly loginLockoutMinutes: number;
readonly loginRateLimitAttempts: number;
readonly loginRateLimitWindowMinutes: number;
@@ -32,6 +34,23 @@ export interface AdminSeedConfig {
export interface RuntimeConfig {
readonly modulesDir: string;
readonly logsDir: string;
readonly moduleUidBase?: number;
}
export interface MarketplaceProviderConfig {
readonly clientId: string;
readonly clientSecret: string;
readonly baseUrl: string;
}
export interface MarketplaceConfig {
readonly publicUrl: string;
readonly tokenEncryptionKey: string;
readonly providers: {
readonly github?: MarketplaceProviderConfig;
readonly gitea?: MarketplaceProviderConfig;
readonly forgejo?: MarketplaceProviderConfig;
};
}
export interface AppConfig {
@@ -41,6 +60,7 @@ export interface AppConfig {
readonly security: SecurityConfig;
readonly adminSeed: AdminSeedConfig;
readonly runtime: RuntimeConfig;
readonly marketplace: MarketplaceConfig;
}
const booleanFromString = z
@@ -63,7 +83,80 @@ const environmentSchema = z.object({
ADMIN_EMAIL: z.string().trim().email(),
ADMIN_PASSWORD: z.string().min(10, 'ADMIN_PASSWORD muss mindestens 10 Zeichen lang sein').max(200),
MODULES_DIR: z.string().min(1).default('./data/modules'),
MODULE_DATA_DIR: z.string().min(1).default('./data/module-data'),
LOGS_DIR: z.string().min(1).default('./data/logs'),
MODULE_UID_BASE: z.coerce.number().int().min(10_000).max(64_535).optional(),
MARKETPLACE_PUBLIC_URL: z.string().url().default('http://127.0.0.1:8081'),
MARKETPLACE_TOKEN_ENCRYPTION_KEY: z.string().default(''),
GITHUB_OAUTH_CLIENT_ID: z.string().default(''),
GITHUB_OAUTH_CLIENT_SECRET: z.string().default(''),
GITEA_BASE_URL: z.string().default(''),
GITEA_OAUTH_CLIENT_ID: z.string().default(''),
GITEA_OAUTH_CLIENT_SECRET: z.string().default(''),
FORGEJO_BASE_URL: z.string().default(''),
FORGEJO_OAUTH_CLIENT_ID: z.string().default(''),
FORGEJO_OAUTH_CLIENT_SECRET: z.string().default(''),
}).superRefine((environment, context) => {
if (environment.NODE_ENV === 'production' && !environment.COOKIE_SECURE) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['COOKIE_SECURE'],
message: 'COOKIE_SECURE muss in production auf true gesetzt sein',
});
}
if (environment.NODE_ENV === 'production' && environment.MODULE_UID_BASE === undefined) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['MODULE_UID_BASE'],
message: 'MODULE_UID_BASE ist in production erforderlich, damit Module getrennte UIDs erhalten',
});
}
const oauthFields = [
environment.GITHUB_OAUTH_CLIENT_ID,
environment.GITHUB_OAUTH_CLIENT_SECRET,
environment.GITEA_BASE_URL,
environment.GITEA_OAUTH_CLIENT_ID,
environment.GITEA_OAUTH_CLIENT_SECRET,
environment.FORGEJO_BASE_URL,
environment.FORGEJO_OAUTH_CLIENT_ID,
environment.FORGEJO_OAUTH_CLIENT_SECRET,
];
if (oauthFields.some(Boolean) && environment.MARKETPLACE_TOKEN_ENCRYPTION_KEY.length < 32) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: ['MARKETPLACE_TOKEN_ENCRYPTION_KEY'],
message: 'Bei aktivierten OAuth-Anbietern ist ein Schlüssel mit mindestens 32 Zeichen erforderlich',
});
}
for (const [provider, fields] of [
['GITHUB', [environment.GITHUB_OAUTH_CLIENT_ID, environment.GITHUB_OAUTH_CLIENT_SECRET]],
['GITEA', [environment.GITEA_BASE_URL, environment.GITEA_OAUTH_CLIENT_ID, environment.GITEA_OAUTH_CLIENT_SECRET]],
['FORGEJO', [environment.FORGEJO_BASE_URL, environment.FORGEJO_OAUTH_CLIENT_ID, environment.FORGEJO_OAUTH_CLIENT_SECRET]],
] as const) {
if (fields.some(Boolean) && fields.some((field) => !field)) {
context.addIssue({
code: z.ZodIssueCode.custom,
path: [`${provider}_OAUTH_CLIENT_ID`],
message: `OAuth-Konfiguration für ${provider} ist unvollständig`,
});
}
}
for (const [field, value] of [['GITEA_BASE_URL', environment.GITEA_BASE_URL], ['FORGEJO_BASE_URL', environment.FORGEJO_BASE_URL]] as const) {
if (!value) continue;
try {
const url = new URL(value);
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname))) {
throw new Error('protocol');
}
if (url.username || url.password || url.search || url.hash) throw new Error('url');
} catch {
context.addIssue({
code: z.ZodIssueCode.custom,
path: [field],
message: 'Forge-URL muss HTTPS verwenden (HTTP ist nur lokal zulässig) und darf keine Zugangsdaten enthalten',
});
}
}
});
/** Lädt und validiert die Konfiguration aus den Umgebungsvariablen. */
@@ -91,6 +184,24 @@ export function loadConfiguration(): AppConfig {
runtime: {
modulesDir: path.resolve(environment.MODULES_DIR),
logsDir: path.resolve(environment.LOGS_DIR),
...(environment.MODULE_UID_BASE !== undefined
? { moduleUidBase: environment.MODULE_UID_BASE }
: {}),
},
marketplace: {
publicUrl: environment.MARKETPLACE_PUBLIC_URL.replace(/\/$/, ''),
tokenEncryptionKey: environment.MARKETPLACE_TOKEN_ENCRYPTION_KEY,
providers: {
...(environment.GITHUB_OAUTH_CLIENT_ID && environment.GITHUB_OAUTH_CLIENT_SECRET
? { github: { clientId: environment.GITHUB_OAUTH_CLIENT_ID, clientSecret: environment.GITHUB_OAUTH_CLIENT_SECRET, baseUrl: 'https://github.com' } }
: {}),
...(environment.GITEA_BASE_URL && environment.GITEA_OAUTH_CLIENT_ID && environment.GITEA_OAUTH_CLIENT_SECRET
? { gitea: { clientId: environment.GITEA_OAUTH_CLIENT_ID, clientSecret: environment.GITEA_OAUTH_CLIENT_SECRET, baseUrl: environment.GITEA_BASE_URL.replace(/\/$/, '') } }
: {}),
...(environment.FORGEJO_BASE_URL && environment.FORGEJO_OAUTH_CLIENT_ID && environment.FORGEJO_OAUTH_CLIENT_SECRET
? { forgejo: { clientId: environment.FORGEJO_OAUTH_CLIENT_ID, clientSecret: environment.FORGEJO_OAUTH_CLIENT_SECRET, baseUrl: environment.FORGEJO_BASE_URL.replace(/\/$/, '') } }
: {}),
},
},
};
}
}