create-nexwire 4.0.0 → 4.1.1

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 (173) hide show
  1. package/README.md +5 -4
  2. package/express/README.md +13 -7
  3. package/express/package.json +2 -2
  4. package/express/src/config/session.ts +1 -1
  5. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  6. package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
  7. package/express/src/database/schema.ts +3 -3
  8. package/express/src/framework/cache/cache.ts +82 -81
  9. package/express/src/framework/database/seed.ts +1 -1
  10. package/express/src/framework/facade.ts +1 -24
  11. package/express/src/framework/http/app.ts +5 -5
  12. package/express/src/framework/http/openapi.ts +1 -1
  13. package/express/src/framework/http/router.ts +3 -18
  14. package/express/src/framework/http/types.ts +19 -0
  15. package/express/src/framework/http/validation.ts +18 -18
  16. package/express/src/framework/maker-cli/middleware/core.mjs +41 -47
  17. package/express/src/framework/maker-cli/module/core.mjs +920 -684
  18. package/express/src/framework/maker-cli/module/index.mjs +202 -140
  19. package/express/src/framework/maker-cli/runtime/core.mjs +1 -2
  20. package/express/src/framework/maker-cli/stubs/controller/openapi.ts.stub +40 -14
  21. package/express/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +40 -15
  22. package/express/src/framework/maker-cli/stubs/controller/plain.ts.stub +42 -16
  23. package/express/src/framework/maker-cli/stubs/controller/standalone.ts.stub +47 -0
  24. package/express/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
  25. package/express/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
  26. package/express/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
  27. package/express/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +9 -8
  28. package/express/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +9 -8
  29. package/express/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +9 -8
  30. package/express/src/framework/maker-cli/stubs/notification/route.api.ts.stub +50 -50
  31. package/express/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +11 -11
  32. package/express/src/framework/maker-cli/stubs/route/api.ts.stub +93 -79
  33. package/express/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -10
  34. package/express/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +113 -0
  35. package/express/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +37 -0
  36. package/express/src/framework/maker-cli/stubs/{controller/schema.plain.ts.stub → schema/name.plain.ts.stub} +13 -13
  37. package/express/src/framework/maker-cli/stubs/{controller/schema.ts.stub → schema/name.ts.stub} +32 -32
  38. package/express/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
  39. package/express/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
  40. package/express/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
  41. package/express/src/framework/maker-cli/utils/naming.mjs +56 -13
  42. package/express/src/framework/modules/discover.ts +57 -54
  43. package/express/src/framework/notification/index.ts +1 -1
  44. package/express/src/framework/realtime/broadcast.ts +46 -46
  45. package/express/src/framework/server.ts +2 -1
  46. package/express/src/framework/session/session.ts +102 -98
  47. package/express/src/framework/support/mail.ts +48 -48
  48. package/express/src/framework/support/status-codes.ts +75 -0
  49. package/express/src/middlewares/auth-middleware.ts +1 -1
  50. package/express/src/modules/auth/__tests__/forgetpass.test.ts +200 -0
  51. package/express/src/modules/auth/__tests__/login.test.ts +171 -0
  52. package/express/src/modules/auth/__tests__/logout.test.ts +60 -0
  53. package/express/src/modules/auth/__tests__/logoutalldevices.test.ts +49 -0
  54. package/express/src/modules/auth/__tests__/me.test.ts +91 -0
  55. package/express/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
  56. package/express/src/modules/auth/__tests__/register.test.ts +291 -0
  57. package/express/src/modules/auth/__tests__/resetpass.test.ts +206 -0
  58. package/express/src/modules/auth/__tests__/verifyemail.test.ts +183 -0
  59. package/express/src/modules/auth/controllers/auth.ts +254 -0
  60. package/express/src/modules/auth/controllers/{role.controller.ts → role.ts} +25 -15
  61. package/{hono/src/modules/auth/database/models/notifications.ts → express/src/modules/auth/database/models/notification.ts} +1 -1
  62. package/express/src/modules/auth/database/models/role.ts +1 -1
  63. package/express/src/modules/auth/database/models/user.ts +1 -1
  64. package/express/src/modules/auth/facade.ts +12 -0
  65. package/express/src/modules/auth/{controllers/auth.helpers.ts → helpers/auth.ts} +3 -4
  66. package/express/src/modules/auth/jobs/registeruser.ts +23 -23
  67. package/express/src/modules/auth/routes/{api.ts → index.ts} +150 -150
  68. package/express/src/modules/auth/routes/role.ts +33 -33
  69. package/{hono/src/modules/auth/controllers/auth.schema.ts → express/src/modules/auth/schemas/auth.ts} +102 -102
  70. package/express/src/modules/auth/services/forgetpassword.ts +66 -0
  71. package/express/src/modules/auth/services/login.ts +63 -0
  72. package/express/src/modules/auth/services/logout.ts +25 -0
  73. package/express/src/modules/auth/services/logoutalldevices.ts +19 -0
  74. package/express/src/modules/auth/services/me.ts +31 -0
  75. package/express/src/modules/auth/services/refreshtoken.ts +110 -0
  76. package/express/src/modules/auth/services/register.ts +141 -0
  77. package/express/src/modules/auth/services/resetpassword.ts +69 -0
  78. package/express/src/modules/auth/services/verifyemail.ts +77 -0
  79. package/express/src/modules/auth/types/auth.ts +31 -0
  80. package/express/src/resources/src/assets/images/logo.png +0 -0
  81. package/express/src/resources/src/pages/auth/forgetPassword.vue +1 -0
  82. package/express/src/resources/src/pages/auth/login.vue +1 -1
  83. package/express/src/storage/database/nexwire.sqlite +0 -0
  84. package/hono/README.md +13 -7
  85. package/hono/package.json +1 -1
  86. package/hono/src/config/session.ts +1 -1
  87. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  88. package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
  89. package/hono/src/database/schema.ts +3 -3
  90. package/hono/src/framework/cache/cache.ts +82 -81
  91. package/hono/src/framework/database/seed.ts +1 -1
  92. package/hono/src/framework/http/app.ts +5 -5
  93. package/hono/src/framework/http/validation.ts +21 -21
  94. package/hono/src/framework/maker-cli/middleware/core.mjs +40 -46
  95. package/hono/src/framework/maker-cli/module/core.mjs +401 -168
  96. package/hono/src/framework/maker-cli/module/index.mjs +73 -11
  97. package/hono/src/framework/maker-cli/stubs/controller/openapi.ts.stub +28 -16
  98. package/hono/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +28 -17
  99. package/hono/src/framework/maker-cli/stubs/controller/plain.ts.stub +28 -16
  100. package/hono/src/framework/maker-cli/stubs/controller/standalone.ts.stub +56 -0
  101. package/hono/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
  102. package/hono/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
  103. package/hono/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
  104. package/hono/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +9 -8
  105. package/hono/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +9 -8
  106. package/hono/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +9 -8
  107. package/hono/src/framework/maker-cli/stubs/notification/route.api.ts.stub +49 -49
  108. package/hono/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +11 -11
  109. package/hono/src/framework/maker-cli/stubs/route/api.ts.stub +93 -79
  110. package/hono/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -10
  111. package/hono/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +112 -0
  112. package/hono/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +37 -0
  113. package/hono/src/framework/maker-cli/stubs/{controller/schema.plain.ts.stub → schema/name.plain.ts.stub} +13 -13
  114. package/hono/src/framework/maker-cli/stubs/{controller/schema.ts.stub → schema/name.ts.stub} +32 -32
  115. package/hono/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
  116. package/hono/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
  117. package/hono/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
  118. package/hono/src/framework/maker-cli/utils/naming.mjs +56 -13
  119. package/hono/src/framework/modules/discover.ts +57 -54
  120. package/hono/src/framework/notification/index.ts +1 -1
  121. package/hono/src/framework/realtime/broadcast.ts +46 -46
  122. package/hono/src/framework/server.ts +1 -1
  123. package/hono/src/framework/session/session.ts +121 -116
  124. package/hono/src/middlewares/auth-middleware.ts +1 -1
  125. package/hono/src/modules/auth/__tests__/forgetpass.test.ts +170 -0
  126. package/hono/src/modules/auth/__tests__/login.test.ts +154 -0
  127. package/hono/src/modules/auth/__tests__/logout.test.ts +53 -0
  128. package/hono/src/modules/auth/__tests__/logoutalldevices.test.ts +34 -0
  129. package/hono/src/modules/auth/__tests__/me.test.ts +87 -0
  130. package/hono/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
  131. package/hono/src/modules/auth/__tests__/register.test.ts +258 -0
  132. package/hono/src/modules/auth/__tests__/resetpass.test.ts +214 -0
  133. package/hono/src/modules/auth/__tests__/verifyemail.test.ts +189 -0
  134. package/hono/src/modules/auth/controllers/auth.ts +238 -0
  135. package/hono/src/modules/auth/controllers/{role.controller.ts → role.ts} +25 -25
  136. package/{express/src/modules/auth/database/models/notifications.ts → hono/src/modules/auth/database/models/notification.ts} +1 -1
  137. package/hono/src/modules/auth/database/models/role.ts +1 -1
  138. package/hono/src/modules/auth/database/models/user.ts +1 -1
  139. package/hono/src/modules/auth/facade.ts +12 -0
  140. package/hono/src/modules/auth/{controllers/auth.helpers.ts → helpers/auth.ts} +5 -6
  141. package/hono/src/modules/auth/jobs/registeruser.ts +23 -23
  142. package/hono/src/modules/auth/routes/{api.ts → index.ts} +150 -150
  143. package/hono/src/modules/auth/routes/role.ts +33 -33
  144. package/{express/src/modules/auth/controllers/auth.schema.ts → hono/src/modules/auth/schemas/auth.ts} +102 -102
  145. package/hono/src/modules/auth/services/forgetpassword.ts +58 -0
  146. package/hono/src/modules/auth/services/login.ts +61 -0
  147. package/hono/src/modules/auth/services/logout.ts +24 -0
  148. package/hono/src/modules/auth/services/logoutalldevices.ts +19 -0
  149. package/hono/src/modules/auth/services/me.ts +31 -0
  150. package/hono/src/modules/auth/services/refreshtoken.ts +110 -0
  151. package/hono/src/modules/auth/services/register.ts +132 -0
  152. package/hono/src/modules/auth/services/resetpassword.ts +69 -0
  153. package/hono/src/modules/auth/services/verifyemail.ts +77 -0
  154. package/hono/src/modules/auth/types/auth.ts +31 -0
  155. package/hono/src/resources/src/assets/images/logo.png +0 -0
  156. package/hono/src/storage/database/nexwire.sqlite +0 -0
  157. package/package.json +1 -1
  158. package/express/src/framework/maker-cli/stubs/example/console.ts.stub +0 -34
  159. package/express/src/framework/maker-cli/stubs/example/controller.ts.stub +0 -418
  160. package/express/src/framework/maker-cli/stubs/example/job.ts.stub +0 -75
  161. package/express/src/framework/maker-cli/stubs/example/route.api.ts.stub +0 -207
  162. package/express/src/framework/maker-cli/stubs/example/route.plain.ts.stub +0 -18
  163. package/express/src/framework/maker-cli/stubs/example/schema.plain.ts.stub +0 -42
  164. package/express/src/framework/maker-cli/stubs/example/schema.ts.stub +0 -42
  165. package/express/src/modules/auth/controllers/auth.controller.ts +0 -358
  166. package/hono/src/framework/maker-cli/stubs/example/console.ts.stub +0 -34
  167. package/hono/src/framework/maker-cli/stubs/example/controller.ts.stub +0 -505
  168. package/hono/src/framework/maker-cli/stubs/example/job.ts.stub +0 -75
  169. package/hono/src/framework/maker-cli/stubs/example/route.api.ts.stub +0 -207
  170. package/hono/src/framework/maker-cli/stubs/example/route.plain.ts.stub +0 -18
  171. package/hono/src/framework/maker-cli/stubs/example/schema.plain.ts.stub +0 -42
  172. package/hono/src/framework/maker-cli/stubs/example/schema.ts.stub +0 -42
  173. package/hono/src/modules/auth/controllers/auth.controller.ts +0 -413
