UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

312 lines (294 loc) 8.8 kB
import { $inject, z } from "alepha"; import { $secure } from "alepha/security"; import { $action, okSchema } from "alepha/server"; import { completePasswordResetRequestSchema } from "../schemas/completePasswordResetRequestSchema.ts"; import { completeRegistrationRequestSchema } from "../schemas/completeRegistrationRequestSchema.ts"; import { passwordResetIntentResponseSchema } from "../schemas/passwordResetIntentResponseSchema.ts"; import { registerQuerySchema } from "../schemas/registerQuerySchema.ts"; import { registerRequestSchema } from "../schemas/registerRequestSchema.ts"; import { registrationIntentResponseSchema } from "../schemas/registrationIntentResponseSchema.ts"; import { userResourceSchema } from "../schemas/userResourceSchema.ts"; import { CredentialService } from "../services/CredentialService.ts"; import { RegistrationService } from "../services/RegistrationService.ts"; import { UserService } from "../services/UserService.ts"; export class UserController { protected readonly url = "/users"; protected readonly group = "users"; protected readonly credentialService = $inject(CredentialService); protected readonly userService = $inject(UserService); protected readonly registrationService = $inject(RegistrationService); /** * Phase 1: Create a registration intent. * Validates data, creates verification sessions, and stores intent in cache. */ public readonly createRegistrationIntent = $action({ group: this.group, method: "POST", path: `${this.url}/register`, schema: { body: registerRequestSchema, query: registerQuerySchema, response: registrationIntentResponseSchema, }, handler: ({ body, query }) => this.registrationService.createRegistrationIntent( body, query.userRealmName, ), }); /** * Phase 2: Complete registration using an intent. * Validates verification codes and creates the user. */ public readonly createUserFromIntent = $action({ group: this.group, method: "POST", path: `${this.url}/register/complete`, schema: { body: completeRegistrationRequestSchema, response: userResourceSchema, }, handler: ({ body }) => this.registrationService.completeRegistration(body), }); /** * Phase 1: Create a password reset intent. * Validates email, sends verification code, and stores intent in cache. */ public readonly createPasswordResetIntent = $action({ group: this.group, method: "POST", path: `${this.url}/password-reset`, schema: { query: z.object({ userRealmName: z.string().optional(), }), body: z.object({ email: z.email(), }), response: passwordResetIntentResponseSchema, }, handler: ({ body, query }) => this.credentialService.createPasswordResetIntent( body.email, query.userRealmName, ), }); /** * Phase 2: Complete password reset using an intent. * Validates verification code, updates password, and invalidates sessions. */ public readonly completePasswordReset = $action({ group: this.group, method: "POST", path: `${this.url}/password-reset/complete`, schema: { body: completePasswordResetRequestSchema, response: okSchema, }, handler: async ({ body }) => { await this.credentialService.completePasswordReset(body); return { ok: true }; }, }); // Legacy endpoints for backward compatibility /** * @deprecated Use createPasswordResetIntent instead */ public requestPasswordReset = $action({ path: "/users/password-reset/request", group: this.group, schema: { query: z.object({ userRealmName: z.string().optional(), }), body: z.object({ email: z.email(), }), response: z.object({ success: z.boolean(), message: z.string(), }), }, handler: async ({ body, query }) => { await this.credentialService.requestPasswordReset( body.email, query.userRealmName, ); return { success: true, message: "If an account exists with this email, a password reset code has been sent.", }; }, }); /** * @deprecated Use completePasswordReset instead */ public validateResetToken = $action({ path: "/users/password-reset/validate", group: this.group, schema: { query: z.object({ email: z.email(), token: z.string(), userRealmName: z.string().optional(), }), response: z.object({ valid: z.boolean(), email: z.email().optional(), }), }, handler: async ({ query }) => { try { const email = await this.credentialService.validateResetToken( query.email, query.token, query.userRealmName, ); return { valid: true, email, }; } catch { return { valid: false, }; } }, }); /** * @deprecated Use completePasswordReset instead */ public resetPassword = $action({ path: "/users/password-reset/reset", group: this.group, schema: { query: z.object({ userRealmName: z.string().optional(), }), body: z.object({ email: z.email(), token: z.string(), newPassword: z.string().min(8), }), response: z.object({ success: z.boolean(), message: z.string(), }), }, handler: async ({ body, query }) => { await this.credentialService.resetPassword( body.email, body.token, body.newPassword, query.userRealmName, ); return { success: true, message: "Password has been reset successfully. Please log in.", }; }, }); /** * Request email verification. * Generates a verification token using verification service and sends an email to the user. * @param method - The verification method: "code" (default) sends a 6-digit code, "link" sends a clickable verification link. * @param verifyUrl - Required when method is "link". The base URL for the verification link. Token and email will be appended as query params. */ public requestEmailVerification = $action({ path: "/users/email-verification/request", group: this.group, schema: { query: z.object({ userRealmName: z.string().optional(), method: z .enum(["code", "link"]) .describe( 'Verification method: "code" sends a 6-digit code, "link" sends a clickable verification link. When using "link", configure verifyEmailUrl in realm settings.', ) .default("code") .optional(), }), body: z.object({ email: z.email(), }), response: z.object({ success: z.boolean(), message: z.string(), }), }, handler: async ({ body, query }) => { const method = query.method ?? "code"; await this.userService.requestEmailVerification( body.email, query.userRealmName, method, ); return { success: true, message: method === "link" ? "If an account exists with this email, a verification link has been sent." : "If an account exists with this email, a verification code has been sent.", }; }, }); /** * Verify email with a valid token. * Updates the user's emailVerified status. */ public verifyEmail = $action({ path: "/users/email-verification/verify", group: this.group, schema: { query: z.object({ userRealmName: z.string().optional(), }), body: z.object({ email: z.email(), token: z.string(), }), response: z.object({ success: z.boolean(), message: z.string(), }), }, handler: async ({ body, query }) => { await this.userService.verifyEmail( body.email, body.token, query.userRealmName, ); return { success: true, message: "Email has been verified successfully.", }; }, }); /** * Check if an email is verified. */ public checkEmailVerification = $action({ path: "/users/email-verification/check", group: this.group, use: [$secure()], schema: { query: z.object({ email: z.email(), userRealmName: z.string().optional(), }), response: z.object({ verified: z.boolean(), }), }, handler: async ({ query }) => { const verified = await this.userService.isEmailVerified( query.email, query.userRealmName, ); return { verified, }; }, }); }