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,214 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("@/framework/facade.js", () => ({
4
+ db: {
5
+ query: { users: { findFirst: vi.fn() } },
6
+ transaction: vi.fn()
7
+ },
8
+ password: { hashPassword: vi.fn() }
9
+ }));
10
+
11
+ vi.mock("@/modules/auth/helpers/auth.js", () => ({
12
+ hashResetToken: vi.fn((t: string) => `hashed:${t}`)
13
+ }));
14
+
15
+ vi.mock("@/modules/auth/database/models/user.js", () => ({
16
+ users: { __table: "users", id: "users.id", email: "users.email" },
17
+ passwordResetTokens: {
18
+ __table: "passwordResetTokens",
19
+ email: "passwordResetTokens.email",
20
+ token: "passwordResetTokens.token",
21
+ expiresAt: "passwordResetTokens.expiresAt"
22
+ },
23
+ refreshTokens: { __table: "refreshTokens", userId: "refreshTokens.userId" }
24
+ }));
25
+
26
+ import { db, password } from "@/framework/facade.js";
27
+ import { hashResetToken } from "@/modules/auth/helpers/auth.js";
28
+ import { resetPasswordService } from "@/modules/auth/services/resetpassword.js";
29
+
30
+ const fakeUser = { id: 1, email: "user@example.com", name: "Jane" };
31
+ const body = {
32
+ email: "user@example.com",
33
+ token: "plain-token",
34
+ password: "new-secret"
35
+ };
36
+
37
+ /**
38
+ * Build a fake `tx`.
39
+ * Options:
40
+ * - consumeRow: the row returned by delete().returning(), or null for "no match"
41
+ * - failOnUpdate: 1-indexed update that should reject, or undefined for none
42
+ */
43
+ function buildTx({ consumeRow = { id: 1 } as unknown, failOnUpdate }: { consumeRow?: unknown; failOnUpdate?: 1 | 2 } = {}) {
44
+ const updateCalls: { table: unknown; values: unknown; where: unknown }[] = [];
45
+
46
+ const txDeleteReturning = vi.fn((_args?: unknown) => Promise.resolve(consumeRow ? [consumeRow] : []));
47
+ const txDeleteWhere = vi.fn((_where: unknown) => ({
48
+ returning: txDeleteReturning
49
+ }));
50
+ const txDelete = vi.fn((_table: unknown) => ({ where: txDeleteWhere }));
51
+
52
+ const txUpdate = vi.fn((table: unknown) => {
53
+ const callIndex = updateCalls.length + 1;
54
+ let currentValues: unknown;
55
+
56
+ const txUpdateWhere = vi.fn((where: unknown) => {
57
+ updateCalls.push({ table, values: currentValues, where });
58
+ if (failOnUpdate === callIndex) {
59
+ return Promise.reject(new Error("constraint violation"));
60
+ }
61
+ return Promise.resolve(undefined);
62
+ });
63
+
64
+ const txUpdateSet = vi.fn((values: unknown) => {
65
+ currentValues = values;
66
+ return { where: txUpdateWhere };
67
+ });
68
+
69
+ return { set: txUpdateSet };
70
+ });
71
+
72
+ return {
73
+ tx: { delete: txDelete, update: txUpdate },
74
+ txDelete,
75
+ txDeleteWhere,
76
+ txDeleteReturning,
77
+ txUpdate,
78
+ updateCalls
79
+ };
80
+ }
81
+
82
+ describe("resetPasswordService.resetPassword", () => {
83
+ beforeEach(() => {
84
+ vi.clearAllMocks();
85
+ vi.mocked(db.query.users.findFirst).mockResolvedValue(fakeUser as any);
86
+ vi.mocked(password.hashPassword).mockResolvedValue("hashed-new-pw" as any);
87
+ });
88
+
89
+ it("returns invalid_token without touching the DB when user does not exist", async () => {
90
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(undefined);
91
+
92
+ const result = await resetPasswordService.resetPassword(body);
93
+
94
+ expect(result).toEqual({
95
+ kind: "invalid_token",
96
+ message: "Invalid or expired reset token"
97
+ });
98
+ expect(db.transaction).not.toHaveBeenCalled();
99
+ expect(password.hashPassword).not.toHaveBeenCalled();
100
+ });
101
+
102
+ it("returns invalid_token when no reset token row was consumed", async () => {
103
+ const { tx } = buildTx({ consumeRow: null });
104
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
105
+
106
+ const result = await resetPasswordService.resetPassword(body);
107
+
108
+ expect(result).toEqual({
109
+ kind: "invalid_token",
110
+ message: "Invalid or expired reset token"
111
+ });
112
+ });
113
+
114
+ it("hashes the password BEFORE opening the transaction", async () => {
115
+ const callOrder: string[] = [];
116
+
117
+ vi.mocked(password.hashPassword).mockImplementationOnce(async () => {
118
+ callOrder.push("hashPassword");
119
+ return "hashed-new-pw" as any;
120
+ });
121
+
122
+ const { tx } = buildTx();
123
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => {
124
+ callOrder.push("transaction");
125
+ return fn(tx);
126
+ });
127
+
128
+ await resetPasswordService.resetPassword(body);
129
+
130
+ expect(callOrder).toEqual(["hashPassword", "transaction"]);
131
+ });
132
+
133
+ it("consumes the token by its HASHED value inside the transaction", async () => {
134
+ const { tx, txDelete, txDeleteWhere } = buildTx();
135
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
136
+
137
+ await resetPasswordService.resetPassword(body);
138
+
139
+ expect(hashResetToken).toHaveBeenCalledWith("plain-token");
140
+ expect(txDelete).toHaveBeenCalledTimes(1);
141
+
142
+ const whereArg = txDeleteWhere.mock.calls[0][0];
143
+ const serialized = JSON.stringify(whereArg);
144
+ expect(serialized).toContain("hashed:plain-token");
145
+ expect(serialized).not.toContain('"plain-token"');
146
+ });
147
+
148
+ it("updates password + revokes sessions inside the same transaction", async () => {
149
+ const { tx, updateCalls } = buildTx();
150
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
151
+
152
+ const result = await resetPasswordService.resetPassword(body);
153
+
154
+ expect(result).toEqual({
155
+ kind: "reset",
156
+ message: "Password reset successfully"
157
+ });
158
+
159
+ expect(updateCalls).toHaveLength(2);
160
+
161
+ // 1st update: user's password, matched by id
162
+ expect((updateCalls[0].table as any).__table).toBe("users");
163
+ expect(updateCalls[0].values).toEqual({
164
+ password: "hashed-new-pw",
165
+ updatedAt: expect.any(Date)
166
+ });
167
+
168
+ // 2nd update: revoke all refresh tokens for this user
169
+ expect((updateCalls[1].table as any).__table).toBe("refreshTokens");
170
+ expect(updateCalls[1].values).toEqual({ revoked: 1 });
171
+ });
172
+
173
+ it("returns invalid_token when the transaction fails before consuming", async () => {
174
+ vi.mocked(db.transaction).mockRejectedValueOnce(new Error("db down"));
175
+
176
+ const result = await resetPasswordService.resetPassword(body);
177
+
178
+ expect(result).toEqual({
179
+ kind: "invalid_token",
180
+ message: "Invalid or expired reset token"
181
+ });
182
+ });
183
+
184
+ it("stops at the failing update — second update is never attempted", async () => {
185
+ const { tx, updateCalls } = buildTx({ failOnUpdate: 1 });
186
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
187
+
188
+ const result = await resetPasswordService.resetPassword(body);
189
+
190
+ expect(result).toEqual({
191
+ kind: "invalid_token",
192
+ message: "Invalid or expired reset token"
193
+ });
194
+
195
+ expect(updateCalls).toHaveLength(1);
196
+ expect((updateCalls[0].table as any).__table).toBe("users");
197
+ expect(updateCalls.some((c) => (c.table as any).__table === "refreshTokens")).toBe(false);
198
+ });
199
+
200
+ it("propagates failure if the 2nd update rejects (session revocation fails)", async () => {
201
+ const { tx, updateCalls } = buildTx({ failOnUpdate: 2 });
202
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
203
+
204
+ const result = await resetPasswordService.resetPassword(body);
205
+
206
+ expect(result).toEqual({
207
+ kind: "invalid_token",
208
+ message: "Invalid or expired reset token"
209
+ });
210
+
211
+ expect(updateCalls).toHaveLength(2);
212
+ expect((updateCalls[1].table as any).__table).toBe("refreshTokens");
213
+ });
214
+ });
@@ -0,0 +1,189 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ vi.mock("@/config/index.js", () => ({
4
+ authConfig: { requireEmailVerification: true }
5
+ }));
6
+
7
+ vi.mock("@/framework/facade.js", () => ({
8
+ db: {
9
+ query: { users: { findFirst: vi.fn() } },
10
+ transaction: vi.fn()
11
+ }
12
+ }));
13
+
14
+ vi.mock("@/modules/auth/helpers/auth.js", () => ({
15
+ hashEmailVerificationToken: vi.fn((t: string) => `hashed:${t}`)
16
+ }));
17
+
18
+ vi.mock("@/modules/auth/database/models/user.js", () => ({
19
+ users: { __table: "users", id: "users.id", email: "users.email" },
20
+ emailVerificationTokens: {
21
+ __table: "emailVerificationTokens",
22
+ email: "emailVerificationTokens.email",
23
+ token: "emailVerificationTokens.token",
24
+ expiresAt: "emailVerificationTokens.expiresAt"
25
+ }
26
+ }));
27
+
28
+ import { authConfig } from "@/config/index.js";
29
+ import { db } from "@/framework/facade.js";
30
+ import { hashEmailVerificationToken } from "@/modules/auth/helpers/auth.js";
31
+ import { verifyEmailService } from "@/modules/auth/services/verifyemail.js";
32
+
33
+ const fakeUser = { id: 1, email: "user@example.com", name: "Jane" };
34
+ const body = { email: "user@example.com", token: "plain-verify-token" };
35
+
36
+ /**
37
+ * Build a fake tx.
38
+ * - consumeRow: row returned by delete().returning(), or null for "no match"
39
+ * - failOnUpdate: 1-indexed update that rejects (only one update here)
40
+ */
41
+ function buildTx({ consumeRow = { id: 1 } as unknown, failOnUpdate }: { consumeRow?: unknown; failOnUpdate?: 1 } = {}) {
42
+ const updateCalls: { table: unknown; values: unknown; where: unknown }[] = [];
43
+
44
+ const txDeleteReturning = vi.fn((_args?: unknown) => Promise.resolve(consumeRow ? [consumeRow] : []));
45
+ const txDeleteWhere = vi.fn((_where: unknown) => ({
46
+ returning: txDeleteReturning
47
+ }));
48
+ const txDelete = vi.fn((_table: unknown) => ({ where: txDeleteWhere }));
49
+
50
+ const txUpdate = vi.fn((table: unknown) => {
51
+ const callIndex = updateCalls.length + 1;
52
+ let currentValues: unknown;
53
+
54
+ const txUpdateWhere = vi.fn((where: unknown) => {
55
+ updateCalls.push({ table, values: currentValues, where });
56
+ if (failOnUpdate === callIndex) {
57
+ return Promise.reject(new Error("constraint violation"));
58
+ }
59
+ return Promise.resolve(undefined);
60
+ });
61
+
62
+ const txUpdateSet = vi.fn((values: unknown) => {
63
+ currentValues = values;
64
+ return { where: txUpdateWhere };
65
+ });
66
+
67
+ return { set: txUpdateSet };
68
+ });
69
+
70
+ return {
71
+ tx: { delete: txDelete, update: txUpdate },
72
+ txDelete,
73
+ txDeleteWhere,
74
+ txDeleteReturning,
75
+ txUpdate,
76
+ updateCalls
77
+ };
78
+ }
79
+
80
+ describe("verifyEmailService.verifyEmail", () => {
81
+ beforeEach(() => {
82
+ vi.clearAllMocks();
83
+ vi.mocked(authConfig).requireEmailVerification = true;
84
+ vi.mocked(db.query.users.findFirst).mockResolvedValue(fakeUser as any);
85
+ });
86
+
87
+ it("returns not_required without touching the DB when verification is disabled", async () => {
88
+ vi.mocked(authConfig).requireEmailVerification = false;
89
+
90
+ const result = await verifyEmailService.verifyEmail(body);
91
+
92
+ expect(result).toEqual({
93
+ kind: "not_required",
94
+ message: "Email verification is not required"
95
+ });
96
+ expect(db.query.users.findFirst).not.toHaveBeenCalled();
97
+ expect(db.transaction).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it("returns invalid_token when the user does not exist", async () => {
101
+ vi.mocked(db.query.users.findFirst).mockResolvedValueOnce(undefined);
102
+
103
+ const result = await verifyEmailService.verifyEmail(body);
104
+
105
+ expect(result).toEqual({
106
+ kind: "invalid_token",
107
+ message: "Invalid or expired verification token"
108
+ });
109
+ expect(db.transaction).not.toHaveBeenCalled();
110
+ });
111
+
112
+ it("returns invalid_token when no token row was consumed", async () => {
113
+ const { tx } = buildTx({ consumeRow: null });
114
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
115
+
116
+ const result = await verifyEmailService.verifyEmail(body);
117
+
118
+ expect(result).toEqual({
119
+ kind: "invalid_token",
120
+ message: "Invalid or expired verification token"
121
+ });
122
+ });
123
+
124
+ it("consumes the token by its HASHED value inside the transaction", async () => {
125
+ const { tx, txDelete, txDeleteWhere } = buildTx();
126
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
127
+
128
+ await verifyEmailService.verifyEmail(body);
129
+
130
+ expect(hashEmailVerificationToken).toHaveBeenCalledWith("plain-verify-token");
131
+ expect(txDelete).toHaveBeenCalledTimes(1);
132
+
133
+ const whereArg = txDeleteWhere.mock.calls[0][0];
134
+ const serialized = JSON.stringify(whereArg);
135
+ expect(serialized).toContain("hashed:plain-verify-token");
136
+ expect(serialized).not.toContain('"plain-verify-token"');
137
+ });
138
+
139
+ it("marks the user verified by id (not email) inside the transaction", async () => {
140
+ const { tx, updateCalls } = buildTx();
141
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
142
+
143
+ const result = await verifyEmailService.verifyEmail(body);
144
+
145
+ expect(result).toEqual({
146
+ kind: "verified",
147
+ message: "Email verified successfully"
148
+ });
149
+
150
+ expect(updateCalls).toHaveLength(1);
151
+ expect((updateCalls[0].table as any).__table).toBe("users");
152
+
153
+ const values = updateCalls[0].values as {
154
+ emailVerifiedAt: Date;
155
+ updatedAt: Date;
156
+ };
157
+ expect(values.emailVerifiedAt).toBeInstanceOf(Date);
158
+ expect(values.updatedAt).toBeInstanceOf(Date);
159
+
160
+ // Same Date instance for both fields — proves `now` is shared
161
+ expect(values.emailVerifiedAt).toBe(values.updatedAt);
162
+ });
163
+
164
+ it("returns invalid_token when the transaction rejects", async () => {
165
+ vi.mocked(db.transaction).mockRejectedValueOnce(new Error("db down"));
166
+
167
+ const result = await verifyEmailService.verifyEmail(body);
168
+
169
+ expect(result).toEqual({
170
+ kind: "invalid_token",
171
+ message: "Invalid or expired verification token" // ← matches the service
172
+ });
173
+ });
174
+
175
+ it("returns invalid_token when the update rejects inside the transaction", async () => {
176
+ const { tx, updateCalls } = buildTx({ failOnUpdate: 1 });
177
+ vi.mocked(db.transaction).mockImplementationOnce(async (fn: any) => fn(tx));
178
+
179
+ const result = await verifyEmailService.verifyEmail(body);
180
+
181
+ expect(result).toEqual({
182
+ kind: "invalid_token",
183
+ message: "Invalid or expired verification token"
184
+ });
185
+
186
+ expect(updateCalls).toHaveLength(1);
187
+ expect((updateCalls[0].table as any).__table).toBe("users");
188
+ });
189
+ });
@@ -0,0 +1,238 @@
1
+ import type { Handler } from "hono";
2
+ import { cookie, HttpStatusCodes } from "@/framework/facade.js";
3
+ import { forgotPasswordService } from "@/modules/auth/services/forgetpassword.js";
4
+ import { loginService } from "@/modules/auth/services/login.js";
5
+ import { logoutService } from "@/modules/auth/services/logout.js";
6
+ import { meService } from "@/modules/auth/services/me.js";
7
+ import { registerService } from "@/modules/auth/services/register.js";
8
+ import { resetPasswordService } from "@/modules/auth/services/resetpassword.js";
9
+ import {
10
+ ForgotPasswordInput,
11
+ LoginInput,
12
+ RefreshTokenInput,
13
+ RegisterInput,
14
+ ResetPasswordInput,
15
+ VerifyEmailInput
16
+ } from "@/modules/auth/types/auth.js";
17
+ import { logoutAllDevicesService } from "../services/logoutalldevices.js";
18
+ import { refreshTokenService } from "../services/refreshtoken.js";
19
+ import { verifyEmailService } from "../services/verifyemail.js";
20
+
21
+ /**
22
+ * Why: Creates a new user, issues tokens, and triggers signup side effects.
23
+ * When: Used on first-time account creation.
24
+ * Where: POST auth register route.
25
+ */
26
+ export const register: Handler = async (c: any) => {
27
+ try {
28
+ const body = c.req.valid("json") as RegisterInput;
29
+ const result = await registerService.register(c, body);
30
+
31
+ switch (result.kind) {
32
+ case "email_exists":
33
+ return c.json({ message: result.message }, HttpStatusCodes.UNPROCESSABLE_ENTITY);
34
+
35
+ case "verification_required":
36
+ return c.json({ message: result.message }, HttpStatusCodes.CREATED);
37
+
38
+ case "registered":
39
+ return c.json({ message: result.message, data: result.data }, HttpStatusCodes.CREATED);
40
+ }
41
+ } catch (error) {
42
+ console.error("Register error:", error);
43
+ return c.json({ message: "Failed to register user" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
44
+ }
45
+ };
46
+
47
+ /**
48
+ * Why: Authenticates user credentials and rotates active login cookies/tokens.
49
+ * When: Used whenever a user signs in.
50
+ * Where: POST auth login route.
51
+ */
52
+ export const login: Handler = async (c: any) => {
53
+ try {
54
+ const body = c.req.valid("json") as LoginInput;
55
+ const result = await loginService.login(c, body);
56
+
57
+ switch (result.kind) {
58
+ case "invalid_credentials":
59
+ return c.json({ message: result.message }, HttpStatusCodes.UNAUTHORIZED);
60
+
61
+ case "email_not_verified":
62
+ return c.json({ message: result.message }, HttpStatusCodes.FORBIDDEN);
63
+
64
+ case "logged_in":
65
+ return c.json({ message: result.message, data: result.data }, HttpStatusCodes.OK);
66
+ }
67
+ } catch (error) {
68
+ console.error("Login error:", error);
69
+ return c.json({ message: "Failed to login" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
70
+ }
71
+ };
72
+
73
+ /**
74
+ * Why: Returns the currently authenticated user profile.
75
+ * When: Used by clients to bootstrap session/user state.
76
+ * Where: GET auth me route.
77
+ */
78
+ export const me: Handler = async (c: any) => {
79
+ try {
80
+ const auth = c.get("auth");
81
+ const result = await meService.me(auth.id);
82
+
83
+ switch (result.kind) {
84
+ case "not_found":
85
+ return c.json({ message: result.message }, HttpStatusCodes.NOT_FOUND);
86
+
87
+ case "found":
88
+ return c.json({ message: result.message, data: result.data }, HttpStatusCodes.OK);
89
+ }
90
+ } catch (error) {
91
+ console.error("Me error:", error);
92
+ return c.json({ message: "Failed to fetch user" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
93
+ }
94
+ };
95
+
96
+ /**
97
+ * Why: Revokes current refresh token and clears auth cookies.
98
+ * When: Used when the active device logs out.
99
+ * Where: POST auth logout route.
100
+ */
101
+ export const logout: Handler = async (c: any) => {
102
+ try {
103
+ const result = await logoutService.logout(c);
104
+
105
+ return c.json({ message: result.message }, HttpStatusCodes.OK);
106
+ } catch (error) {
107
+ console.error("Logout error:", error);
108
+
109
+ // always clear cookies, even if revocation failed
110
+ cookie.deleteAuth(c);
111
+ cookie.deleteRefresh(c);
112
+
113
+ return c.json({ message: "Failed to logout" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
114
+ }
115
+ };
116
+
117
+ /**
118
+ * Why: Starts password reset flow and queues email delivery event.
119
+ * When: Used when user requests a forgot-password link.
120
+ * Where: POST auth forgot-password route.
121
+ */
122
+ export const forgotPassword: Handler = async (c: any) => {
123
+ try {
124
+ const body = c.req.valid("json") as ForgotPasswordInput;
125
+ const result = await forgotPasswordService.forgotPassword(body);
126
+
127
+ // Single success path — intentionally identical whether or not the email exists.
128
+ return c.json({ message: result.message }, HttpStatusCodes.OK);
129
+ } catch (error) {
130
+ console.error("Forgot password error:", error);
131
+ return c.json({ message: "Failed to process forgot password request" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
132
+ }
133
+ };
134
+
135
+ /**
136
+ * Why: Verifies reset token and persists the new password.
137
+ * When: Used after user submits reset token + new password.
138
+ * Where: POST auth reset-password route.
139
+ */
140
+ export const resetPassword: Handler = async (c: any) => {
141
+ try {
142
+ const body = c.req.valid("json") as ResetPasswordInput;
143
+ const result = await resetPasswordService.resetPassword(body);
144
+
145
+ switch (result.kind) {
146
+ case "invalid_token":
147
+ return c.json({ message: result.message }, HttpStatusCodes.UNPROCESSABLE_ENTITY);
148
+
149
+ case "reset":
150
+ return c.json({ message: result.message }, HttpStatusCodes.OK);
151
+ }
152
+ } catch (error) {
153
+ console.error("Reset password error:", error);
154
+ return c.json({ message: "Failed to reset password" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
155
+ }
156
+ };
157
+
158
+ /**
159
+ * Why: Validates email verification token and marks user as verified.
160
+ * When: Used when user opens verification link from inbox.
161
+ * Where: POST auth verify-email route.
162
+ */
163
+ export const verifyEmail: Handler = async (c: any) => {
164
+ try {
165
+ const body = c.req.valid("json") as VerifyEmailInput;
166
+ const result = await verifyEmailService.verifyEmail(body);
167
+
168
+ switch (result.kind) {
169
+ case "not_required":
170
+ return c.json({ message: result.message }, HttpStatusCodes.OK);
171
+
172
+ case "invalid_token":
173
+ return c.json({ message: result.message }, HttpStatusCodes.UNPROCESSABLE_ENTITY);
174
+
175
+ case "verified":
176
+ return c.json({ message: result.message }, HttpStatusCodes.OK);
177
+ }
178
+ } catch (error) {
179
+ console.error("Verify email error:", error);
180
+ return c.json({ message: "Failed to verify email" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
181
+ }
182
+ };
183
+
184
+ /**
185
+ * Why: Rotates refresh token and reissues access credentials.
186
+ * When: Used when access token expires but refresh token is still valid.
187
+ * Where: POST auth refresh-token route.
188
+ */
189
+ export const refreshToken: Handler = async (c: any) => {
190
+ try {
191
+ const body = c.req.valid("json") as RefreshTokenInput;
192
+ const result = await refreshTokenService.refreshToken(body);
193
+
194
+ switch (result.kind) {
195
+ case "invalid_token":
196
+ case "revoked":
197
+ case "expired":
198
+ case "user_not_found":
199
+ return c.json({ message: result.message }, HttpStatusCodes.UNAUTHORIZED);
200
+
201
+ case "refreshed": {
202
+ // Cookie handling stays in the controller — it's an HTTP concern.
203
+ await cookie.setAuth(c, result.cookies.accessToken);
204
+ await cookie.setRefresh(c, result.cookies.refreshToken, result.cookies.refreshExpiry);
205
+
206
+ return c.json({ message: result.message, data: result.data }, HttpStatusCodes.OK);
207
+ }
208
+ }
209
+ } catch (error) {
210
+ console.error("Refresh token error:", error);
211
+ return c.json({ message: "Invalid or expired refresh token" }, HttpStatusCodes.UNAUTHORIZED);
212
+ }
213
+ };
214
+
215
+ /**
216
+ * Why: Revokes all refresh tokens for account-wide logout.
217
+ * When: Used for "logout from all devices" security action.
218
+ * Where: POST auth logout-all-devices route.
219
+ */
220
+ export const logoutAllDevices: Handler = async (c: any) => {
221
+ try {
222
+ const auth = c.get("auth");
223
+ if (!auth) {
224
+ return c.json({ message: "Unauthorized" }, HttpStatusCodes.UNAUTHORIZED);
225
+ }
226
+
227
+ const result = await logoutAllDevicesService.logoutAllDevices(auth.id);
228
+
229
+ // Cookies are an HTTP concern — kept out of the service.
230
+ cookie.deleteAuth(c);
231
+ cookie.deleteRefresh(c);
232
+
233
+ return c.json({ message: result.message }, HttpStatusCodes.OK);
234
+ } catch (error) {
235
+ console.error("Logout all devices error:", error);
236
+ return c.json({ message: "Failed to logout from all devices" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
237
+ }
238
+ };
@@ -1,25 +1,25 @@
1
- import { eq } from "drizzle-orm";
2
- import type { Handler } from "hono";
3
- import { db, HttpStatusCodes } from "@/framework/facade.js";
4
- import { roles } from "@/modules/auth/database/models/role.js";
5
-
6
- /**
7
- * Why: Returns role records for role-aware UI and authorization setup.
8
- * When: Used by admin/management screens that need available roles.
9
- * Where: Mounted under auth role routes.
10
- */
11
- export const index: Handler = async (c: any) => {
12
- const result = await db.query.roles.findMany();
13
- return c.json({ message: "Success", data: result }, HttpStatusCodes.OK);
14
- };
15
-
16
- /**
17
- * Why: Fetches a single role to inspect role metadata by id.
18
- * When: Used for detail views or role validation checks.
19
- * Where: Mounted on auth role detail route with path param id.
20
- */
21
- export const show: Handler = async (c: any) => {
22
- const { id } = c.req.valid("param");
23
- const result = await db.query.roles.findFirst({ where: eq(roles.id, id) });
24
- return c.json({ message: "Success", data: result }, HttpStatusCodes.OK);
25
- };
1
+ import { eq } from "drizzle-orm";
2
+ import type { Handler } from "hono";
3
+ import { db, HttpStatusCodes } from "@/framework/facade.js";
4
+ import { roles } from "@/modules/auth/database/models/role.js";
5
+
6
+ /**
7
+ * Why: Returns role records for role-aware UI and authorization setup.
8
+ * When: Used by admin/management screens that need available roles.
9
+ * Where: Mounted under auth role routes.
10
+ */
11
+ export const index: Handler = async (c: any) => {
12
+ const result = await db.query.roles.findMany();
13
+ return c.json({ message: "Success", data: result }, HttpStatusCodes.OK);
14
+ };
15
+
16
+ /**
17
+ * Why: Fetches a single role to inspect role metadata by id.
18
+ * When: Used for detail views or role validation checks.
19
+ * Where: Mounted on auth role detail route with path param id.
20
+ */
21
+ export const show: Handler = async (c: any) => {
22
+ const { id } = c.req.valid("param");
23
+ const result = await db.query.roles.findFirst({ where: eq(roles.id, id) });
24
+ return c.json({ message: "Success", data: result }, HttpStatusCodes.OK);
25
+ };
@@ -21,4 +21,4 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
21
21
  fields: [notifications.userId],
22
22
  references: [users.id]
23
23
  })
24
- }));
24
+ }));