@@ -1,102 +1,102 @@
1
- import { z } from "@/framework/facade.js";
2
-
3
- export const MessageSchema = z.object({
4
- message: z.string()
5
- });
6
-
7
- export const RoleSchema = z.object({
8
- id: z.number(),
9
- name: z.string(),
10
- createdAt: z.string().nullable().optional(),
11
- updatedAt: z.string().nullable().optional()
12
- });
13
-
14
- export const UserSchema = z.object({
15
- id: z.number(),
16
- name: z.string(),
17
- email: z.email(),
18
- roleId: z.number().nullable().optional(),
19
- role: RoleSchema.nullable().optional(),
20
- emailVerifiedAt: z.string().nullable().optional(),
21
- createdAt: z.string().nullable().optional(),
22
- updatedAt: z.string().nullable().optional()
23
- });
24
-
25
- export const RegisterSchema = z
26
- .object({
27
- name: z.string().min(2).max(100),
28
- email: z.email(),
29
- password: z
30
- .string()
31
- .min(6)
32
- .max(100)
33
- .regex(/[a-z]/, "Password must contain lowercase letter")
34
- .regex(/[A-Z]/, "Password must contain uppercase letter")
35
- .regex(/[^A-Za-z0-9]/, "Password must contain special character"),
36
- password_confirmation: z.string()
37
- })
38
- .superRefine((data, ctx) => {
39
- if (data.password !== data.password_confirmation) {
40
- ctx.addIssue({
41
- code: "custom",
42
- path: ["password_confirmation"],
43
- message: "Password confirmation does not match"
44
- });
45
- }
46
- });
47
-
48
- export const LoginSchema = z.object({
49
- email: z.email(),
50
- password: z.string().min(1),
51
- remember: z.boolean().optional().default(false)
52
- });
53
-
54
- export const ForgotPasswordSchema = z.object({
55
- email: z.email()
56
- });
57
-
58
- export const ResetPasswordSchema = z
59
- .object({
60
- email: z.email(),
61
- token: z.string().min(1),
62
- password: z
63
- .string()
64
- .min(6)
65
- .max(100)
66
- .regex(/[a-z]/, "Password must contain lowercase letter")
67
- .regex(/[A-Z]/, "Password must contain uppercase letter")
68
- .regex(/[^A-Za-z0-9]/, "Password must contain special character"),
69
- password_confirmation: z.string()
70
- })
71
- .superRefine((data, ctx) => {
72
- if (data.password !== data.password_confirmation) {
73
- ctx.addIssue({
74
- code: "custom",
75
- path: ["password_confirmation"],
76
- message: "Password confirmation does not match"
77
- });
78
- }
79
- });
80
-
81
- export const RefreshTokenSchema = z.object({
82
- refresh_token: z.string().min(1)
83
- });
84
-
85
- export const VerifyEmailSchema = z.object({
86
- email: z.email(),
87
- token: z.string().min(1)
88
- });
89
-
90
- export const AuthResponseSchema = z.object({
91
- message: z.string(),
92
- data: z.object({
93
- user: UserSchema,
94
- access_token: z.string(),
95
- refresh_token: z.string(),
96
- token_type: z.literal("Bearer")
97
- })
98
- });
99
-
100
- export const IdParamsSchema = z.object({
101
- id: z.coerce.number()
102
- });
1
+ import { z } from "@/framework/facade.js";
2
+
3
+ export const MessageSchema = z.object({
4
+ message: z.string()
5
+ });
6
+
7
+ export const RoleSchema = z.object({
8
+ id: z.number(),
9
+ name: z.string(),
10
+ createdAt: z.string().nullable().optional(),
11
+ updatedAt: z.string().nullable().optional()
12
+ });
13
+
14
+ export const UserSchema = z.object({
15
+ id: z.number(),
16
+ name: z.string(),
17
+ email: z.email(),
18
+ roleId: z.number().nullable().optional(),
19
+ role: RoleSchema.nullable().optional(),
20
+ emailVerifiedAt: z.string().nullable().optional(),
21
+ createdAt: z.string().nullable().optional(),
22
+ updatedAt: z.string().nullable().optional()
23
+ });
24
+
25
+ export const RegisterSchema = z
26
+ .object({
27
+ name: z.string().min(2).max(100),
28
+ email: z.email(),
29
+ password: z
30
+ .string()
31
+ .min(6)
32
+ .max(100)
33
+ .regex(/[a-z]/, "Password must contain lowercase letter")
34
+ .regex(/[A-Z]/, "Password must contain uppercase letter")
35
+ .regex(/[^A-Za-z0-9]/, "Password must contain special character"),
36
+ password_confirmation: z.string()
37
+ })
38
+ .superRefine((data, ctx) => {
39
+ if (data.password !== data.password_confirmation) {
40
+ ctx.addIssue({
41
+ code: "custom",
42
+ path: ["password_confirmation"],
43
+ message: "Password confirmation does not match"
44
+ });
45
+ }
46
+ });
47
+
48
+ export const LoginSchema = z.object({
49
+ email: z.email(),
50
+ password: z.string().min(1),
51
+ remember: z.boolean().optional().default(false)
52
+ });
53
+
54
+ export const ForgotPasswordSchema = z.object({
55
+ email: z.email()
56
+ });
57
+
58
+ export const ResetPasswordSchema = z
59
+ .object({
60
+ email: z.email(),
61
+ token: z.string().min(1),
62
+ password: z
63
+ .string()
64
+ .min(6)
65
+ .max(100)
66
+ .regex(/[a-z]/, "Password must contain lowercase letter")
67
+ .regex(/[A-Z]/, "Password must contain uppercase letter")
68
+ .regex(/[^A-Za-z0-9]/, "Password must contain special character"),
69
+ password_confirmation: z.string()
70
+ })
71
+ .superRefine((data, ctx) => {
72
+ if (data.password !== data.password_confirmation) {
73
+ ctx.addIssue({
74
+ code: "custom",
75
+ path: ["password_confirmation"],
76
+ message: "Password confirmation does not match"
77
+ });
78
+ }
79
+ });
80
+
81
+ export const RefreshTokenSchema = z.object({
82
+ refresh_token: z.string().min(1)
83
+ });
84
+
85
+ export const VerifyEmailSchema = z.object({
86
+ email: z.email(),
87
+ token: z.string().min(1)
88
+ });
89
+
90
+ export const AuthResponseSchema = z.object({
91
+ message: z.string(),
92
+ data: z.object({
93
+ user: UserSchema,
94
+ access_token: z.string(),
95
+ refresh_token: z.string(),
96
+ token_type: z.literal("Bearer")
97
+ })
98
+ });
99
+
100
+ export const IdParamsSchema = z.object({
101
+ id: z.coerce.number()
102
+ });
@@ -0,0 +1,66 @@
1
+ import { and, eq, lt } from "drizzle-orm";
2
+ import { db, dispatchEvent, urls } from "@/framework/facade.js";
3
+ import { passwordResetTokens, users } from "@/modules/auth/database/models/user.js";
4
+ import { hashResetToken, makeResetToken } from "@/modules/auth/helpers/auth.js";
5
+ import type { ForgotPasswordInput } from "@/modules/auth/types/auth.js";
6
+
7
+ export type ForgotPasswordResult = {
8
+ kind: "sent";
9
+ message: string;
10
+ };
11
+
12
+ const TOKEN_TTL_MS = 15 * 60 * 1000;
13
+
14
+ export const forgotPasswordService = {
15
+ forgotPassword: async (body: ForgotPasswordInput): Promise<ForgotPasswordResult> => {
16
+ const now = new Date();
17
+
18
+ // Cleanup + replace happen atomically so a crash mid-flow can't leave
19
+ // the user without a valid token.
20
+ const issued = await db.transaction(async (tx) => {
21
+ // 1. Drop this email's expired tokens (housekeeping, not critical)
22
+ await tx.delete(passwordResetTokens).where(and(eq(passwordResetTokens.email, body.email), lt(passwordResetTokens.expiresAt, now)));
23
+
24
+ const user = await tx.query.users.findFirst({
25
+ where: eq(users.email, body.email)
26
+ });
27
+
28
+ if (!user) return null;
29
+
30
+ // 2. Revoke any existing unexpired token for this email
31
+ await tx.delete(passwordResetTokens).where(eq(passwordResetTokens.email, body.email));
32
+
33
+ // 3. Issue a fresh one
34
+ const plainToken = makeResetToken();
35
+ const expiresAt = new Date(now.getTime() + TOKEN_TTL_MS);
36
+
37
+ await tx.insert(passwordResetTokens).values({
38
+ email: user.email,
39
+ token: hashResetToken(plainToken),
40
+ expiresAt,
41
+ createdAt: now
42
+ });
43
+
44
+ return { user, plainToken };
45
+ });
46
+
47
+ // Same response whether or not the user exists (no enumeration).
48
+ if (!issued) {
49
+ return { kind: "sent", message: "Reset link has been sent" };
50
+ }
51
+
52
+ const resetUrl = urls.url(`/reset-password?token=${issued.plainToken}&email=${encodeURIComponent(issued.user.email)}`);
53
+
54
+ await dispatchEvent(
55
+ "user:forget-password",
56
+ {
57
+ email: issued.user.email,
58
+ name: issued.user.name,
59
+ resetUrl
60
+ },
61
+ { queue: "mail" }
62
+ );
63
+
64
+ return { kind: "sent", message: "Reset link has been sent" };
65
+ }
66
+ };
@@ -0,0 +1,63 @@
1
+ import { eq } from "drizzle-orm";
2
+ import type { Request, Response } from "express";
3
+ import { authConfig } from "@/config/index.js";
4
+ import { db, password } from "@/framework/facade.js";
5
+ import { users } from "@/modules/auth/database/models/user.js";
6
+ import { issueTokens, revokeCurrentRefreshToken, sanitizeUser } from "@/modules/auth/helpers/auth.js";
7
+ import type { LoginInput } from "@/modules/auth/types/auth.js";
8
+
9
+ export type LoginResult =
10
+ | { kind: "invalid_credentials"; message: string }
11
+ | { kind: "email_not_verified"; message: string }
12
+ | {
13
+ kind: "logged_in";
14
+ message: string;
15
+ data: {
16
+ user: ReturnType<typeof sanitizeUser>;
17
+ access_token: string;
18
+ refresh_token: string;
19
+ token_type: "Bearer";
20
+ };
21
+ };
22
+
23
+ export const loginService = {
24
+ login: async (req: Request, res: Response, body: LoginInput): Promise<LoginResult> => {
25
+ const user = await db.query.users.findFirst({
26
+ where: eq(users.email, body.email),
27
+ with: { role: true }
28
+ });
29
+
30
+ // Generic message for both "user missing" and "wrong password" —
31
+ // never tell an attacker which one it was.
32
+ const passwordOk = user && (await password.verifyPassword(body.password, user.password));
33
+
34
+ if (!user || !passwordOk) {
35
+ return { kind: "invalid_credentials", message: "Invalid credentials" };
36
+ }
37
+
38
+ if (authConfig.requireEmailVerification && !user.emailVerifiedAt) {
39
+ return {
40
+ kind: "email_not_verified",
41
+ message: "Please verify your email before logging in"
42
+ };
43
+ }
44
+
45
+ // Rotate the current refresh token before issuing a new pair.
46
+ await revokeCurrentRefreshToken(req, res);
47
+
48
+ const tokens = await issueTokens(req, res, user, {
49
+ remember: !!body.remember
50
+ });
51
+
52
+ return {
53
+ kind: "logged_in",
54
+ message: "User logged in successfully",
55
+ data: {
56
+ user: sanitizeUser(user),
57
+ access_token: tokens.accessToken,
58
+ refresh_token: tokens.refreshToken,
59
+ token_type: "Bearer"
60
+ }
61
+ };
62
+ }
63
+ };
@@ -0,0 +1,25 @@
1
+ import type { Request, Response } from "express";
2
+ import { cookie, logger } from "@/framework/facade.js";
3
+ import { revokeCurrentRefreshToken } from "@/modules/auth/helpers/auth.js";
4
+
5
+ export type LogoutResult = {
6
+ kind: "logged_out";
7
+ message: string;
8
+ };
9
+
10
+ export const logoutService = {
11
+ logout: async (req: Request, res: Response): Promise<LogoutResult> => {
12
+ try {
13
+ await revokeCurrentRefreshToken(req, res);
14
+ } catch (err) {
15
+ // Log and continue — the user is logging out either way.
16
+ // The server-side token will expire on its own.
17
+ logger.error("failed to revoke refresh token on logout", { err });
18
+ }
19
+
20
+ cookie.deleteAuth(res);
21
+ cookie.deleteRefresh(res);
22
+
23
+ return { kind: "logged_out", message: "Logged out successfully" };
24
+ }
25
+ };
@@ -0,0 +1,19 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { db } from "@/framework/facade.js";
3
+ import { refreshTokens } from "@/modules/auth/database/models/user.js";
4
+
5
+ export type LogoutAllDevicesResult = {
6
+ kind: "logged_out";
7
+ message: string;
8
+ };
9
+
10
+ export const logoutAllDevicesService = {
11
+ logoutAllDevices: async (userId: number): Promise<LogoutAllDevicesResult> => {
12
+ await db.delete(refreshTokens).where(eq(refreshTokens.userId, userId));
13
+
14
+ return {
15
+ kind: "logged_out",
16
+ message: "Logged out from all devices successfully"
17
+ };
18
+ }
19
+ };
@@ -0,0 +1,31 @@
1
+ import { eq } from "drizzle-orm";
2
+ import { db } from "@/framework/facade.js";
3
+ import { users } from "@/modules/auth/database/models/user.js";
4
+ import { sanitizeUser } from "@/modules/auth/helpers/auth.js";
5
+
6
+ export type MeResult =
7
+ | { kind: "not_found"; message: string }
8
+ | {
9
+ kind: "found";
10
+ message: string;
11
+ data: ReturnType<typeof sanitizeUser>;
12
+ };
13
+
14
+ export const meService = {
15
+ me: async (userId: number): Promise<MeResult> => {
16
+ const user = await db.query.users.findFirst({
17
+ where: eq(users.id, userId),
18
+ with: { role: true }
19
+ });
20
+
21
+ if (!user) {
22
+ return { kind: "not_found", message: "User not found" };
23
+ }
24
+
25
+ return {
26
+ kind: "found",
27
+ message: "Authenticated user fetched successfully",
28
+ data: sanitizeUser(user)
29
+ };
30
+ }
31
+ };
@@ -0,0 +1,110 @@
1
+ import { and, eq } from "drizzle-orm";
2
+ import { jwtConfig } from "@/config/index.js";
3
+ import { db, jwt } from "@/framework/facade.js";
4
+ import { refreshTokens, users } from "@/modules/auth/database/models/user.js";
5
+ import { sanitizeUser } from "@/modules/auth/helpers/auth.js";
6
+ import type { RefreshTokenInput } from "@/modules/auth/types/auth.js";
7
+
8
+ export type RefreshTokenResult =
9
+ | { kind: "invalid_token"; message: string }
10
+ | { kind: "revoked"; message: string }
11
+ | { kind: "expired"; message: string }
12
+ | { kind: "user_not_found"; message: string }
13
+ | {
14
+ kind: "refreshed";
15
+ message: string;
16
+ data: {
17
+ user: ReturnType<typeof sanitizeUser>;
18
+ access_token: string;
19
+ refresh_token: string;
20
+ token_type: "Bearer";
21
+ };
22
+ cookies: {
23
+ accessToken: string;
24
+ refreshToken: string;
25
+ refreshExpiry?: number;
26
+ };
27
+ };
28
+
29
+ export const refreshTokenService = {
30
+ refreshToken: async (body: RefreshTokenInput): Promise<RefreshTokenResult> => {
31
+ const payload = await jwt.verifyToken(body.refresh_token, "refresh");
32
+
33
+ if (!payload?.jti || typeof payload.jti !== "string") {
34
+ return { kind: "invalid_token", message: "Invalid refresh token" };
35
+ }
36
+
37
+ const userId = Number(payload.id);
38
+ if (!Number.isInteger(userId) || userId <= 0) {
39
+ return { kind: "invalid_token", message: "Invalid refresh token" };
40
+ }
41
+
42
+ const storedToken = await db.query.refreshTokens.findFirst({
43
+ where: eq(refreshTokens.jti, payload.jti)
44
+ });
45
+
46
+ if (!storedToken || storedToken.revoked === 1) {
47
+ return { kind: "revoked", message: "Refresh token revoked" };
48
+ }
49
+
50
+ if (storedToken.expiresAt.getTime() < Date.now()) {
51
+ await db.delete(refreshTokens).where(eq(refreshTokens.id, storedToken.id));
52
+ return { kind: "expired", message: "Refresh token expired" };
53
+ }
54
+
55
+ const user = await db.query.users.findFirst({
56
+ where: eq(users.id, userId),
57
+ with: { role: true }
58
+ });
59
+
60
+ if (!user) {
61
+ return { kind: "user_not_found", message: "User not found" };
62
+ }
63
+
64
+ const remember = !!payload.remember;
65
+ const refreshExpiry = remember ? jwtConfig.refreshRememberExpirySeconds : undefined;
66
+
67
+ const tokenPayload = {
68
+ id: user.id,
69
+ email: user.email,
70
+ roleId: user.role?.id,
71
+ role: user.role?.name,
72
+ remember
73
+ };
74
+
75
+ const accessToken = await jwt.generateToken(tokenPayload, "access");
76
+ const newRefreshToken = await jwt.generateToken(tokenPayload, "refresh", refreshExpiry);
77
+
78
+ // Compare-and-swap: only rotate if the jti is still the one we read.
79
+ // Two concurrent requests with the same old token → exactly one wins.
80
+ const [rotated] = await db
81
+ .update(refreshTokens)
82
+ .set({
83
+ jti: newRefreshToken.jti,
84
+ expiresAt: new Date(newRefreshToken.exp * 1000),
85
+ revoked: 0
86
+ })
87
+ .where(and(eq(refreshTokens.id, storedToken.id), eq(refreshTokens.jti, payload.jti)))
88
+ .returning();
89
+
90
+ if (!rotated) {
91
+ return { kind: "revoked", message: "Refresh token revoked" };
92
+ }
93
+
94
+ return {
95
+ kind: "refreshed",
96
+ message: "Token refreshed successfully",
97
+ data: {
98
+ user: sanitizeUser(user),
99
+ access_token: accessToken.token,
100
+ refresh_token: newRefreshToken.token,
101
+ token_type: "Bearer"
102
+ },
103
+ cookies: {
104
+ accessToken: accessToken.token,
105
+ refreshToken: newRefreshToken.token,
106
+ refreshExpiry
107
+ }
108
+ };
109
+ }
110
+ };
@@ -0,0 +1,141 @@
1
+ import { eq } from "drizzle-orm";
2
+ import type { Request, Response } from "express";
3
+ import { authConfig } from "@/config/index.js";
4
+ import { db, dispatchEvent, password, urls } from "@/framework/facade.js";
5
+ import { roles } from "@/modules/auth/database/models/role.js";
6
+ import { emailVerificationTokens, users } from "@/modules/auth/database/models/user.js";
7
+ import {
8
+ hashEmailVerificationToken,
9
+ issueTokens,
10
+ makeEmailVerificationToken,
11
+ revokeCurrentRefreshToken,
12
+ sanitizeUser
13
+ } from "@/modules/auth/helpers/auth.js";
14
+ import type { RegisterInput } from "@/modules/auth/types/auth.js";
15
+
16
+ export type RegisterResult =
17
+ | { kind: "email_exists"; message: string }
18
+ | { kind: "verification_required"; message: string }
19
+ | {
20
+ kind: "registered";
21
+ message: string;
22
+ data: {
23
+ user: ReturnType<typeof sanitizeUser>;
24
+ access_token: string;
25
+ refresh_token: string;
26
+ token_type: "Bearer";
27
+ };
28
+ };
29
+
30
+ const VERIFY_TOKEN_TTL_MS = 24 * 60 * 60 * 1000;
31
+
32
+ export const registerService = {
33
+ register: async (req: Request, res: Response, body: RegisterInput): Promise<RegisterResult> => {
34
+ const defaultRole = await db.query.roles.findFirst({
35
+ where: eq(roles.name, "user")
36
+ });
37
+
38
+ if (!defaultRole) {
39
+ throw new Error('Default role "user" not found — check your seeds');
40
+ }
41
+
42
+ const existingUser = await db.query.users.findFirst({
43
+ where: eq(users.email, body.email)
44
+ });
45
+
46
+ if (existingUser) {
47
+ return { kind: "email_exists", message: "Email already exists" };
48
+ }
49
+
50
+ const hashedPassword = await password.hashPassword(body.password);
51
+ const now = new Date();
52
+
53
+ const created = await db.transaction(async (tx) => {
54
+ const [inserted] = await tx
55
+ .insert(users)
56
+ .values({
57
+ name: body.name,
58
+ email: body.email,
59
+ password: hashedPassword,
60
+ roleId: defaultRole.id
61
+ })
62
+ .returning({ id: users.id });
63
+
64
+ if (!inserted) throw new Error("Failed to insert user");
65
+
66
+ const user = await tx.query.users.findFirst({
67
+ where: eq(users.id, inserted.id),
68
+ with: { role: true }
69
+ });
70
+
71
+ if (!user) throw new Error("Inserted user not found");
72
+
73
+ let plainVerifyToken: string | null = null;
74
+
75
+ if (authConfig.requireEmailVerification) {
76
+ plainVerifyToken = makeEmailVerificationToken();
77
+ const expiresAt = new Date(now.getTime() + VERIFY_TOKEN_TTL_MS);
78
+
79
+ // One active verification token per email
80
+ await tx.delete(emailVerificationTokens).where(eq(emailVerificationTokens.email, user.email));
81
+
82
+ await tx.insert(emailVerificationTokens).values({
83
+ email: user.email,
84
+ token: hashEmailVerificationToken(plainVerifyToken),
85
+ expiresAt,
86
+ createdAt: now
87
+ });
88
+ }
89
+
90
+ return { user, plainVerifyToken };
91
+ });
92
+
93
+ // ---- Side effects AFTER commit ----
94
+
95
+ if (created.plainVerifyToken) {
96
+ const verifyUrl = urls.url(`/verify-email?token=${created.plainVerifyToken}&email=${encodeURIComponent(created.user.email)}`);
97
+
98
+ await dispatchEvent(
99
+ "user:verify-email",
100
+ {
101
+ email: created.user.email,
102
+ name: created.user.name,
103
+ verifyUrl
104
+ },
105
+ { queue: "mail" }
106
+ );
107
+
108
+ return {
109
+ kind: "verification_required",
110
+ message: "User registered successfully. Please verify your email before logging in."
111
+ };
112
+ }
113
+
114
+ await revokeCurrentRefreshToken(req, res);
115
+ const tokens = await issueTokens(req, res, created.user, {
116
+ remember: !!body.remember
117
+ });
118
+
119
+ await dispatchEvent(
120
+ "user:signup",
121
+ {
122
+ userId: created.user.id,
123
+ email: created.user.email,
124
+ name: created.user.name
125
+ // NO password — see explanation below
126
+ },
127
+ { queue: "mail" }
128
+ );
129
+
130
+ return {
131
+ kind: "registered",
132
+ message: "User registered successfully",
133
+ data: {
134
+ user: sanitizeUser(created.user),
135
+ access_token: tokens.accessToken,
136
+ refresh_token: tokens.refreshToken,
137
+ token_type: "Bearer"
138
+ }
139
+ };
140
+ }
141
+ };