alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
312 lines (294 loc) • 8.8 kB
text/typescript
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,
};
},
});
}