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