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