create-nexwire 4.0.0 → 4.1.0

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 (175) hide show
  1. package/README.md +5 -4
  2. package/express/.env +1 -1
  3. package/express/README.md +13 -7
  4. package/express/package.json +2 -2
  5. package/express/src/config/session.ts +1 -1
  6. package/express/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  7. package/express/src/database/migrations/sqlite/meta/_journal.json +1 -1
  8. package/express/src/database/schema.ts +3 -3
  9. package/express/src/framework/cache/cache.ts +82 -81
  10. package/express/src/framework/database/seed.ts +1 -1
  11. package/express/src/framework/facade.ts +1 -24
  12. package/express/src/framework/http/app.ts +5 -5
  13. package/express/src/framework/http/openapi.ts +1 -1
  14. package/express/src/framework/http/router.ts +3 -18
  15. package/express/src/framework/http/types.ts +19 -0
  16. package/express/src/framework/http/validation.ts +18 -18
  17. package/express/src/framework/maker-cli/middleware/core.mjs +41 -47
  18. package/express/src/framework/maker-cli/module/core.mjs +920 -684
  19. package/express/src/framework/maker-cli/module/index.mjs +202 -140
  20. package/express/src/framework/maker-cli/runtime/core.mjs +1 -2
  21. package/express/src/framework/maker-cli/stubs/controller/openapi.ts.stub +40 -14
  22. package/express/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +40 -15
  23. package/express/src/framework/maker-cli/stubs/controller/plain.ts.stub +42 -16
  24. package/express/src/framework/maker-cli/stubs/controller/standalone.ts.stub +47 -0
  25. package/express/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
  26. package/express/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
  27. package/express/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
  28. package/express/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +9 -8
  29. package/express/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +9 -8
  30. package/express/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +9 -8
  31. package/express/src/framework/maker-cli/stubs/notification/route.api.ts.stub +50 -50
  32. package/express/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +11 -11
  33. package/express/src/framework/maker-cli/stubs/route/api.ts.stub +93 -79
  34. package/express/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -10
  35. package/express/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +113 -0
  36. package/express/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +37 -0
  37. package/express/src/framework/maker-cli/stubs/{controller/schema.plain.ts.stub → schema/name.plain.ts.stub} +13 -13
  38. package/express/src/framework/maker-cli/stubs/{controller/schema.ts.stub → schema/name.ts.stub} +32 -32
  39. package/express/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
  40. package/express/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
  41. package/express/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
  42. package/express/src/framework/maker-cli/utils/naming.mjs +56 -13
  43. package/express/src/framework/modules/discover.ts +57 -54
  44. package/express/src/framework/notification/index.ts +1 -1
  45. package/express/src/framework/realtime/broadcast.ts +46 -46
  46. package/express/src/framework/server.ts +2 -1
  47. package/express/src/framework/session/session.ts +102 -98
  48. package/express/src/framework/support/mail.ts +48 -48
  49. package/express/src/framework/support/status-codes.ts +75 -0
  50. package/express/src/middlewares/auth-middleware.ts +1 -1
  51. package/express/src/modules/auth/__tests__/forgetpass.test.ts +200 -0
  52. package/express/src/modules/auth/__tests__/login.test.ts +171 -0
  53. package/express/src/modules/auth/__tests__/logout.test.ts +60 -0
  54. package/express/src/modules/auth/__tests__/logoutalldevices.test.ts +49 -0
  55. package/express/src/modules/auth/__tests__/me.test.ts +91 -0
  56. package/express/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
  57. package/express/src/modules/auth/__tests__/register.test.ts +291 -0
  58. package/express/src/modules/auth/__tests__/resetpass.test.ts +206 -0
  59. package/express/src/modules/auth/__tests__/verifyemail.test.ts +183 -0
  60. package/express/src/modules/auth/controllers/auth.ts +254 -0
  61. package/express/src/modules/auth/controllers/{role.controller.ts → role.ts} +25 -15
  62. package/{hono/src/modules/auth/database/models/notifications.ts → express/src/modules/auth/database/models/notification.ts} +1 -1
  63. package/express/src/modules/auth/database/models/role.ts +1 -1
  64. package/express/src/modules/auth/database/models/user.ts +1 -1
  65. package/express/src/modules/auth/facade.ts +12 -0
  66. package/express/src/modules/auth/{controllers/auth.helpers.ts → helpers/auth.ts} +3 -4
  67. package/express/src/modules/auth/jobs/registeruser.ts +23 -23
  68. package/express/src/modules/auth/routes/{api.ts → index.ts} +150 -150
  69. package/express/src/modules/auth/routes/role.ts +33 -33
  70. package/{hono/src/modules/auth/controllers/auth.schema.ts → express/src/modules/auth/schemas/auth.ts} +102 -102
  71. package/express/src/modules/auth/services/forgetpassword.ts +66 -0
  72. package/express/src/modules/auth/services/login.ts +63 -0
  73. package/express/src/modules/auth/services/logout.ts +25 -0
  74. package/express/src/modules/auth/services/logoutalldevices.ts +19 -0
  75. package/express/src/modules/auth/services/me.ts +31 -0
  76. package/express/src/modules/auth/services/refreshtoken.ts +110 -0
  77. package/express/src/modules/auth/services/register.ts +141 -0
  78. package/express/src/modules/auth/services/resetpassword.ts +69 -0
  79. package/express/src/modules/auth/services/verifyemail.ts +77 -0
  80. package/express/src/modules/auth/types/auth.ts +31 -0
  81. package/express/src/resources/src/assets/images/logo.png +0 -0
  82. package/express/src/resources/src/pages/auth/forgetPassword.vue +1 -0
  83. package/express/src/resources/src/pages/auth/login.vue +1 -1
  84. package/express/src/storage/database/nexwire.sqlite +0 -0
  85. package/hono/.env +1 -1
  86. package/hono/README.md +13 -7
  87. package/hono/package.json +1 -1
  88. package/hono/src/config/session.ts +1 -1
  89. package/hono/src/database/migrations/sqlite/meta/0000_snapshot.json +1 -1
  90. package/hono/src/database/migrations/sqlite/meta/_journal.json +1 -1
  91. package/hono/src/database/schema.ts +3 -3
  92. package/hono/src/framework/cache/cache.ts +82 -81
  93. package/hono/src/framework/database/seed.ts +1 -1
  94. package/hono/src/framework/http/app.ts +5 -5
  95. package/hono/src/framework/http/validation.ts +21 -21
  96. package/hono/src/framework/maker-cli/middleware/core.mjs +40 -46
  97. package/hono/src/framework/maker-cli/module/core.mjs +401 -168
  98. package/hono/src/framework/maker-cli/module/index.mjs +73 -11
  99. package/hono/src/framework/maker-cli/stubs/controller/openapi.ts.stub +28 -16
  100. package/hono/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +28 -17
  101. package/hono/src/framework/maker-cli/stubs/controller/plain.ts.stub +28 -16
  102. package/hono/src/framework/maker-cli/stubs/controller/standalone.ts.stub +56 -0
  103. package/hono/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
  104. package/hono/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
  105. package/hono/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
  106. package/hono/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +9 -8
  107. package/hono/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +9 -8
  108. package/hono/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +9 -8
  109. package/hono/src/framework/maker-cli/stubs/notification/route.api.ts.stub +49 -49
  110. package/hono/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +11 -11
  111. package/hono/src/framework/maker-cli/stubs/route/api.ts.stub +93 -79
  112. package/hono/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -10
  113. package/hono/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +112 -0
  114. package/hono/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +37 -0
  115. package/hono/src/framework/maker-cli/stubs/{controller/schema.plain.ts.stub → schema/name.plain.ts.stub} +13 -13
  116. package/hono/src/framework/maker-cli/stubs/{controller/schema.ts.stub → schema/name.ts.stub} +32 -32
  117. package/hono/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
  118. package/hono/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
  119. package/hono/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
  120. package/hono/src/framework/maker-cli/utils/naming.mjs +56 -13
  121. package/hono/src/framework/modules/discover.ts +57 -54
  122. package/hono/src/framework/notification/index.ts +1 -1
  123. package/hono/src/framework/realtime/broadcast.ts +46 -46
  124. package/hono/src/framework/server.ts +1 -1
  125. package/hono/src/framework/session/session.ts +121 -116
  126. package/hono/src/middlewares/auth-middleware.ts +1 -1
  127. package/hono/src/modules/auth/__tests__/forgetpass.test.ts +170 -0
  128. package/hono/src/modules/auth/__tests__/login.test.ts +154 -0
  129. package/hono/src/modules/auth/__tests__/logout.test.ts +53 -0
  130. package/hono/src/modules/auth/__tests__/logoutalldevices.test.ts +34 -0
  131. package/hono/src/modules/auth/__tests__/me.test.ts +87 -0
  132. package/hono/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
  133. package/hono/src/modules/auth/__tests__/register.test.ts +258 -0
  134. package/hono/src/modules/auth/__tests__/resetpass.test.ts +214 -0
  135. package/hono/src/modules/auth/__tests__/verifyemail.test.ts +189 -0
  136. package/hono/src/modules/auth/controllers/auth.ts +238 -0
  137. package/hono/src/modules/auth/controllers/{role.controller.ts → role.ts} +25 -25
  138. package/{express/src/modules/auth/database/models/notifications.ts → hono/src/modules/auth/database/models/notification.ts} +1 -1
  139. package/hono/src/modules/auth/database/models/role.ts +1 -1
  140. package/hono/src/modules/auth/database/models/user.ts +1 -1
  141. package/hono/src/modules/auth/facade.ts +12 -0
  142. package/hono/src/modules/auth/{controllers/auth.helpers.ts → helpers/auth.ts} +5 -6
  143. package/hono/src/modules/auth/jobs/registeruser.ts +23 -23
  144. package/hono/src/modules/auth/routes/{api.ts → index.ts} +150 -150
  145. package/hono/src/modules/auth/routes/role.ts +33 -33
  146. package/{express/src/modules/auth/controllers/auth.schema.ts → hono/src/modules/auth/schemas/auth.ts} +102 -102
  147. package/hono/src/modules/auth/services/forgetpassword.ts +58 -0
  148. package/hono/src/modules/auth/services/login.ts +61 -0
  149. package/hono/src/modules/auth/services/logout.ts +24 -0
  150. package/hono/src/modules/auth/services/logoutalldevices.ts +19 -0
  151. package/hono/src/modules/auth/services/me.ts +31 -0
  152. package/hono/src/modules/auth/services/refreshtoken.ts +110 -0
  153. package/hono/src/modules/auth/services/register.ts +132 -0
  154. package/hono/src/modules/auth/services/resetpassword.ts +69 -0
  155. package/hono/src/modules/auth/services/verifyemail.ts +77 -0
  156. package/hono/src/modules/auth/types/auth.ts +31 -0
  157. package/hono/src/resources/src/assets/images/logo.png +0 -0
  158. package/hono/src/storage/database/nexwire.sqlite +0 -0
  159. package/package.json +1 -1
  160. package/express/src/framework/maker-cli/stubs/example/console.ts.stub +0 -34
  161. package/express/src/framework/maker-cli/stubs/example/controller.ts.stub +0 -418
  162. package/express/src/framework/maker-cli/stubs/example/job.ts.stub +0 -75
  163. package/express/src/framework/maker-cli/stubs/example/route.api.ts.stub +0 -207
  164. package/express/src/framework/maker-cli/stubs/example/route.plain.ts.stub +0 -18
  165. package/express/src/framework/maker-cli/stubs/example/schema.plain.ts.stub +0 -42
  166. package/express/src/framework/maker-cli/stubs/example/schema.ts.stub +0 -42
  167. package/express/src/modules/auth/controllers/auth.controller.ts +0 -358
  168. package/hono/src/framework/maker-cli/stubs/example/console.ts.stub +0 -34
  169. package/hono/src/framework/maker-cli/stubs/example/controller.ts.stub +0 -505
  170. package/hono/src/framework/maker-cli/stubs/example/job.ts.stub +0 -75
  171. package/hono/src/framework/maker-cli/stubs/example/route.api.ts.stub +0 -207
  172. package/hono/src/framework/maker-cli/stubs/example/route.plain.ts.stub +0 -18
  173. package/hono/src/framework/maker-cli/stubs/example/schema.plain.ts.stub +0 -42
  174. package/hono/src/framework/maker-cli/stubs/example/schema.ts.stub +0 -42
  175. package/hono/src/modules/auth/controllers/auth.controller.ts +0 -413
