feat: marketplace modules and isolated container management
This commit is contained in:
@@ -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(/\/$/, '') } }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user