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

@@ -26,6 +26,7 @@ function createConfig(overrides: Partial<AppConfig['security']> = {}): AppConfig
},
adminSeed: { username: 'admin', email: 'admin@example.com', password: 'password-123' },
runtime: { modulesDir: '/data/modules', logsDir: '/data/logs' },
marketplace: { publicUrl: 'http://127.0.0.1:8081', tokenEncryptionKey: '', providers: {} },
};
}
@@ -52,7 +53,7 @@ function createUserRecord(overrides: Partial<UserRecord> = {}): UserRecord {
class MockUserRepository {
public findByUsernameResult: UserRecord | null = null;
public updateLoginSuccessCalls: string[] = [];
public updateLoginFailureCalls: Array<{ userId: string; attempts: number; shouldLock: boolean; lockoutMinutes: number }> = [];
public updateLoginFailureCalls: string[] = [];
async findByUsername(): Promise<UserRecord | null> {
return this.findByUsernameResult;
@@ -62,8 +63,8 @@ class MockUserRepository {
this.updateLoginSuccessCalls.push(userId);
}
async updateLoginFailure(userId: string, attempts: number, shouldLock: boolean, lockoutMinutes: number): Promise<void> {
this.updateLoginFailureCalls.push({ userId, attempts, shouldLock, lockoutMinutes });
async updateLoginFailure(userId: string): Promise<void> {
this.updateLoginFailureCalls.push(userId);
}
}
@@ -158,12 +159,12 @@ describe('AuthService', () => {
).rejects.toThrow(UnauthorizedException);
expect(userRepository.updateLoginFailureCalls).toEqual([
{ userId: 'user-1', attempts: 1, shouldLock: false, lockoutMinutes: 15 },
'user-1',
]);
expect(auditService.records.at(-1)?.action).toBe(AUDIT_ACTIONS.LOGIN_FAILED);
});
it('sperrt das Konto nach Erreichen der maximalen Fehlversuche', async () => {
it('verhindert Loginversuche nicht durch Kontosperren', async () => {
const passwordHash = await passwordHasher.hash('Sicheres-Passwort-1');
userRepository.findByUsernameResult = createUserRecord({
passwordHash,
@@ -175,9 +176,9 @@ describe('AuthService', () => {
).rejects.toThrow(UnauthorizedException);
expect(userRepository.updateLoginFailureCalls).toEqual([
{ userId: 'user-1', attempts: 3, shouldLock: true, lockoutMinutes: 15 },
'user-1',
]);
expect(auditService.records.at(-1)?.action).toBe(AUDIT_ACTIONS.LOGIN_LOCKED);
expect(auditService.records.at(-1)?.action).toBe(AUDIT_ACTIONS.LOGIN_FAILED);
});
it('lehnt gesperrte Benutzer ab', async () => {
@@ -187,11 +188,12 @@ describe('AuthService', () => {
lockedUntil: new Date(Date.now() + 60_000),
});
await expect(
authService.login({ username: 'max', password: 'Sicheres-Passwort-1', ipAddress: '127.0.0.1' },
)).rejects.toThrow(UnauthorizedException);
expect(auditService.records.at(-1)?.details).toEqual({ reason: 'ACCOUNT_LOCKED' });
const result = await authService.login({
username: 'max',
password: 'Sicheres-Passwort-1',
ipAddress: '127.0.0.1',
});
expect(result.user.username).toBe('max');
});
it('lehnt deaktivierte Benutzer ab', async () => {
@@ -226,7 +228,7 @@ describe('AuthService', () => {
expect(auditService.records.at(-1)?.details).toEqual({ reason: 'RATE_LIMITED' });
});
it('setzt das Rate-Limit-Fenster nach erfolgreichem Login zurück', async () => {
it('setzt das Rate-Limit-Fenster nach erfolgreichem Login nicht zurück', async () => {
const passwordHash = await passwordHasher.hash('Sicheres-Passwort-1');
userRepository.findByUsernameResult = createUserRecord({ passwordHash });
@@ -243,13 +245,11 @@ describe('AuthService', () => {
});
expect(result.user.username).toBe('max');
// Nach Reset ist ein neuer Login sofort wieder möglich.
const secondResult = await authService.login({
await expect(authService.login({
username: 'max',
password: 'Sicheres-Passwort-1',
ipAddress: '127.0.0.1',
});
expect(secondResult.user.username).toBe('max');
})).rejects.toThrow('Zu viele Anmeldeversuche. Bitte später erneut versuchen.');
});
});
@@ -268,4 +268,4 @@ describe('AuthService', () => {
expect(auditService.records.at(-1)?.action).toBe(AUDIT_ACTIONS.LOGOUT);
});
});
});
});