import { AppError } from '@/shared/utils.js'; type ApiKeyRow = Record & { api_key: string }; type NotificationPreferences = Record & { channels?: Record & { webPush?: boolean }; }; type SettingsDependencies = { apiKeys: { list(userId: number): ApiKeyRow[]; create(userId: number, keyName: string): unknown; remove(userId: number, keyId: number): boolean; toggle(userId: number, keyId: number, isActive: boolean): boolean; }; credentials: { list(userId: number, credentialType: string | null): unknown[]; create( userId: number, name: string, type: string, value: string, description: string | null, ): unknown; remove(userId: number, credentialId: number): boolean; toggle(userId: number, credentialId: number, isActive: boolean): boolean; }; notifications: { getPreferences(userId: number): NotificationPreferences | undefined; updatePreferences(userId: number, preferences: NotificationPreferences): unknown; createEnabledEvent(): unknown; notifyUser(userId: number, event: unknown): void | Promise; }; pushSubscriptions: { save(userId: number, endpoint: string, p256dh: string, auth: string): void; remove(endpoint: string): void; }; getVapidPublicKey(): string | null; }; function requiredString(value: unknown, fieldName: string, code: string): string { const normalizedValue = typeof value === 'string' ? value.trim() : ''; if (!normalizedValue) { throw new AppError(`${fieldName} is required`, { code, statusCode: 400 }); } return normalizedValue; } function assertFound(found: boolean, resourceName: string, code: string): void { if (!found) { throw new AppError(`${resourceName} not found`, { code, statusCode: 404 }); } } /** Creates settings workflows with repositories and notification effects injected. */ export function createSettingsService(dependencies: SettingsDependencies) { return { listApiKeys(userId: number) { const apiKeys = dependencies.apiKeys.list(userId).map((key) => ({ ...key, api_key: `${key.api_key.substring(0, 10)}...`, })); return { apiKeys }; }, createApiKey(userId: number, keyNameInput: unknown) { const keyName = requiredString(keyNameInput, 'Key name', 'API_KEY_NAME_REQUIRED'); return { success: true, apiKey: dependencies.apiKeys.create(userId, keyName) }; }, deleteApiKey(userId: number, keyId: number) { assertFound(dependencies.apiKeys.remove(userId, keyId), 'API key', 'API_KEY_NOT_FOUND'); return { success: true }; }, toggleApiKey(userId: number, keyId: number, isActive: unknown) { if (typeof isActive !== 'boolean') { throw new AppError('isActive must be a boolean', { code: 'INVALID_ACTIVE_STATE', statusCode: 400, }); } assertFound( dependencies.apiKeys.toggle(userId, keyId, isActive), 'API key', 'API_KEY_NOT_FOUND', ); return { success: true }; }, listCredentials(userId: number, credentialType: string | null) { return { credentials: dependencies.credentials.list(userId, credentialType) }; }, createCredential(userId: number, input: Record) { const credentialName = requiredString( input.credentialName, 'Credential name', 'CREDENTIAL_NAME_REQUIRED', ); const credentialType = requiredString( input.credentialType, 'Credential type', 'CREDENTIAL_TYPE_REQUIRED', ); const credentialValue = requiredString( input.credentialValue, 'Credential value', 'CREDENTIAL_VALUE_REQUIRED', ); const description = typeof input.description === 'string' ? input.description.trim() || null : null; return { success: true, credential: dependencies.credentials.create( userId, credentialName, credentialType, credentialValue, description, ), }; }, deleteCredential(userId: number, credentialId: number) { assertFound( dependencies.credentials.remove(userId, credentialId), 'Credential', 'CREDENTIAL_NOT_FOUND', ); return { success: true }; }, toggleCredential(userId: number, credentialId: number, isActive: unknown) { if (typeof isActive !== 'boolean') { throw new AppError('isActive must be a boolean', { code: 'INVALID_ACTIVE_STATE', statusCode: 400, }); } assertFound( dependencies.credentials.toggle(userId, credentialId, isActive), 'Credential', 'CREDENTIAL_NOT_FOUND', ); return { success: true }; }, getNotificationPreferences(userId: number) { return { success: true, preferences: dependencies.notifications.getPreferences(userId) }; }, updateNotificationPreferences(userId: number, preferences: NotificationPreferences) { return { success: true, preferences: dependencies.notifications.updatePreferences(userId, preferences), }; }, getVapidPublicKey() { return { publicKey: dependencies.getVapidPublicKey() }; }, subscribeToPush(userId: number, input: Record) { const endpoint = requiredString(input.endpoint, 'Endpoint', 'PUSH_SUBSCRIPTION_REQUIRED'); const keys = typeof input.keys === 'object' && input.keys !== null ? input.keys as Record : {}; const p256dh = requiredString(keys.p256dh, 'p256dh', 'PUSH_SUBSCRIPTION_REQUIRED'); const auth = requiredString(keys.auth, 'auth', 'PUSH_SUBSCRIPTION_REQUIRED'); dependencies.pushSubscriptions.save(userId, endpoint, p256dh, auth); const currentPreferences = dependencies.notifications.getPreferences(userId); if (!currentPreferences?.channels?.webPush) { dependencies.notifications.updatePreferences(userId, { ...currentPreferences, channels: { ...currentPreferences?.channels, webPush: true }, }); } const event = dependencies.notifications.createEnabledEvent(); void dependencies.notifications.notifyUser(userId, event); return { success: true }; }, unsubscribeFromPush(userId: number, endpointInput: unknown) { const endpoint = requiredString(endpointInput, 'Endpoint', 'PUSH_ENDPOINT_REQUIRED'); dependencies.pushSubscriptions.remove(endpoint); const currentPreferences = dependencies.notifications.getPreferences(userId); if (currentPreferences?.channels?.webPush) { dependencies.notifications.updatePreferences(userId, { ...currentPreferences, channels: { ...currentPreferences.channels, webPush: false }, }); } return { success: true }; }, }; }