@@ -0,0 +1,200 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("@/framework/facade.js", () => ({
4
+ db: {
5
+ transaction: vi.fn()
6
+ },
7
+ dispatchEvent: vi.fn(),
8
+ urls: { url: vi.fn((p: string) => `https://app.test${p}`) }
9
+ }));
10
+
11
+ vi.mock("@/modules/auth/helpers/auth.js", () => ({
12
+ makeResetToken: vi.fn(() => "plain-token-123"),
13
+ hashResetToken: vi.fn((t: string) => `hashed:${t}`)
14
+ }));
15
+
16
+ vi.mock("@/modules/auth/database/models/user.js", () => ({
17
+ users: { __table: "users", id: "users.id", email: "users.email" },
18
+ passwordResetTokens: {
19
+ __table: "passwordResetTokens",
20
+ email: "passwordResetTokens.email",
21
+ token: "passwordResetTokens.token",
22
+ expiresAt: "passwordResetTokens.expiresAt",
23
+ createdAt: "passwordResetTokens.createdAt"
24
+ }
25
+ }));
26
+
27
+ import { db, dispatchEvent, urls } from "@/framework/facade.js";
28
+ import { hashResetToken, makeResetToken } from "@/modules/auth/helpers/auth.js";
29
+ import { forgotPasswordService } from "@/modules/auth/services/forgetpassword.js";
30
+
31
+ const fakeUser = {
32
+ id: 1,
33
+ email: "user@example.com",
34
+ name: "Jane"
35
+ };
36
+
37
+ const body = { email: "user@example.com" };
38
+
39
+ /**
40
+ * Build a fake tx whose calls we can inspect.
41
+ * - user: what query.users.findFirst returns (null = no user)
42
+ */
43
+ function buildTx({ user = fakeUser as unknown | null } = {}) {
44
+ const deleteCalls: { table: unknown; where: unknown }[] = [];
45
+ let currentDeleteTable: unknown;
46
+
47
+ const txDeleteWhere = vi.fn((where: unknown) => {
48
+ deleteCalls.push({ table: currentDeleteTable, where });
49
+ return Promise.resolve(undefined);
50
+ });
51
+ const txDelete = vi.fn((table: unknown) => {
52
+ currentDeleteTable = table;
53
+ return { where: txDeleteWhere };
54
+ });
55
+
56
+ const insertCalls: { table: unknown; values: unknown }[] = [];
57
+ let currentInsertTable: unknown;
58
+
59
+ const txInsertValues = vi.fn((values: unknown) => {
60
+ insertCalls.push({ table: currentInsertTable, values });
61
+ return Promise.resolve(undefined);
62
+ });
63
+ const txInsert = vi.fn((table: unknown) => {
64
+ currentInsertTable = table;
65
+ return { values: txInsertValues };
66
+ });
67
+
68
+ const txFindFirst = vi.fn((_args: unknown) => Promise.resolve(user));
69
+
70
+ return {
71
+ tx: {
72
+ delete: txDelete,
73
+ insert: txInsert,
74
+ query: { users: { findFirst: txFindFirst } }
75
+ },
76
+ txDelete,
77
+ txDeleteWhere,
78
+ txInsert,
79
+ txInsertValues,
80
+ txFindFirst,
81
+ deleteCalls,
82
+ insertCalls
83
+ };
84
+ }
85
+
86
+ describe("forgotPasswordService.forgotPassword", () => {
87
+ beforeEach(() => vi.clearAllMocks());
88
+
89
+ it("returns 'sent' and does nothing else when the user does not exist", async () => {
90
+ const { tx, txDelete, txInsert, txFindFirst } = buildTx({ user: null });
91
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
92
+
93
+ const result = await forgotPasswordService.forgotPassword(body);
94
+
95
+ expect(result).toEqual({
96
+ kind: "sent",
97
+ message: "Reset link has been sent"
98
+ });
99
+
100
+ // Only the housekeeping delete ran — no insert, no email
101
+ expect(txDelete).toHaveBeenCalledTimes(1);
102
+ expect(txFindFirst).toHaveBeenCalledTimes(1);
103
+ expect(txInsert).not.toHaveBeenCalled();
104
+ expect(dispatchEvent).not.toHaveBeenCalled();
105
+ expect(urls.url).not.toHaveBeenCalled();
106
+ });
107
+
108
+ it("issues a fresh token, stores its HASH, and dispatches the mail event", async () => {
109
+ const { tx, txInsertValues } = buildTx();
110
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
111
+
112
+ const result = await forgotPasswordService.forgotPassword(body);
113
+
114
+ expect(result).toEqual({
115
+ kind: "sent",
116
+ message: "Reset link has been sent"
117
+ });
118
+
119
+ // Plain token generated and hashed
120
+ expect(makeResetToken).toHaveBeenCalledTimes(1);
121
+ expect(hashResetToken).toHaveBeenCalledWith("plain-token-123");
122
+
123
+ // Inserted token is the HASH, never the plain one
124
+ expect(txInsertValues).toHaveBeenCalledTimes(1);
125
+ const inserted = txInsertValues.mock.calls[0][0] as any;
126
+ expect(inserted).toMatchObject({
127
+ email: "user@example.com",
128
+ token: "hashed:plain-token-123"
129
+ });
130
+ expect(inserted.token).not.toBe("plain-token-123");
131
+ expect(inserted.expiresAt).toBeInstanceOf(Date);
132
+ expect(inserted.createdAt).toBeInstanceOf(Date);
133
+
134
+ // ~15 min TTL between createdAt and expiresAt
135
+ const ttl = inserted.expiresAt.getTime() - inserted.createdAt.getTime();
136
+ expect(ttl).toBe(15 * 60 * 1000);
137
+
138
+ // Reset URL contains the PLAIN token (the user has to receive it)
139
+ expect(urls.url).toHaveBeenCalledWith("/reset-password?token=plain-token-123&email=user%40example.com");
140
+
141
+ // Mail dispatched with the reset URL
142
+ expect(dispatchEvent).toHaveBeenCalledWith(
143
+ "user:forget-password",
144
+ {
145
+ email: "user@example.com",
146
+ name: "Jane",
147
+ resetUrl: "https://app.test/reset-password?token=plain-token-123&email=user%40example.com"
148
+ },
149
+ { queue: "mail" }
150
+ );
151
+ });
152
+
153
+ it("revokes any existing tokens for the email before inserting a new one", async () => {
154
+ const { tx, txDelete, deleteCalls } = buildTx();
155
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
156
+
157
+ await forgotPasswordService.forgotPassword(body);
158
+
159
+ // Two deletes: expired cleanup + revoke existing
160
+ expect(txDelete).toHaveBeenCalledTimes(2);
161
+ expect(deleteCalls).toHaveLength(2);
162
+
163
+ // Both target passwordResetTokens
164
+ expect((deleteCalls[0].table as any).__table).toBe("passwordResetTokens");
165
+ expect((deleteCalls[1].table as any).__table).toBe("passwordResetTokens");
166
+ });
167
+
168
+ it("sends the same message whether or not the user exists (no enumeration)", async () => {
169
+ const missing = buildTx({ user: null });
170
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(missing.tx));
171
+ const r1 = await forgotPasswordService.forgotPassword(body);
172
+
173
+ vi.clearAllMocks();
174
+
175
+ const exists = buildTx();
176
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(exists.tx));
177
+ const r2 = await forgotPasswordService.forgotPassword(body);
178
+
179
+ expect(r1).toEqual(r2);
180
+ });
181
+
182
+ it("does not dispatch the email if the transaction fails", async () => {
183
+ vi.mocked(db.transaction).mockRejectedValueOnce(new Error("db down"));
184
+
185
+ await expect(forgotPasswordService.forgotPassword(body)).rejects.toThrow("db down");
186
+
187
+ expect(dispatchEvent).not.toHaveBeenCalled();
188
+ expect(urls.url).not.toHaveBeenCalled();
189
+ });
190
+
191
+ it("propagates a dispatch failure (token is already stored)", async () => {
192
+ const { tx } = buildTx();
193
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
194
+ vi.mocked(dispatchEvent).mockRejectedValueOnce(new Error("queue down"));
195
+
196
+ // Current behavior: dispatch failure aborts the request.
197
+ // The token IS in the DB — the user can retry "forgot password".
198
+ await expect(forgotPasswordService.forgotPassword(body)).rejects.toThrow("queue down");
199
+ });
200
+ });
@@ -0,0 +1,171 @@
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: { query: { users: { findFirst: vi.fn() } } },
10
+ password: { verifyPassword: vi.fn() }
11
+ }));
12
+
13
+ vi.mock("@/modules/auth/helpers/auth.js", () => ({
14
+ issueTokens: vi.fn(),
15
+ revokeCurrentRefreshToken: vi.fn(),
16
+ sanitizeUser: vi.fn((u: any) => ({ id: u.id, email: u.email, name: u.name }))
17
+ }));
18
+
19
+ import { authConfig } from "@/config/index.js";
20
+ import { db, password } from "@/framework/facade.js";
21
+ import { issueTokens, revokeCurrentRefreshToken } from "@/modules/auth/helpers/auth.js";
22
+ import { loginService } from "@/modules/auth/services/login.js";
23
+
24
+ const fakeUser = {
25
+ id: 1,
26
+ email: "user@example.com",
27
+ name: "Jane",
28
+ password: "hashed-pw",
29
+ emailVerifiedAt: new Date()
30
+ };
31
+
32
+ const body = { email: "user@example.com", password: "pw" };
33
+
34
+ /** Minimal fake req/res. The service just passes them through. */
35
+ function makeReqRes() {
36
+ const req = {} as Request;
37
+ const res = {} as Response;
38
+ return { req, res };
39
+ }
40
+
41
+ describe("loginService.login (express)", () => {
42
+ beforeEach(() => {
43
+ vi.clearAllMocks();
44
+ vi.mocked(authConfig).requireEmailVerification = false;
45
+ });
46
+
47
+ it("returns invalid_credentials when the user does not exist (no hash attempt)", async () => {
48
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(undefined);
49
+ const { req, res } = makeReqRes();
50
+
51
+ const result = await loginService.login(req, res, body);
52
+
53
+ expect(result).toEqual({
54
+ kind: "invalid_credentials",
55
+ message: "Invalid credentials"
56
+ });
57
+ expect(password.verifyPassword).not.toHaveBeenCalled();
58
+ expect(issueTokens).not.toHaveBeenCalled();
59
+ expect(revokeCurrentRefreshToken).not.toHaveBeenCalled();
60
+ });
61
+
62
+ it("returns invalid_credentials when the password is wrong", async () => {
63
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(fakeUser as any);
64
+ vi.mocked(password.verifyPassword).mockResolvedValueOnce(false);
65
+ const { req, res } = makeReqRes();
66
+
67
+ const result = await loginService.login(req, res, body);
68
+
69
+ expect(result).toEqual({
70
+ kind: "invalid_credentials",
71
+ message: "Invalid credentials"
72
+ });
73
+ expect(password.verifyPassword).toHaveBeenCalledWith("pw", "hashed-pw");
74
+ expect(issueTokens).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it("returns email_not_verified when verification is required and the email is not verified", async () => {
78
+ vi.mocked(authConfig).requireEmailVerification = true;
79
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce({
80
+ ...fakeUser,
81
+ emailVerifiedAt: null
82
+ } as any);
83
+ vi.mocked(password.verifyPassword).mockResolvedValueOnce(true);
84
+ const { req, res } = makeReqRes();
85
+
86
+ const result = await loginService.login(req, res, body);
87
+
88
+ expect(result).toEqual({
89
+ kind: "email_not_verified",
90
+ message: "Please verify your email before logging in"
91
+ });
92
+ expect(issueTokens).not.toHaveBeenCalled();
93
+ expect(revokeCurrentRefreshToken).not.toHaveBeenCalled();
94
+ });
95
+
96
+ it("does NOT check email verification when the feature is disabled", async () => {
97
+ vi.mocked(authConfig).requireEmailVerification = false;
98
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce({
99
+ ...fakeUser,
100
+ emailVerifiedAt: null
101
+ } as any);
102
+ vi.mocked(password.verifyPassword).mockResolvedValueOnce(true);
103
+ vi.mocked(issueTokens).mockResolvedValueOnce({
104
+ accessToken: "a",
105
+ refreshToken: "r"
106
+ } as any);
107
+ const { req, res } = makeReqRes();
108
+
109
+ const result = await loginService.login(req, res, body);
110
+
111
+ expect(result.kind).toBe("logged_in");
112
+ });
113
+
114
+ it("logs in successfully and passes req/res through to the helpers", async () => {
115
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(fakeUser as any);
116
+ vi.mocked(password.verifyPassword).mockResolvedValueOnce(true);
117
+ vi.mocked(issueTokens).mockResolvedValueOnce({
118
+ accessToken: "access-123",
119
+ refreshToken: "refresh-456"
120
+ } as any);
121
+ const { req, res } = makeReqRes();
122
+
123
+ const result = await loginService.login(req, res, {
124
+ ...body,
125
+ remember: true
126
+ });
127
+
128
+ // The Express-specific contract: req and res are forwarded as-is.
129
+ expect(revokeCurrentRefreshToken).toHaveBeenCalledWith(req, res);
130
+ expect(issueTokens).toHaveBeenCalledWith(req, res, fakeUser, {
131
+ remember: true
132
+ });
133
+
134
+ expect(result).toEqual({
135
+ kind: "logged_in",
136
+ message: "User logged in successfully",
137
+ data: {
138
+ user: { id: 1, email: "user@example.com", name: "Jane" },
139
+ access_token: "access-123",
140
+ refresh_token: "refresh-456",
141
+ token_type: "Bearer"
142
+ }
143
+ });
144
+ });
145
+
146
+ it("coerces a missing remember flag to false", async () => {
147
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(fakeUser as any);
148
+ vi.mocked(password.verifyPassword).mockResolvedValueOnce(true);
149
+ vi.mocked(issueTokens).mockResolvedValueOnce({
150
+ accessToken: "a",
151
+ refreshToken: "r"
152
+ } as any);
153
+ const { req, res } = makeReqRes();
154
+
155
+ await loginService.login(req, res, body);
156
+
157
+ expect(issueTokens).toHaveBeenCalledWith(req, res, fakeUser, {
158
+ remember: false
159
+ });
160
+ });
161
+
162
+ it("does not issue tokens when credentials are invalid", async () => {
163
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(undefined);
164
+ const { req, res } = makeReqRes();
165
+
166
+ await loginService.login(req, res, body);
167
+
168
+ expect(revokeCurrentRefreshToken).not.toHaveBeenCalled();
169
+ expect(issueTokens).not.toHaveBeenCalled();
170
+ });
171
+ });
@@ -0,0 +1,60 @@
1
+ import type { Request, Response } from "express";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ vi.mock("@/framework/facade.js", () => ({
5
+ cookie: {
6
+ deleteAuth: vi.fn(),
7
+ deleteRefresh: vi.fn()
8
+ },
9
+ logger: {
10
+ error: vi.fn()
11
+ }
12
+ }));
13
+
14
+ vi.mock("@/modules/auth/helpers/auth.js", () => ({
15
+ revokeCurrentRefreshToken: vi.fn()
16
+ }));
17
+
18
+ import { cookie, logger } from "@/framework/facade.js";
19
+ import { revokeCurrentRefreshToken } from "@/modules/auth/helpers/auth.js";
20
+ import { logoutService } from "@/modules/auth/services/logout.js";
21
+
22
+ function makeReqRes() {
23
+ const req = {} as Request;
24
+ const res = {} as Response;
25
+ return { req, res };
26
+ }
27
+
28
+ describe("logoutService.logout (express)", () => {
29
+ beforeEach(() => vi.clearAllMocks());
30
+
31
+ it("revokes the refresh token and clears cookies", async () => {
32
+ const { req, res } = makeReqRes();
33
+
34
+ const result = await logoutService.logout(req, res);
35
+
36
+ expect(revokeCurrentRefreshToken).toHaveBeenCalledWith(req, res);
37
+ expect(cookie.deleteAuth).toHaveBeenCalledWith(res);
38
+ expect(cookie.deleteRefresh).toHaveBeenCalledWith(res);
39
+ expect(result).toEqual({
40
+ kind: "logged_out",
41
+ message: "Logged out successfully"
42
+ });
43
+ });
44
+
45
+ it("still clears cookies and returns success if revoke throws", async () => {
46
+ const err = new Error("db down");
47
+ vi.mocked(revokeCurrentRefreshToken).mockRejectedValueOnce(err);
48
+
49
+ const { req, res } = makeReqRes();
50
+ const result = await logoutService.logout(req, res);
51
+
52
+ expect(cookie.deleteAuth).toHaveBeenCalledWith(res);
53
+ expect(cookie.deleteRefresh).toHaveBeenCalledWith(res);
54
+ expect(logger.error).toHaveBeenCalledWith("failed to revoke refresh token on logout", { err });
55
+ expect(result).toEqual({
56
+ kind: "logged_out",
57
+ message: "Logged out successfully"
58
+ });
59
+ });
60
+ });
@@ -0,0 +1,49 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("drizzle-orm", () => ({
4
+ eq: vi.fn((col: unknown, val: unknown) => ({ op: "eq", col, val }))
5
+ }));
6
+
7
+ vi.mock("@/framework/facade.js", () => ({
8
+ db: {
9
+ delete: vi.fn(() => ({ where: vi.fn() }))
10
+ }
11
+ }));
12
+
13
+ vi.mock("@/modules/auth/database/models/user.js", () => ({
14
+ refreshTokens: { __table: "refreshTokens", userId: "refreshTokens.userId" }
15
+ }));
16
+
17
+ import { eq } from "drizzle-orm";
18
+ import { db } from "@/framework/facade.js";
19
+ import { refreshTokens } from "@/modules/auth/database/models/user.js";
20
+ import { logoutAllDevicesService } from "@/modules/auth/services/logoutalldevices.js";
21
+
22
+ describe("logoutAllDevicesService.logoutAllDevices", () => {
23
+ beforeEach(() => vi.clearAllMocks());
24
+
25
+ it("deletes refresh tokens for the given user", async () => {
26
+ await logoutAllDevicesService.logoutAllDevices(42);
27
+
28
+ expect(db.delete).toHaveBeenCalledWith(refreshTokens);
29
+
30
+ // The where-clause targets refreshTokens.userId with the userId value
31
+ expect(eq).toHaveBeenCalledWith(refreshTokens.userId, 42);
32
+
33
+ const whereArg = (db.delete as any).mock.results[0].value.where.mock.calls[0][0] as any;
34
+ expect(whereArg).toEqual({
35
+ op: "eq",
36
+ col: "refreshTokens.userId",
37
+ val: 42
38
+ });
39
+ });
40
+
41
+ it("returns logged_out", async () => {
42
+ const result = await logoutAllDevicesService.logoutAllDevices(42);
43
+
44
+ expect(result).toEqual({
45
+ kind: "logged_out",
46
+ message: "Logged out from all devices successfully"
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,91 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("@/framework/facade.js", () => ({
4
+ db: { query: { users: { findFirst: vi.fn() } } }
5
+ }));
6
+
7
+ // NOTE: we do NOT mock "@/modules/auth/helpers/auth.js" — we want the
8
+ // real sanitizeUser so the "no password" test is meaningful.
9
+
10
+ vi.mock("@/modules/auth/database/models/user.js", () => ({
11
+ users: { __table: "users", id: "users.id", email: "users.email" }
12
+ }));
13
+
14
+ import { db } from "@/framework/facade.js";
15
+ import { sanitizeUser } from "@/modules/auth/helpers/auth.js";
16
+ import { meService } from "@/modules/auth/services/me.js";
17
+
18
+ /** A user row as it comes from the DB — includes sensitive fields. */
19
+ const dbUser = {
20
+ id: 1,
21
+ email: "user@example.com",
22
+ name: "Jane",
23
+ password: "$2b$10$hashedpassword",
24
+ emailVerifiedAt: new Date("2025-01-01"),
25
+ roleId: 2,
26
+ createdAt: new Date("2024-01-01"),
27
+ updatedAt: new Date("2024-01-01"),
28
+ role: { id: 2, name: "user" }
29
+ };
30
+
31
+ describe("meService.me", () => {
32
+ beforeEach(() => vi.clearAllMocks());
33
+
34
+ it("returns not_found when the user does not exist", async () => {
35
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(undefined);
36
+
37
+ const result = await meService.me(999);
38
+
39
+ expect(result).toEqual({
40
+ kind: "not_found",
41
+ message: "User not found"
42
+ });
43
+ });
44
+
45
+ it("strips sensitive fields from the returned user", async () => {
46
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(dbUser as any);
47
+
48
+ const result = await meService.me(1);
49
+
50
+ if (result.kind !== "found") throw new Error("expected found");
51
+
52
+ // The real sanitizeUser must not leak the password hash.
53
+ expect(result.data).not.toHaveProperty("password");
54
+ });
55
+
56
+ it("returns exactly what sanitizeUser produces", async () => {
57
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(dbUser as any);
58
+
59
+ const result = await meService.me(1);
60
+
61
+ if (result.kind !== "found") throw new Error("expected found");
62
+
63
+ expect(result.data).toEqual(sanitizeUser(dbUser as any));
64
+ });
65
+
66
+ it("keeps the fields a client actually needs", async () => {
67
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(dbUser as any);
68
+
69
+ const result = await meService.me(1);
70
+
71
+ if (result.kind !== "found") throw new Error("expected found");
72
+
73
+ expect(result.data).toMatchObject({
74
+ id: 1,
75
+ email: "user@example.com",
76
+ name: "Jane"
77
+ });
78
+ });
79
+
80
+ it("returns the found variant with the expected message", async () => {
81
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(dbUser as any);
82
+
83
+ const result = await meService.me(1);
84
+
85
+ expect(result).toEqual({
86
+ kind: "found",
87
+ message: "Authenticated user fetched successfully",
88
+ data: expect.objectContaining({ id: 1, email: "user@example.com" })
89
+ });
90
+ });
91
+ });