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
@@ -0,0 +1,305 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("@/config/index.js", () => ({
4
+ jwtConfig: { refreshRememberExpirySeconds: 60 * 60 * 24 * 30 }
5
+ }));
6
+
7
+ vi.mock("drizzle-orm", () => ({
8
+ eq: vi.fn((col: unknown, val: unknown) => ({ op: "eq", col, val })),
9
+ and: vi.fn((...args: unknown[]) => ({ op: "and", args }))
10
+ }));
11
+
12
+ vi.mock("@/framework/facade.js", () => ({
13
+ db: {
14
+ query: {
15
+ refreshTokens: { findFirst: vi.fn() },
16
+ users: { findFirst: vi.fn() }
17
+ },
18
+ delete: vi.fn((_table: unknown) => ({
19
+ where: vi.fn((_where: unknown) => Promise.resolve(undefined))
20
+ })),
21
+ update: vi.fn((_table: unknown) => ({
22
+ set: vi.fn((_values: unknown) => ({
23
+ where: vi.fn((_where: unknown) => ({
24
+ returning: vi.fn((_args?: unknown) => Promise.resolve([]))
25
+ }))
26
+ }))
27
+ }))
28
+ },
29
+ jwt: {
30
+ verifyToken: vi.fn(),
31
+ generateToken: vi.fn()
32
+ }
33
+ }));
34
+
35
+ vi.mock("@/modules/auth/helpers/auth.js", () => ({
36
+ sanitizeUser: vi.fn((u: any) => ({ id: u.id, email: u.email, name: u.name }))
37
+ }));
38
+
39
+ vi.mock("@/modules/auth/database/models/user.js", () => ({
40
+ users: { __table: "users", id: "users.id", email: "users.email" },
41
+ refreshTokens: {
42
+ __table: "refreshTokens",
43
+ id: "refreshTokens.id",
44
+ jti: "refreshTokens.jti",
45
+ userId: "refreshTokens.userId",
46
+ revoked: "refreshTokens.revoked",
47
+ expiresAt: "refreshTokens.expiresAt"
48
+ }
49
+ }));
50
+
51
+ import { db, jwt } from "@/framework/facade.js";
52
+ import { refreshTokenService } from "@/modules/auth/services/refreshtoken.js";
53
+
54
+ const futureDate = new Date(Date.now() + 60_000);
55
+ const pastDate = new Date(Date.now() - 60_000);
56
+
57
+ const storedToken = {
58
+ id: 10,
59
+ jti: "jti-old",
60
+ revoked: 0,
61
+ expiresAt: futureDate
62
+ };
63
+
64
+ const fakeUser = {
65
+ id: 1,
66
+ email: "user@example.com",
67
+ name: "Jane",
68
+ role: { id: 2, name: "user" }
69
+ };
70
+
71
+ /**
72
+ * Wire db.update so its returning() resolves with `rows`.
73
+ * Returns handles to inspect the set/where calls.
74
+ */
75
+ function mockRotate(rows: unknown[]) {
76
+ const returning = vi.fn((_args?: unknown) => Promise.resolve(rows));
77
+ const where = vi.fn((_where: unknown) => ({ returning }));
78
+ const set = vi.fn((_values: unknown) => ({ where }));
79
+ const update = vi.fn((_table: unknown) => ({ set }));
80
+
81
+ vi.mocked(db.update).mockImplementationOnce(update as any);
82
+
83
+ return { update, set, where, returning };
84
+ }
85
+
86
+ /** Wire jwt.generateToken to succeed twice (access then refresh). */
87
+ function mockTokenGeneration() {
88
+ vi.mocked(jwt.generateToken)
89
+ .mockResolvedValueOnce({
90
+ token: "new-access",
91
+ exp: 111,
92
+ jti: "access-jti"
93
+ } as any)
94
+ .mockResolvedValueOnce({
95
+ token: "new-refresh",
96
+ exp: 222,
97
+ jti: "refresh-jti"
98
+ } as any);
99
+ }
100
+
101
+ describe("refreshTokenService.refreshToken", () => {
102
+ beforeEach(() => vi.clearAllMocks());
103
+
104
+ it("returns invalid_token when jti is missing from the payload", async () => {
105
+ vi.mocked(jwt.verifyToken).mockResolvedValueOnce({ id: 1 } as any);
106
+
107
+ const result = await refreshTokenService.refreshToken({ refresh_token: "x" });
108
+
109
+ expect(result).toEqual({
110
+ kind: "invalid_token",
111
+ message: "Invalid refresh token"
112
+ });
113
+ expect(db.query.refreshTokens.findFirst).not.toHaveBeenCalled();
114
+ });
115
+
116
+ it("returns invalid_token when jti is not a string", async () => {
117
+ vi.mocked(jwt.verifyToken).mockResolvedValueOnce({
118
+ jti: 12345,
119
+ id: 1
120
+ } as any);
121
+
122
+ const result = await refreshTokenService.refreshToken({ refresh_token: "x" });
123
+
124
+ expect(result.kind).toBe("invalid_token");
125
+ expect(db.query.refreshTokens.findFirst).not.toHaveBeenCalled();
126
+ });
127
+
128
+ it("returns invalid_token when id is not a positive integer", async () => {
129
+ vi.mocked(jwt.verifyToken).mockResolvedValueOnce({
130
+ jti: "jti-old",
131
+ id: "not-a-number"
132
+ } as any);
133
+
134
+ const result = await refreshTokenService.refreshToken({ refresh_token: "x" });
135
+
136
+ expect(result.kind).toBe("invalid_token");
137
+ expect(db.query.refreshTokens.findFirst).not.toHaveBeenCalled();
138
+ });
139
+
140
+ it("returns revoked when no stored token matches the jti", async () => {
141
+ vi.mocked(jwt.verifyToken).mockResolvedValueOnce({
142
+ jti: "jti-old",
143
+ id: 1
144
+ } as any);
145
+ vi.mocked(db.query.refreshTokens.findFirst).mockResolvedValueOnce(undefined);
146
+
147
+ const result = await refreshTokenService.refreshToken({ refresh_token: "x" });
148
+
149
+ expect(result).toEqual({ kind: "revoked", message: "Refresh token revoked" });
150
+ });
151
+
152
+ it("returns revoked when the stored token is revoked", async () => {
153
+ vi.mocked(jwt.verifyToken).mockResolvedValueOnce({
154
+ jti: "jti-old",
155
+ id: 1
156
+ } as any);
157
+ vi.mocked(db.query.refreshTokens.findFirst).mockResolvedValueOnce({
158
+ ...storedToken,
159
+ revoked: 1
160
+ } as any);
161
+
162
+ const result = await refreshTokenService.refreshToken({ refresh_token: "x" });
163
+
164
+ expect(result.kind).toBe("revoked");
165
+ });
166
+
167
+ it("returns expired and deletes the row when the token has expired", async () => {
168
+ vi.mocked(jwt.verifyToken).mockResolvedValueOnce({
169
+ jti: "jti-old",
170
+ id: 1
171
+ } as any);
172
+ vi.mocked(db.query.refreshTokens.findFirst).mockResolvedValueOnce({
173
+ ...storedToken,
174
+ expiresAt: pastDate
175
+ } as any);
176
+
177
+ const result = await refreshTokenService.refreshToken({ refresh_token: "x" });
178
+
179
+ expect(result).toEqual({ kind: "expired", message: "Refresh token expired" });
180
+ expect(db.delete).toHaveBeenCalledTimes(1);
181
+ expect(db.update).not.toHaveBeenCalled();
182
+ });
183
+
184
+ it("returns user_not_found when the user has been deleted", async () => {
185
+ vi.mocked(jwt.verifyToken).mockResolvedValueOnce({
186
+ jti: "jti-old",
187
+ id: 1
188
+ } as any);
189
+ vi.mocked(db.query.refreshTokens.findFirst).mockResolvedValueOnce(storedToken as any);
190
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(undefined);
191
+
192
+ const result = await refreshTokenService.refreshToken({ refresh_token: "x" });
193
+
194
+ expect(result).toEqual({
195
+ kind: "user_not_found",
196
+ message: "User not found"
197
+ });
198
+ expect(db.update).not.toHaveBeenCalled();
199
+ });
200
+
201
+ it("rotates the token and returns new cookies on the happy path", async () => {
202
+ vi.mocked(jwt.verifyToken).mockResolvedValueOnce({
203
+ jti: "jti-old",
204
+ id: 1,
205
+ remember: false
206
+ } as any);
207
+ vi.mocked(db.query.refreshTokens.findFirst).mockResolvedValueOnce(storedToken as any);
208
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(fakeUser as any);
209
+ mockTokenGeneration();
210
+
211
+ const { update, set, where } = mockRotate([{ id: 10 }]);
212
+
213
+ const result = await refreshTokenService.refreshToken({
214
+ refresh_token: "old-token"
215
+ });
216
+
217
+ expect(result).toEqual({
218
+ kind: "refreshed",
219
+ message: "Token refreshed successfully",
220
+ data: {
221
+ user: { id: 1, email: "user@example.com", name: "Jane" },
222
+ access_token: "new-access",
223
+ refresh_token: "new-refresh",
224
+ token_type: "Bearer"
225
+ },
226
+ cookies: {
227
+ accessToken: "new-access",
228
+ refreshToken: "new-refresh",
229
+ refreshExpiry: undefined
230
+ }
231
+ });
232
+
233
+ // Rotation wrote the new jti + expiry, and cleared revoked
234
+ expect(set).toHaveBeenCalledWith({
235
+ jti: "refresh-jti",
236
+ expiresAt: new Date(222 * 1000),
237
+ revoked: 0
238
+ });
239
+
240
+ // The CAS where-clause targets both the row id AND the old jti.
241
+ const whereArg = where.mock.calls[0][0] as any;
242
+ expect(whereArg.op).toBe("and");
243
+
244
+ const eqArgs = whereArg.args.filter((a: any) => a?.op === "eq");
245
+ const idEq = eqArgs.find((c: any) => c.col === "refreshTokens.id");
246
+ const jtiEq = eqArgs.find((c: any) => c.col === "refreshTokens.jti");
247
+
248
+ expect(idEq).toBeDefined();
249
+ expect(idEq.val).toBe(10);
250
+ expect(jtiEq).toBeDefined();
251
+ expect(jtiEq.val).toBe("jti-old");
252
+
253
+ expect(update).toHaveBeenCalledTimes(1);
254
+ });
255
+
256
+ it("uses remember-me expiry when the payload has remember=true", async () => {
257
+ vi.mocked(jwt.verifyToken).mockResolvedValueOnce({
258
+ jti: "jti-old",
259
+ id: 1,
260
+ remember: true
261
+ } as any);
262
+ vi.mocked(db.query.refreshTokens.findFirst).mockResolvedValueOnce(storedToken as any);
263
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(fakeUser as any);
264
+ mockTokenGeneration();
265
+ mockRotate([{ id: 10 }]);
266
+
267
+ const result = await refreshTokenService.refreshToken({
268
+ refresh_token: "old-token"
269
+ });
270
+
271
+ expect(jwt.generateToken).toHaveBeenNthCalledWith(
272
+ 2,
273
+ expect.objectContaining({ id: 1, email: "user@example.com", remember: true }),
274
+ "refresh",
275
+ 60 * 60 * 24 * 30
276
+ );
277
+
278
+ if (result.kind === "refreshed") {
279
+ expect(result.cookies.refreshExpiry).toBe(60 * 60 * 24 * 30);
280
+ }
281
+ });
282
+
283
+ it("returns revoked when the CAS loses (token already rotated by another request)", async () => {
284
+ vi.mocked(jwt.verifyToken).mockResolvedValueOnce({
285
+ jti: "jti-old",
286
+ id: 1,
287
+ remember: false
288
+ } as any);
289
+ vi.mocked(db.query.refreshTokens.findFirst).mockResolvedValueOnce(storedToken as any);
290
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(fakeUser as any);
291
+ mockTokenGeneration();
292
+
293
+ // Simulate a losing CAS: the conditional UPDATE matches no rows
294
+ mockRotate([]);
295
+
296
+ const result = await refreshTokenService.refreshToken({
297
+ refresh_token: "old-token"
298
+ });
299
+
300
+ expect(result).toEqual({
301
+ kind: "revoked",
302
+ message: "Refresh token revoked"
303
+ });
304
+ });
305
+ });
@@ -0,0 +1,291 @@
1
+ import type { Request, Response } from "express";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ vi.mock("@/config/index.js", () => ({
5
+ authConfig: { requireEmailVerification: false }
6
+ }));
7
+
8
+ vi.mock("@/framework/facade.js", () => ({
9
+ db: {
10
+ query: {
11
+ roles: { findFirst: vi.fn() },
12
+ users: { findFirst: vi.fn() }
13
+ },
14
+ transaction: vi.fn()
15
+ },
16
+ dispatchEvent: vi.fn(),
17
+ password: { hashPassword: vi.fn() },
18
+ urls: { url: vi.fn((p: string) => `https://app.test${p}`) }
19
+ }));
20
+
21
+ vi.mock("@/modules/auth/database/models/role.js", () => ({
22
+ roles: { __table: "roles", name: "roles.name" }
23
+ }));
24
+
25
+ vi.mock("@/modules/auth/database/models/user.js", () => ({
26
+ users: { __table: "users", id: "users.id", email: "users.email" },
27
+ emailVerificationTokens: {
28
+ __table: "emailVerificationTokens",
29
+ email: "emailVerificationTokens.email",
30
+ token: "emailVerificationTokens.token",
31
+ expiresAt: "emailVerificationTokens.expiresAt",
32
+ createdAt: "emailVerificationTokens.createdAt"
33
+ }
34
+ }));
35
+
36
+ vi.mock("@/modules/auth/helpers/auth.js", () => ({
37
+ hashEmailVerificationToken: vi.fn((t: string) => `hashed:${t}`),
38
+ makeEmailVerificationToken: vi.fn(() => "plain-verify-token"),
39
+ issueTokens: vi.fn(),
40
+ revokeCurrentRefreshToken: vi.fn(),
41
+ sanitizeUser: vi.fn((u: any) => ({ id: u.id, email: u.email, name: u.name }))
42
+ }));
43
+
44
+ import { authConfig } from "@/config/index.js";
45
+ import { db, dispatchEvent, password, urls } from "@/framework/facade.js";
46
+ import { issueTokens, revokeCurrentRefreshToken } from "@/modules/auth/helpers/auth.js";
47
+ import { registerService } from "@/modules/auth/services/register.js";
48
+
49
+ const body = { name: "Jane", email: "jane@example.com", password: "secret123" };
50
+
51
+ const fakeUser = {
52
+ id: 1,
53
+ name: "Jane",
54
+ email: "jane@example.com",
55
+ role: { id: 2, name: "user" }
56
+ };
57
+
58
+ function makeReqRes() {
59
+ const req = {} as Request;
60
+ const res = {} as Response;
61
+ return { req, res };
62
+ }
63
+
64
+ /**
65
+ * Build a fake tx.
66
+ * - insertedRow: what insert().returning() resolves to
67
+ * - refetchedUser: what query.users.findFirst returns inside the tx
68
+ */
69
+ function buildTx({ insertedRow = { id: 1 } as unknown, refetchedUser = fakeUser as unknown | null } = {}) {
70
+ const insertCalls: { table: unknown; values: unknown }[] = [];
71
+ let currentInsertTable: unknown;
72
+
73
+ const txInsertReturning = vi.fn((_args?: unknown) => Promise.resolve(insertedRow ? [insertedRow] : []));
74
+ const txInsertValues = vi.fn((values: unknown) => {
75
+ insertCalls.push({ table: currentInsertTable, values });
76
+ return { returning: txInsertReturning };
77
+ });
78
+ const txInsert = vi.fn((table: unknown) => {
79
+ currentInsertTable = table;
80
+ return { values: txInsertValues };
81
+ });
82
+
83
+ const txDeleteWhere = vi.fn((_where: unknown) => Promise.resolve(undefined));
84
+ const txDelete = vi.fn((_table: unknown) => ({ where: txDeleteWhere }));
85
+
86
+ const txFindFirst = vi.fn((_args: unknown) => Promise.resolve(refetchedUser));
87
+
88
+ return {
89
+ tx: {
90
+ insert: txInsert,
91
+ delete: txDelete,
92
+ query: { users: { findFirst: txFindFirst } }
93
+ },
94
+ txInsert,
95
+ txInsertValues,
96
+ txInsertReturning,
97
+ txDelete,
98
+ txDeleteWhere,
99
+ txFindFirst,
100
+ insertCalls
101
+ };
102
+ }
103
+
104
+ describe("registerService.register (express)", () => {
105
+ beforeEach(() => {
106
+ vi.clearAllMocks();
107
+ vi.mocked(authConfig).requireEmailVerification = false;
108
+ vi.mocked(password.hashPassword).mockResolvedValue("hashed-secret" as any);
109
+ vi.mocked(db.query.roles.findFirst).mockResolvedValue({
110
+ id: 2,
111
+ name: "user"
112
+ } as any);
113
+ vi.mocked(db.query.users.findFirst).mockResolvedValue(undefined as any);
114
+ });
115
+
116
+ it("returns email_exists when the email is already taken (no hashing, no tx)", async () => {
117
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(fakeUser as any);
118
+ const { req, res } = makeReqRes();
119
+
120
+ const result = await registerService.register(req, res, body);
121
+
122
+ expect(result).toEqual({
123
+ kind: "email_exists",
124
+ message: "Email already exists"
125
+ });
126
+ expect(password.hashPassword).not.toHaveBeenCalled();
127
+ expect(db.transaction).not.toHaveBeenCalled();
128
+ });
129
+
130
+ it("throws if the default 'user' role is missing", async () => {
131
+ vi.mocked(db.query.roles.findFirst).mockResolvedValueOnce(undefined);
132
+ const { req, res } = makeReqRes();
133
+
134
+ await expect(registerService.register(req, res, body)).rejects.toThrow('Default role "user" not found');
135
+
136
+ expect(db.transaction).not.toHaveBeenCalled();
137
+ });
138
+
139
+ it("hashes the password BEFORE opening the transaction", async () => {
140
+ const order: string[] = [];
141
+ vi.mocked(password.hashPassword).mockImplementationOnce(async () => {
142
+ order.push("hash");
143
+ return "hashed-secret" as any;
144
+ });
145
+
146
+ const { tx } = buildTx();
147
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => {
148
+ order.push("tx");
149
+ return fn(tx);
150
+ });
151
+ vi.mocked(issueTokens).mockResolvedValueOnce({
152
+ accessToken: "a",
153
+ refreshToken: "r"
154
+ } as any);
155
+
156
+ const { req, res } = makeReqRes();
157
+ await registerService.register(req, res, body);
158
+
159
+ expect(order).toEqual(["hash", "tx"]);
160
+ });
161
+
162
+ it("inserts the user with the hashed password and the default role id", async () => {
163
+ const { tx, insertCalls } = buildTx();
164
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
165
+ vi.mocked(issueTokens).mockResolvedValueOnce({
166
+ accessToken: "a",
167
+ refreshToken: "r"
168
+ } as any);
169
+ const { req, res } = makeReqRes();
170
+
171
+ await registerService.register(req, res, body);
172
+
173
+ const inserted = insertCalls[0].values as any;
174
+ expect(inserted).toMatchObject({
175
+ name: "Jane",
176
+ email: "jane@example.com",
177
+ password: "hashed-secret",
178
+ roleId: 2
179
+ });
180
+ expect(inserted.password).not.toBe("secret123");
181
+ });
182
+
183
+ it("registers, issues tokens, and dispatches user:signup WITHOUT the password", async () => {
184
+ const { tx } = buildTx();
185
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
186
+ vi.mocked(issueTokens).mockResolvedValueOnce({
187
+ accessToken: "access-123",
188
+ refreshToken: "refresh-456"
189
+ } as any);
190
+ const { req, res } = makeReqRes();
191
+
192
+ const result = await registerService.register(req, res, {
193
+ ...body,
194
+ remember: true
195
+ });
196
+
197
+ expect(revokeCurrentRefreshToken).toHaveBeenCalledWith(req, res);
198
+ expect(issueTokens).toHaveBeenCalledWith(req, res, fakeUser, {
199
+ remember: true
200
+ });
201
+
202
+ // The critical assertion: password must NOT be in the event payload
203
+ expect(dispatchEvent).toHaveBeenCalledWith(
204
+ "user:signup",
205
+ {
206
+ userId: 1,
207
+ email: "jane@example.com",
208
+ name: "Jane"
209
+ },
210
+ { queue: "mail" }
211
+ );
212
+
213
+ const [, eventPayload] = vi.mocked(dispatchEvent).mock.calls[0];
214
+ expect(eventPayload).not.toHaveProperty("password");
215
+
216
+ expect(result).toEqual({
217
+ kind: "registered",
218
+ message: "User registered successfully",
219
+ data: {
220
+ user: { id: 1, email: "jane@example.com", name: "Jane" },
221
+ access_token: "access-123",
222
+ refresh_token: "refresh-456",
223
+ token_type: "Bearer"
224
+ }
225
+ });
226
+ });
227
+
228
+ it("returns verification_required and dispatches user:verify-email when verification is on", async () => {
229
+ vi.mocked(authConfig).requireEmailVerification = true;
230
+
231
+ const { tx, insertCalls } = buildTx();
232
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
233
+ const { req, res } = makeReqRes();
234
+
235
+ const result = await registerService.register(req, res, body);
236
+
237
+ expect(result).toEqual({
238
+ kind: "verification_required",
239
+ message: "User registered successfully. Please verify your email before logging in."
240
+ });
241
+
242
+ expect(dispatchEvent).toHaveBeenCalledWith(
243
+ "user:verify-email",
244
+ {
245
+ email: "jane@example.com",
246
+ name: "Jane",
247
+ verifyUrl: "https://app.test/verify-email?token=plain-verify-token&email=jane%40example.com"
248
+ },
249
+ { queue: "mail" }
250
+ );
251
+
252
+ // No signup event, no tokens
253
+ expect(issueTokens).not.toHaveBeenCalled();
254
+ expect(revokeCurrentRefreshToken).not.toHaveBeenCalled();
255
+
256
+ // Second insert (the token) stored the HASH, not the plain token
257
+ const tokenInsert = insertCalls[1].values as any;
258
+ expect(tokenInsert.token).toBe("hashed:plain-verify-token");
259
+ expect(tokenInsert.token).not.toBe("plain-verify-token");
260
+ expect(tokenInsert.email).toBe("jane@example.com");
261
+ });
262
+
263
+ it("does not dispatch any email if the transaction fails", async () => {
264
+ vi.mocked(db.transaction).mockRejectedValueOnce(new Error("db down"));
265
+ const { req, res } = makeReqRes();
266
+
267
+ await expect(registerService.register(req, res, body)).rejects.toThrow("db down");
268
+
269
+ expect(dispatchEvent).not.toHaveBeenCalled();
270
+ });
271
+
272
+ it("throws when the insert returns no row", async () => {
273
+ const { tx } = buildTx({ insertedRow: null });
274
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
275
+ const { req, res } = makeReqRes();
276
+
277
+ await expect(registerService.register(req, res, body)).rejects.toThrow("Failed to insert user");
278
+
279
+ expect(dispatchEvent).not.toHaveBeenCalled();
280
+ });
281
+
282
+ it("throws when the inserted user cannot be re-fetched", async () => {
283
+ const { tx } = buildTx({ refetchedUser: null });
284
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
285
+ const { req, res } = makeReqRes();
286
+
287
+ await expect(registerService.register(req, res, body)).rejects.toThrow("Inserted user not found");
288
+
289
+ expect(dispatchEvent).not.toHaveBeenCalled();
290
+ });
291
+ });