create-warlock 4.2.6 → 4.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/package.json +2 -2
  3. package/templates/warlock/.env.example +36 -0
  4. package/templates/warlock/.gitattributes +1 -0
  5. package/templates/warlock/.husky/pre-commit +4 -0
  6. package/templates/warlock/.prettierignore +4 -0
  7. package/templates/warlock/.prettierrc.json +10 -0
  8. package/templates/warlock/.vscode/settings.json +41 -0
  9. package/templates/warlock/README.md +57 -0
  10. package/templates/warlock/_.gitignore +6 -0
  11. package/templates/warlock/docs/new-module.md +551 -0
  12. package/templates/warlock/eslint.config.js +98 -0
  13. package/templates/warlock/package.json +74 -0
  14. package/templates/warlock/public/home.css +523 -0
  15. package/templates/warlock/skills/api-design/SKILL.md +461 -0
  16. package/templates/warlock/skills/code-standards/SKILL.md +595 -0
  17. package/templates/warlock/skills/data-and-persistence/SKILL.md +330 -0
  18. package/templates/warlock/skills/git-workflow/SKILL.md +282 -0
  19. package/templates/warlock/skills/module-boundaries/SKILL.md +283 -0
  20. package/templates/warlock/skills/observability-and-resilience/SKILL.md +306 -0
  21. package/templates/warlock/skills/security-baseline/SKILL.md +352 -0
  22. package/templates/warlock/skills/testing-strategy/SKILL.md +323 -0
  23. package/templates/warlock/src/app/auth/controllers/forgot-password.controller.ts +28 -0
  24. package/templates/warlock/src/app/auth/controllers/login.controller.ts +22 -0
  25. package/templates/warlock/src/app/auth/controllers/logout-all.controller.ts +16 -0
  26. package/templates/warlock/src/app/auth/controllers/logout.controller.ts +16 -0
  27. package/templates/warlock/src/app/auth/controllers/me.controller.ts +13 -0
  28. package/templates/warlock/src/app/auth/controllers/refresh-token.controller.ts +29 -0
  29. package/templates/warlock/src/app/auth/controllers/reset-password.controller.ts +23 -0
  30. package/templates/warlock/src/app/auth/main.ts +9 -0
  31. package/templates/warlock/src/app/auth/models/otp/index.ts +1 -0
  32. package/templates/warlock/src/app/auth/models/otp/migrations/22-12-2025_10-30-20.otp-migration.ts +30 -0
  33. package/templates/warlock/src/app/auth/models/otp/otp.model.ts +69 -0
  34. package/templates/warlock/src/app/auth/requests/guarded.request.ts +10 -0
  35. package/templates/warlock/src/app/auth/routes.ts +22 -0
  36. package/templates/warlock/src/app/auth/schema/login.schema.ts +8 -0
  37. package/templates/warlock/src/app/auth/schema/reset-password.schema.ts +9 -0
  38. package/templates/warlock/src/app/auth/services/auth.service.ts +66 -0
  39. package/templates/warlock/src/app/auth/services/forgot-password.service.ts +28 -0
  40. package/templates/warlock/src/app/auth/services/otp.service.ts +173 -0
  41. package/templates/warlock/src/app/auth/services/reset-password.service.ts +39 -0
  42. package/templates/warlock/src/app/auth/utils/auth-error-code.ts +6 -0
  43. package/templates/warlock/src/app/auth/utils/locales.ts +89 -0
  44. package/templates/warlock/src/app/auth/utils/types.ts +14 -0
  45. package/templates/warlock/src/app/posts/controllers/create-new-post.controller.ts +21 -0
  46. package/templates/warlock/src/app/posts/controllers/update-post.controller.ts +30 -0
  47. package/templates/warlock/src/app/posts/models/post/migrations/09-01-2026_02-07-51-post.migration.ts +15 -0
  48. package/templates/warlock/src/app/posts/models/post/post.model.ts +23 -0
  49. package/templates/warlock/src/app/posts/resources/post.resource.ts +14 -0
  50. package/templates/warlock/src/app/posts/routes.ts +8 -0
  51. package/templates/warlock/src/app/posts/schema/create-post.schema.ts +9 -0
  52. package/templates/warlock/src/app/posts/schema/update-post.schema.ts +9 -0
  53. package/templates/warlock/src/app/shared/components/HomePageComponent.tsx +229 -0
  54. package/templates/warlock/src/app/shared/controllers/home-page.controller.ts +18 -0
  55. package/templates/warlock/src/app/shared/controllers/home-page.controller.tsx +8 -0
  56. package/templates/warlock/src/app/shared/routes.ts +4 -0
  57. package/templates/warlock/src/app/shared/services/scheduler.service.ts +3 -0
  58. package/templates/warlock/src/app/shared/tests/infrastructure.test.ts +22 -0
  59. package/templates/warlock/src/app/shared/utils/global-columns-schema.ts +8 -0
  60. package/templates/warlock/src/app/shared/utils/locales.ts +766 -0
  61. package/templates/warlock/src/app/shared/utils/router.ts +30 -0
  62. package/templates/warlock/src/app/uploads/controllers/fetch-uploaded-file.controller.ts +33 -0
  63. package/templates/warlock/src/app/uploads/routes.ts +4 -0
  64. package/templates/warlock/src/app/users/commands/hello-world.command.ts +8 -0
  65. package/templates/warlock/src/app/users/controllers/create-new-user.controller.ts +27 -0
  66. package/templates/warlock/src/app/users/controllers/list-users.controller.ts +12 -0
  67. package/templates/warlock/src/app/users/events/inject-created-by-user.into-model.event.ts +32 -0
  68. package/templates/warlock/src/app/users/events/sync.ts +5 -0
  69. package/templates/warlock/src/app/users/main.ts +5 -0
  70. package/templates/warlock/src/app/users/models/user/index.ts +1 -0
  71. package/templates/warlock/src/app/users/models/user/migrations/11-12-2025_23-58-03-user.migration.ts +15 -0
  72. package/templates/warlock/src/app/users/models/user/user.model.ts +64 -0
  73. package/templates/warlock/src/app/users/repositories/users.repository.ts +23 -0
  74. package/templates/warlock/src/app/users/resources/user.resource.ts +14 -0
  75. package/templates/warlock/src/app/users/routes.ts +8 -0
  76. package/templates/warlock/src/app/users/schema/create-user.schema.ts +11 -0
  77. package/templates/warlock/src/app/users/seeds/users.seed.ts +21 -0
  78. package/templates/warlock/src/app/users/services/get-users.service.ts +5 -0
  79. package/templates/warlock/src/app/users/services/list-users.service.ts +17 -0
  80. package/templates/warlock/src/app/users/services/login-social.ts +19 -0
  81. package/templates/warlock/src/config/app.ts +12 -0
  82. package/templates/warlock/src/config/auth.ts +20 -0
  83. package/templates/warlock/src/config/cache.ts +59 -0
  84. package/templates/warlock/src/config/database.ts +65 -0
  85. package/templates/warlock/src/config/http.ts +23 -0
  86. package/templates/warlock/src/config/log.ts +22 -0
  87. package/templates/warlock/src/config/mail.ts +16 -0
  88. package/templates/warlock/src/config/repository.ts +11 -0
  89. package/templates/warlock/src/config/storage.ts +34 -0
  90. package/templates/warlock/src/config/tests.ts +5 -0
  91. package/templates/warlock/src/config/validation.ts +7 -0
  92. package/templates/warlock/storage/.gitignore +2 -0
  93. package/templates/warlock/tsconfig.json +27 -0
  94. package/templates/warlock/warlock.config.ts +15 -0
  95. package/templates/warlock/yarn.lock +2332 -0
