This repository has no description
Something went wrong. Try again.
13 kB · 374 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375// SPDX-License-Identifier: AGPL-3.0-or-later
import {requireClientIp} from '@fluxer/ip_utils/src/ClientIp';import { DisableTotpRequest, EnableMfaTotpRequest, InboundSmsChallengeStartResponse, MfaBackupCodesRequest, MfaBackupCodesResponse, PhoneSendVerificationRequest, PhoneSendVerificationResponse, PhoneVerifyRequest, PhoneVerifyResponse, SudoMfaMethodsResponse, SudoVerificationSchema, WebAuthnChallengeResponse, WebAuthnCredentialListResponse, WebAuthnCredentialUpdateRequest, WebAuthnRegisterRequest,} from '@fluxer/schema/src/domains/auth/AuthSchemas';import {CredentialIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';import {requireSudoMode} from '../../auth/services/SudoVerificationService';import {Config} from '../../Config';import {DefaultUserOnly, LoginRequired, LoginRequiredAllowSuspicious} from '../../middleware/AuthMiddleware';import {RateLimitMiddleware} from '../../middleware/RateLimitMiddleware';import {OpenAPI} from '../../middleware/ResponseTypeMiddleware';import {SudoModeMiddleware} from '../../middleware/SudoModeMiddleware';import {RateLimitConfigs} from '../../RateLimitConfig';import type {HonoApp} from '../../types/HonoEnv';import {Validator} from '../../Validator';
export function UserAuthController(app: HonoApp) { app.post( '/users/@me/mfa/totp/enable', RateLimitMiddleware(RateLimitConfigs.USER_MFA_TOTP_ENABLE), LoginRequired, DefaultUserOnly, SudoModeMiddleware, Validator('json', EnableMfaTotpRequest), OpenAPI({ operationId: 'enable_totp_mfa', summary: 'Enable TOTP multi-factor authentication', responseSchema: MfaBackupCodesResponse, statusCode: 200, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: 'Enable time-based one-time password (TOTP) MFA on the current account. Returns backup codes for account recovery. Requires sudo mode verification.', }), async (ctx) => { const body = ctx.req.valid('json'); const user = ctx.get('user'); const sudoResult = await requireSudoMode(ctx, user, body); return ctx.json( await ctx.get('userAuthRequestService').enableTotp({ user, data: body, sudoContext: sudoResult, }), ); }, ); app.post( '/users/@me/mfa/totp/disable', RateLimitMiddleware(RateLimitConfigs.USER_MFA_TOTP_DISABLE), LoginRequired, DefaultUserOnly, SudoModeMiddleware, Validator('json', DisableTotpRequest), OpenAPI({ operationId: 'disable_totp_mfa', summary: 'Disable TOTP multi-factor authentication', responseSchema: null, statusCode: 204, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: 'Disable TOTP multi-factor authentication on the current account. Requires sudo mode verification for security.', }), async (ctx) => { const body = ctx.req.valid('json'); const user = ctx.get('user'); const sudoResult = await requireSudoMode(ctx, user, body); await ctx.get('userAuthRequestService').disableTotp({user, data: body, sudoContext: sudoResult}); return ctx.body(null, 204); }, ); app.post( '/users/@me/mfa/backup-codes', RateLimitMiddleware(RateLimitConfigs.USER_MFA_BACKUP_CODES), LoginRequired, DefaultUserOnly, SudoModeMiddleware, Validator('json', MfaBackupCodesRequest), OpenAPI({ operationId: 'get_backup_codes_mfa', summary: 'Get backup codes for multi-factor authentication', responseSchema: MfaBackupCodesResponse, statusCode: 200, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: 'Generate and retrieve new backup codes for account recovery. Requires sudo mode verification. Old codes are invalidated.', }), async (ctx) => { const body = ctx.req.valid('json'); const user = ctx.get('user'); const sudoResult = await requireSudoMode(ctx, user, body); return ctx.json( await ctx.get('userAuthRequestService').getBackupCodes({user, data: body, sudoContext: sudoResult}), ); }, ); app.post( '/users/@me/phone/send-verification', RateLimitMiddleware(RateLimitConfigs.PHONE_SEND_VERIFICATION), LoginRequiredAllowSuspicious, DefaultUserOnly, Validator('json', PhoneSendVerificationRequest), OpenAPI({ operationId: 'send_phone_verification_code', summary: 'Send phone verification code', responseSchema: PhoneSendVerificationResponse, statusCode: 200, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: 'Send a one-time code on the requested channel. Defaults to the first available channel from server policy. Pass channel="sms" to request SMS (only honoured for SMS-allowlisted destinations) or channel="inbound_challenge" to receive challenge details to text in. Expensive outbound destinations always downgrade to an inbound challenge.', }), async (ctx) => { return ctx.json( await ctx.get('userAuthRequestService').sendPhoneVerificationCode({ user: ctx.get('user'), data: ctx.req.valid('json'), clientIp: requireClientIp(ctx.req.raw, { trustClientIpHeader: Config.proxy.trust_client_ip_header, clientIpHeaderName: Config.proxy.client_ip_header, }), }), ); }, ); app.post( '/users/@me/phone/inbound-challenge', RateLimitMiddleware(RateLimitConfigs.PHONE_SEND_VERIFICATION), LoginRequiredAllowSuspicious, DefaultUserOnly, OpenAPI({ operationId: 'start_inbound_phone_challenge', summary: 'Start an inbound SMS challenge', responseSchema: InboundSmsChallengeStartResponse, statusCode: 200, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: "For very-high-risk registrations the platform requires the user to text a one-time code to the platform's number, instead of receiving a code from the platform. This endpoint generates the code and the destination number to display.", }), async (ctx) => { return ctx.json(await ctx.get('userAuthRequestService').startInboundPhoneChallenge(ctx.get('user'))); }, ); app.post( '/users/@me/phone/verify', RateLimitMiddleware(RateLimitConfigs.PHONE_VERIFY_CODE), LoginRequiredAllowSuspicious, DefaultUserOnly, Validator('json', PhoneVerifyRequest), OpenAPI({ operationId: 'verify_phone_code', summary: 'Verify phone code', responseSchema: PhoneVerifyResponse, statusCode: 200, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: 'Verify a phone number by confirming the SMS verification code. Returns phone verification status.', }), async (ctx) => { return ctx.json( await ctx.get('userAuthRequestService').verifyPhoneCode({user: ctx.get('user'), data: ctx.req.valid('json')}), ); }, ); app.delete( '/users/@me/authorized-ips', RateLimitMiddleware(RateLimitConfigs.USER_AUTHORIZED_IPS_FORGET), LoginRequired, DefaultUserOnly, SudoModeMiddleware, Validator('json', SudoVerificationSchema), OpenAPI({ operationId: 'forget_authorized_ips', summary: 'Forget authorized IPs for current user', responseSchema: null, statusCode: 204, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: 'Clears all authorized IP addresses for the current user. After calling this endpoint, the user will be required to re-authorize any new IP addresses they log in from. Requires sudo mode verification.', }), async (ctx) => { const user = ctx.get('user'); const body = ctx.req.valid('json'); await requireSudoMode(ctx, user, body); await ctx.get('userAuthRequestService').forgetAuthorizedIps(user); return ctx.body(null, 204); }, ); app.get( '/users/@me/mfa/webauthn/credentials', RateLimitMiddleware(RateLimitConfigs.MFA_WEBAUTHN_LIST), LoginRequired, DefaultUserOnly, OpenAPI({ operationId: 'list_webauthn_credentials', summary: 'List WebAuthn credentials', responseSchema: WebAuthnCredentialListResponse, statusCode: 200, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: 'Retrieve all registered WebAuthn credentials (security keys, biometric devices) for the current user. Requires authentication.', }), async (ctx) => { return ctx.json(await ctx.get('userAuthRequestService').listWebAuthnCredentials(ctx.get('user'))); }, ); app.post( '/users/@me/mfa/webauthn/credentials/registration-options', RateLimitMiddleware(RateLimitConfigs.MFA_WEBAUTHN_REGISTRATION_OPTIONS), LoginRequired, DefaultUserOnly, SudoModeMiddleware, Validator('json', SudoVerificationSchema), OpenAPI({ operationId: 'get_webauthn_registration_options', summary: 'Get WebAuthn registration options', responseSchema: WebAuthnChallengeResponse, statusCode: 200, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: 'Generate challenge and options to register a new WebAuthn credential. Requires sudo mode verification.', }), async (ctx) => { const user = ctx.get('user'); const body = ctx.req.valid('json'); await requireSudoMode(ctx, user, body, { issueSudoToken: false, }); return ctx.json(await ctx.get('userAuthRequestService').generateWebAuthnRegistrationOptions(user)); }, ); app.post( '/users/@me/mfa/webauthn/credentials', RateLimitMiddleware(RateLimitConfigs.MFA_WEBAUTHN_REGISTER), LoginRequired, DefaultUserOnly, Validator('json', WebAuthnRegisterRequest), OpenAPI({ operationId: 'register_webauthn_credential', summary: 'Register WebAuthn credential', responseSchema: null, statusCode: 204, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: 'Complete registration of a new WebAuthn credential (security key or biometric device) using a challenge created after sudo mode verification.', }), async (ctx) => { const user = ctx.get('user'); const body = ctx.req.valid('json'); await ctx.get('userAuthRequestService').registerWebAuthnCredential({ user, data: body, }); return ctx.body(null, 204); }, ); app.patch( '/users/@me/mfa/webauthn/credentials/:credential_id', RateLimitMiddleware(RateLimitConfigs.MFA_WEBAUTHN_UPDATE), LoginRequired, DefaultUserOnly, Validator('param', CredentialIdParam), Validator('json', WebAuthnCredentialUpdateRequest), SudoModeMiddleware, OpenAPI({ operationId: 'update_webauthn_credential', summary: 'Update WebAuthn credential', responseSchema: null, statusCode: 204, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: 'Update the name or settings of a registered WebAuthn credential. Requires sudo mode verification.', }), async (ctx) => { const user = ctx.get('user'); const {credential_id} = ctx.req.valid('param'); const {name, ...sudoBody} = ctx.req.valid('json'); await requireSudoMode(ctx, user, sudoBody); await ctx.get('userAuthRequestService').renameWebAuthnCredential({ user, credentialId: credential_id, data: {name}, }); return ctx.body(null, 204); }, ); app.delete( '/users/@me/mfa/webauthn/credentials/:credential_id', RateLimitMiddleware(RateLimitConfigs.MFA_WEBAUTHN_DELETE), LoginRequired, DefaultUserOnly, Validator('param', CredentialIdParam), SudoModeMiddleware, Validator('json', SudoVerificationSchema), OpenAPI({ operationId: 'delete_webauthn_credential', summary: 'Delete WebAuthn credential', responseSchema: null, statusCode: 204, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: 'Remove a registered WebAuthn credential from the current account. Requires sudo mode verification for security.', }), async (ctx) => { const user = ctx.get('user'); const {credential_id} = ctx.req.valid('param'); const body = ctx.req.valid('json'); await requireSudoMode(ctx, user, body); await ctx.get('userAuthRequestService').deleteWebAuthnCredential({user, credentialId: credential_id}); return ctx.body(null, 204); }, ); app.get( '/users/@me/sudo/mfa-methods', RateLimitMiddleware(RateLimitConfigs.SUDO_MFA_METHODS), LoginRequired, DefaultUserOnly, OpenAPI({ operationId: 'list_sudo_mfa_methods', summary: 'List sudo multi-factor authentication methods', responseSchema: SudoMfaMethodsResponse, statusCode: 200, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: 'Retrieve all available MFA methods for sudo mode verification (TOTP, WebAuthn). Requires authentication.', }), async (ctx) => { return ctx.json(await ctx.get('userAuthRequestService').listSudoMfaMethods(ctx.get('user'))); }, ); app.post( '/users/@me/sudo/webauthn/authentication-options', RateLimitMiddleware(RateLimitConfigs.SUDO_WEBAUTHN_OPTIONS), LoginRequired, DefaultUserOnly, OpenAPI({ operationId: 'get_sudo_webauthn_authentication_options', summary: 'Get sudo WebAuthn authentication options', responseSchema: WebAuthnChallengeResponse, statusCode: 200, security: ['bearerToken', 'sessionToken'], tags: ['Users'], description: 'Generate WebAuthn challenge for sudo mode verification using a registered security key or biometric device.', }), async (ctx) => { return ctx.json(await ctx.get('userAuthRequestService').getSudoWebAuthnOptions(ctx.get('user'))); }, );}