@@ -0,0 +1,28 @@
1
+ import { t, type Request, type RequestHandler, type Response } from "@warlock.js/core";
2
+ import { v } from "@warlock.js/seal";
3
+ import { forgotPasswordService } from "../services/forgot-password.service";
4
+
5
+ /**
6
+ * Forgot password controller
7
+ * POST /auth/forgot-password
8
+ */
9
+ export const forgotPasswordController: RequestHandler = async (
10
+ request: Request,
11
+ response: Response,
12
+ ) => {
13
+ const { email } = request.validated();
14
+
15
+ await forgotPasswordService(email);
16
+
17
+ return response.success({
18
+ message: t("auth.otpSent"),
19
+ });
20
+ };
21
+
22
+ forgotPasswordController.description = "Request password reset";
23
+
24
+ forgotPasswordController.validation = {
25
+ schema: v.object({
26
+ email: v.email().required(),
27
+ }),
28
+ };
@@ -0,0 +1,22 @@
1
+ import { type Request, type RequestHandler } from "@warlock.js/core";
2
+ import { type LoginSchema, loginSchema } from "../schema/login.schema";
3
+ import { loginService } from "../services/auth.service";
4
+
5
+ /**
6
+ * Login controller
7
+ * POST /auth/login
8
+ */
9
+ export const loginController: RequestHandler<Request<LoginSchema>> = async (request, response) => {
10
+ const result = await loginService(request.validated(), {
11
+ userAgent: request.userAgent,
12
+ ip: request.ip,
13
+ });
14
+
15
+ return response.success(result);
16
+ };
17
+
18
+ loginController.description = "User Login";
19
+
20
+ loginController.validation = {
21
+ schema: loginSchema,
22
+ };
@@ -0,0 +1,16 @@
1
+ import { t, type Request, type RequestHandler, type Response } from "@warlock.js/core";
2
+ import { logoutAllService } from "../services/auth.service";
3
+
4
+ /**
5
+ * Logout from all devices controller
6
+ * POST /auth/logout-all
7
+ */
8
+ export const logoutAllController: RequestHandler = async (request: Request, response: Response) => {
9
+ await logoutAllService(request.user);
10
+
11
+ return response.success({
12
+ message: t("auth.loggedOutAll"),
13
+ });
14
+ };
15
+
16
+ logoutAllController.description = "Logout from all devices";
@@ -0,0 +1,16 @@
1
+ import { t, type Request, type RequestHandler, type Response } from "@warlock.js/core";
2
+ import { logoutService } from "../services/auth.service";
3
+
4
+ /**
5
+ * Logout controller
6
+ * POST /auth/logout
7
+ */
8
+ export const logoutController: RequestHandler = async (request: Request, response: Response) => {
9
+ await logoutService(request.user);
10
+
11
+ return response.success({
12
+ message: t("auth.loggedOut"),
13
+ });
14
+ };
15
+
16
+ logoutController.description = "User Logout";
@@ -0,0 +1,13 @@
1
+ import { type Request, type RequestHandler, type Response } from "@warlock.js/core";
2
+
3
+ /**
4
+ * Get current user controller
5
+ * GET /auth/me
6
+ */
7
+ export const meController: RequestHandler = async (request: Request, response: Response) => {
8
+ return response.success({
9
+ user: request.user,
10
+ });
11
+ };
12
+
13
+ meController.description = "Get Current User";
@@ -0,0 +1,29 @@
1
+ import { type Request, type RequestHandler, type Response } from "@warlock.js/core";
2
+ import { v } from "@warlock.js/seal";
3
+ import { refreshTokensService } from "../services/auth.service";
4
+
5
+ /**
6
+ * Refresh token controller
7
+ * POST /auth/refresh-token
8
+ */
9
+ export const refreshTokenController: RequestHandler = async (
10
+ request: Request,
11
+ response: Response,
12
+ ) => {
13
+ const token = request.input("refreshToken");
14
+
15
+ const result = await refreshTokensService(token, {
16
+ userAgent: request.userAgent,
17
+ ip: request.ip,
18
+ });
19
+
20
+ return response.success(result);
21
+ };
22
+
23
+ refreshTokenController.description = "Refresh Access Token";
24
+
25
+ refreshTokenController.validation = {
26
+ schema: v.object({
27
+ refreshToken: v.string().required(),
28
+ }),
29
+ };
@@ -0,0 +1,23 @@
1
+ import { t, type Request, type RequestHandler } from "@warlock.js/core";
2
+ import { resetPasswordSchema, type ResetPasswordSchema } from "../schema/reset-password.schema";
3
+ import { resetPasswordService } from "../services/reset-password.service";
4
+
5
+ /**
6
+ * Reset password controller
7
+ */
8
+ export const resetPasswordController: RequestHandler<Request<ResetPasswordSchema>> = async (
9
+ request,
10
+ response,
11
+ ) => {
12
+ await resetPasswordService(request.validated());
13
+
14
+ return response.success({
15
+ message: t("auth.passwordResetSuccess"),
16
+ });
17
+ };
18
+
19
+ resetPasswordController.description = "Reset password with OTP";
20
+
21
+ resetPasswordController.validation = {
22
+ schema: resetPasswordSchema,
23
+ };
@@ -0,0 +1,9 @@
1
+ import { authService } from "@warlock.js/auth";
2
+ import { scheduler } from "app/shared/services/scheduler.service";
3
+ import { cleanupExpiredOtpsService } from "./services/otp.service";
4
+
5
+ // Cleanup expired OTPs every hour
6
+ scheduler.newJob("cleanup-expired-otps", cleanupExpiredOtpsService).everyHour();
7
+
8
+ // Cleanup expired refresh tokens every hour
9
+ scheduler.newJob("cleanup-expired-tokens", () => authService.cleanupExpiredTokens()).everyHour();
@@ -0,0 +1 @@
1
+ export * from "./otp.model";
@@ -0,0 +1,30 @@
1
+ import { bool, integer, json, Migration, string, timestamp } from "@warlock.js/cascade";
2
+ import { OTP } from "../otp.model";
3
+
4
+ export default Migration.create(
5
+ OTP,
6
+ {
7
+ code: string(20).index(),
8
+ type: string(50),
9
+ target: string(255),
10
+ channel: string(50),
11
+ userId: integer().index(),
12
+ userType: string(50),
13
+ expiresAt: timestamp().index(),
14
+ usedAt: timestamp().nullable(),
15
+ attempts: integer(),
16
+ maxAttempts: integer(),
17
+ metadata: json().nullable(),
18
+ isActive: bool(),
19
+ createdBy: json().nullable(),
20
+ updatedBy: json().nullable(),
21
+ deletedBy: json().nullable(),
22
+ },
23
+ {
24
+ index: [
25
+ {
26
+ columns: ["target", "type"],
27
+ },
28
+ ],
29
+ },
30
+ );
@@ -0,0 +1,69 @@
1
+ import { Model, RegisterModel } from "@warlock.js/cascade";
2
+ import { v, type Infer } from "@warlock.js/seal";
3
+
4
+ const otpSchema = v.object({
5
+ code: v.string().required(),
6
+ type: v.string().required(),
7
+ target: v.string().required(),
8
+ channel: v.string().required(),
9
+ userId: v.number().required(),
10
+ userType: v.string().required(),
11
+ expiresAt: v.date().required(),
12
+ usedAt: v.date().optional(),
13
+ attempts: v.number().default(0),
14
+ maxAttempts: v.number().default(5),
15
+ metadata: v.record(v.any()).optional(),
16
+ });
17
+
18
+ type OTPSchema = Infer<typeof otpSchema>;
19
+
20
+ @RegisterModel()
21
+ export class OTP extends Model<OTPSchema> {
22
+ /**
23
+ * Table name
24
+ */
25
+ public static table = "otps";
26
+
27
+ /**
28
+ * Model schema
29
+ */
30
+ public static schema = otpSchema;
31
+
32
+ /**
33
+ * Check if OTP is valid (not expired, not used, not max attempts)
34
+ */
35
+ public get isValid(): boolean {
36
+ if (this.get("usedAt")) return false;
37
+ if ((this.get("attempts") ?? 0) >= (this.get("maxAttempts") ?? 0)) return false;
38
+ if (new Date() > new Date(this.get("expiresAt"))) return false;
39
+ return true;
40
+ }
41
+
42
+ /**
43
+ * Check if OTP is expired
44
+ */
45
+ public get isExpired(): boolean {
46
+ return new Date() > new Date(this.get("expiresAt"));
47
+ }
48
+
49
+ /**
50
+ * Check if max attempts exceeded
51
+ */
52
+ public get isMaxAttemptsExceeded(): boolean {
53
+ return (this.get("attempts") ?? 0) >= (this.get("maxAttempts") ?? 0);
54
+ }
55
+
56
+ /**
57
+ * Mark OTP as used
58
+ */
59
+ public async markAsUsed(): Promise<this> {
60
+ return this.set("usedAt", new Date()).save();
61
+ }
62
+
63
+ /**
64
+ * Increment failed attempt
65
+ */
66
+ public async incrementAttempt(): Promise<this> {
67
+ return this.set("attempts", (this.get("attempts") ?? 0) + 1).save();
68
+ }
69
+ }
@@ -0,0 +1,10 @@
1
+ import type { Request, RequestHandler } from "@warlock.js/core";
2
+ import type { User } from "app/users/models/user";
3
+
4
+ export type GuardedRequest<RequestPayload = unknown> = Request<RequestPayload> & {
5
+ user: User;
6
+ };
7
+
8
+ export type GuardedRequestHandler<RequestPayload = unknown> = RequestHandler<
9
+ GuardedRequest<RequestPayload>
10
+ >;
@@ -0,0 +1,22 @@
1
+ import { router } from "@warlock.js/core";
2
+ import { guarded } from "app/shared/utils/router";
3
+ import { forgotPasswordController } from "./controllers/forgot-password.controller";
4
+ import { loginController } from "./controllers/login.controller";
5
+ import { logoutAllController } from "./controllers/logout-all.controller";
6
+ import { logoutController } from "./controllers/logout.controller";
7
+ import { meController } from "./controllers/me.controller";
8
+ import { refreshTokenController } from "./controllers/refresh-token.controller";
9
+ import { resetPasswordController } from "./controllers/reset-password.controller";
10
+
11
+ // Auth routes
12
+ router.prefix("/auth", () => {
13
+ router.post("/login", loginController);
14
+ router.post("/refresh-token", refreshTokenController);
15
+ router.post("/forgot-password", forgotPasswordController);
16
+ router.post("/reset-password", resetPasswordController);
17
+ guarded(() => {
18
+ router.post("/logout", logoutController);
19
+ router.post("/logout-all", logoutAllController);
20
+ router.get("/me", meController);
21
+ });
22
+ });
@@ -0,0 +1,8 @@
1
+ import { v, type Infer } from "@warlock.js/seal";
2
+
3
+ export const loginSchema = v.object({
4
+ email: v.email().required(),
5
+ password: v.string().required(),
6
+ });
7
+
8
+ export type LoginSchema = Infer<typeof loginSchema>;
@@ -0,0 +1,9 @@
1
+ import { v, type Infer } from "@warlock.js/seal";
2
+
3
+ export const resetPasswordSchema = v.object({
4
+ email: v.string().email().required(),
5
+ code: v.string().required(),
6
+ newPassword: v.string().min(8).required(),
7
+ });
8
+
9
+ export type ResetPasswordSchema = Infer<typeof resetPasswordSchema>;
@@ -0,0 +1,66 @@
1
+ import type { AccessTokenOutput, Auth } from "@warlock.js/auth";
2
+ import { authService, type DeviceInfo, type TokenPair } from "@warlock.js/auth";
3
+ import { t, UnAuthorizedError } from "@warlock.js/core";
4
+ import { User } from "app/users/models/user/user.model";
5
+
6
+ export type LoginCredentials = {
7
+ email: string;
8
+ password: string;
9
+ };
10
+
11
+ export type LoginResult =
12
+ | {
13
+ user: User;
14
+ tokens: TokenPair;
15
+ }
16
+ | null
17
+ | {
18
+ user: User;
19
+ accessToken: AccessTokenOutput;
20
+ };
21
+
22
+ /**
23
+ * Login with email and password
24
+ */
25
+ export async function loginService(
26
+ credentials: LoginCredentials,
27
+ deviceInfo?: DeviceInfo,
28
+ ): Promise<LoginResult> {
29
+ const result = await authService.login(User, credentials, deviceInfo);
30
+
31
+ if (!result) {
32
+ throw new UnAuthorizedError(t("auth.invalidCredentials"));
33
+ }
34
+
35
+ return result;
36
+ }
37
+
38
+ /**
39
+ * Logout user
40
+ */
41
+ export async function logoutService(user: Auth): Promise<void> {
42
+ return authService.logout(user);
43
+ }
44
+
45
+ /**
46
+ * Logout from all devices
47
+ */
48
+ export async function logoutAllService(user: Auth): Promise<void> {
49
+ return authService.revokeAllTokens(user);
50
+ }
51
+
52
+ /**
53
+ * Refresh tokens
54
+ */
55
+ export async function refreshTokensService(
56
+ refreshToken: string,
57
+ deviceInfo?: DeviceInfo,
58
+ ): Promise<TokenPair> {
59
+ const result = await authService.refreshTokens(refreshToken, deviceInfo);
60
+
61
+ if (!result) {
62
+ throw new UnAuthorizedError(t("auth.invalidRefreshToken"));
63
+ }
64
+
65
+ return result;
66
+ }
@@ -0,0 +1,28 @@
1
+ import { getFirstUserService } from "app/users/services/list-users.service";
2
+ import { createOtpService } from "./otp.service";
3
+
4
+ /**
5
+ * Example Usage:
6
+ * await forgotPasswordService("user@example.com");
7
+ */
8
+
9
+ /**
10
+ * Handle forgot password request
11
+ *
12
+ * @param email - User email address
13
+ * @returns Promise<void>
14
+ */
15
+ export async function forgotPasswordService(email: string): Promise<void> {
16
+ // Find user by email (silent fail for security)
17
+ const user = await getFirstUserService({ email });
18
+
19
+ // Create a password-reset OTP. Wire a mail service to deliver its code —
20
+ // e.g. `sendPasswordResetEmail(user, otp.get("code"))`.
21
+ await createOtpService({
22
+ target: email,
23
+ channel: "email",
24
+ type: "password-reset",
25
+ userId: user.id,
26
+ userType: user.userType,
27
+ });
28
+ }
@@ -0,0 +1,173 @@
1
+ import { ForbiddenError, t } from "@warlock.js/core";
2
+ import { OTP } from "../models/otp";
3
+ import { AuthErrorCode } from "../utils/auth-error-code";
4
+ import type { OTPChannel, OTPType } from "../utils/types";
5
+
6
+ /** Human-friendly duration parts, summed to milliseconds by `parseDurationToMs`. */
7
+ type Duration = {
8
+ milliseconds?: number;
9
+ seconds?: number;
10
+ minutes?: number;
11
+ hours?: number;
12
+ days?: number;
13
+ weeks?: number;
14
+ };
15
+
16
+ // Default OTP expiration
17
+ const DEFAULT_OTP_EXPIRATION: Duration = { minutes: 15 };
18
+
19
+ // Parse duration to milliseconds
20
+ function parseDurationToMs(duration: Duration): number {
21
+ let ms = 0;
22
+ if (duration.milliseconds) ms += duration.milliseconds;
23
+ if (duration.seconds) ms += duration.seconds * 1000;
24
+ if (duration.minutes) ms += duration.minutes * 60 * 1000;
25
+ if (duration.hours) ms += duration.hours * 60 * 60 * 1000;
26
+ if (duration.days) ms += duration.days * 24 * 60 * 60 * 1000;
27
+ if (duration.weeks) ms += duration.weeks * 7 * 24 * 60 * 60 * 1000;
28
+ return ms;
29
+ }
30
+
31
+ export type CreateOTPOptions = {
32
+ target: string;
33
+ channel: OTPChannel;
34
+ type: OTPType;
35
+ userId?: number | string;
36
+ userType?: string;
37
+ expiresIn?: Duration;
38
+ length?: number;
39
+ alphanumeric?: boolean;
40
+ maxAttempts?: number;
41
+ metadata?: object;
42
+ };
43
+
44
+ /**
45
+ * Generate OTP code
46
+ */
47
+ function generateCode(length: number = 6, alphanumeric: boolean = false): string {
48
+ if (alphanumeric) {
49
+ // Generate alphanumeric code
50
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
51
+ let code = "";
52
+ for (let i = 0; i < length; i++) {
53
+ code += chars.charAt(Math.floor(Math.random() * chars.length));
54
+ }
55
+ return code;
56
+ }
57
+ // Generate numeric code
58
+ const min = Math.pow(10, length - 1);
59
+ const max = Math.pow(10, length) - 1;
60
+ return Math.floor(min + Math.random() * (max - min + 1)).toString();
61
+ }
62
+
63
+ /**
64
+ * Create a new OTP
65
+ */
66
+ export async function createOtpService(options: CreateOTPOptions): Promise<OTP> {
67
+ const {
68
+ target,
69
+ channel,
70
+ type,
71
+ userId,
72
+ userType,
73
+ expiresIn = DEFAULT_OTP_EXPIRATION,
74
+ length = 6,
75
+ alphanumeric = false,
76
+ maxAttempts = 5,
77
+ metadata,
78
+ } = options;
79
+
80
+ // Invalidate any existing unused OTPs for this target+type
81
+ await cleanupOtpService(target, type);
82
+
83
+ const code = generateCode(length, alphanumeric);
84
+ const expiresAt = new Date(Date.now() + parseDurationToMs(expiresIn));
85
+
86
+ return OTP.create({
87
+ code,
88
+ type,
89
+ target,
90
+ channel,
91
+ userId,
92
+ userType,
93
+ expiresAt,
94
+ maxAttempts,
95
+ metadata,
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Verify an OTP
101
+ */
102
+ export async function verifyOtpService(
103
+ code: string,
104
+ target: string,
105
+ type: OTPType,
106
+ ): Promise<OTP | never> {
107
+ const otp = await OTP.first({
108
+ code,
109
+ target,
110
+ type,
111
+ usedAt: null,
112
+ });
113
+
114
+ if (!otp) {
115
+ throw new ForbiddenError(t("auth.missingOtp"), {
116
+ errorCode: AuthErrorCode.OTP_NOT_FOUND,
117
+ });
118
+ }
119
+
120
+ if (otp.isExpired) {
121
+ throw new ForbiddenError(t("auth.otpExpired"), {
122
+ errorCode: AuthErrorCode.OTP_EXPIRED,
123
+ });
124
+ }
125
+
126
+ if (otp.isMaxAttemptsExceeded) {
127
+ throw new ForbiddenError(t("auth.otpMaxAttempts"), {
128
+ errorCode: AuthErrorCode.OTP_MAX_ATTEMPTS,
129
+ });
130
+ }
131
+
132
+ // Mark as used
133
+ await otp.markAsUsed();
134
+
135
+ return otp;
136
+ }
137
+
138
+ export async function cleanupOtpService(target: string, type: OTPType): Promise<void> {
139
+ await OTP.delete({
140
+ target,
141
+ type,
142
+ usedAt: null,
143
+ });
144
+ }
145
+
146
+ /**
147
+ * Resend OTP (invalidate old and create new)
148
+ */
149
+ export async function resendOtpService(
150
+ target: string,
151
+ type: OTPType,
152
+ channel: OTPChannel,
153
+ options?: Partial<CreateOTPOptions>,
154
+ ): Promise<OTP> {
155
+ await cleanupOtpService(target, type);
156
+ return createOtpService({
157
+ target,
158
+ type,
159
+ channel,
160
+ ...options,
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Cleanup expired OTPs
166
+ */
167
+ export async function cleanupExpiredOtpsService(): Promise<number> {
168
+ const expiredOtps = await OTP.query().where("expiresAt", "<", new Date()).get();
169
+
170
+ await Promise.all(expiredOtps.map((otp) => otp.destroy()));
171
+
172
+ return expiredOtps.length;
173
+ }
@@ -0,0 +1,39 @@
1
+ import { authService } from "@warlock.js/auth";
2
+ import { BadRequestError, t } from "@warlock.js/core";
3
+ import { User } from "app/users/models/user";
4
+ import { AuthErrorCode } from "../utils/auth-error-code";
5
+ import { verifyOtpService } from "./otp.service";
6
+
7
+ type ResetPasswordOptions = {
8
+ email: string;
9
+ code: string;
10
+ newPassword: string;
11
+ };
12
+
13
+ /**
14
+ * Reset user password using OTP verification
15
+ */
16
+ export async function resetPasswordService(options: ResetPasswordOptions): Promise<User> {
17
+ const { email, code, newPassword } = options;
18
+
19
+ // Verify OTP
20
+ const otp = await verifyOtpService(code, email, "password-reset");
21
+
22
+ // Find user
23
+ const user = await User.find(otp.get("userId"));
24
+
25
+ if (!user) {
26
+ throw new BadRequestError(t("auth.otpInvalid"), {
27
+ errorCode: AuthErrorCode.OTP_INVALID,
28
+ });
29
+ }
30
+
31
+ // Update password
32
+ await user.set("password", newPassword).save();
33
+
34
+ // Revoke all tokens (force re-login)
35
+ // Or make it an option through user decision.
36
+ await authService.revokeAllTokens(user);
37
+
38
+ return user;
39
+ }
@@ -0,0 +1,6 @@
1
+ export enum AuthErrorCode {
2
+ OTP_NOT_FOUND = "OTX001", // OTP Not Found
3
+ OTP_EXPIRED = "OTX002", // OTP Expired
4
+ OTP_MAX_ATTEMPTS = "OTX003", // OTP Max Attempts
5
+ OTP_INVALID = "OTX004", // OTP Invalid
6
+ }