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.
- package/README.md +5 -4
- 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/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
|
@@ -1,102 +1,102 @@
|
|
|
1
|
-
import { z } from "@/framework/facade.js";
|
|
2
|
-
|
|
3
|
-
export const MessageSchema = z.object({
|
|
4
|
-
message: z.string()
|
|
5
|
-
});
|
|
6
|
-
|
|
7
|
-
export const RoleSchema = z.object({
|
|
8
|
-
id: z.number(),
|
|
9
|
-
name: z.string(),
|
|
10
|
-
createdAt: z.string().nullable().optional(),
|
|
11
|
-
updatedAt: z.string().nullable().optional()
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
export const UserSchema = z.object({
|
|
15
|
-
id: z.number(),
|
|
16
|
-
name: z.string(),
|
|
17
|
-
email: z.email(),
|
|
18
|
-
roleId: z.number().nullable().optional(),
|
|
19
|
-
role: RoleSchema.nullable().optional(),
|
|
20
|
-
emailVerifiedAt: z.string().nullable().optional(),
|
|
21
|
-
createdAt: z.string().nullable().optional(),
|
|
22
|
-
updatedAt: z.string().nullable().optional()
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
export const RegisterSchema = z
|
|
26
|
-
.object({
|
|
27
|
-
name: z.string().min(2).max(100),
|
|
28
|
-
email: z.email(),
|
|
29
|
-
password: z
|
|
30
|
-
.string()
|
|
31
|
-
.min(6)
|
|
32
|
-
.max(100)
|
|
33
|
-
.regex(/[a-z]/, "Password must contain lowercase letter")
|
|
34
|
-
.regex(/[A-Z]/, "Password must contain uppercase letter")
|
|
35
|
-
.regex(/[^A-Za-z0-9]/, "Password must contain special character"),
|
|
36
|
-
password_confirmation: z.string()
|
|
37
|
-
})
|
|
38
|
-
.superRefine((data, ctx) => {
|
|
39
|
-
if (data.password !== data.password_confirmation) {
|
|
40
|
-
ctx.addIssue({
|
|
41
|
-
code: "custom",
|
|
42
|
-
path: ["password_confirmation"],
|
|
43
|
-
message: "Password confirmation does not match"
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
export const LoginSchema = z.object({
|
|
49
|
-
email: z.email(),
|
|
50
|
-
password: z.string().min(1),
|
|
51
|
-
remember: z.boolean().optional().default(false)
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
export const ForgotPasswordSchema = z.object({
|
|
55
|
-
email: z.email()
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
export const ResetPasswordSchema = z
|
|
59
|
-
.object({
|
|
60
|
-
email: z.email(),
|
|
61
|
-
token: z.string().min(1),
|
|
62
|
-
password: z
|
|
63
|
-
.string()
|
|
64
|
-
.min(6)
|
|
65
|
-
.max(100)
|
|
66
|
-
.regex(/[a-z]/, "Password must contain lowercase letter")
|
|
67
|
-
.regex(/[A-Z]/, "Password must contain uppercase letter")
|
|
68
|
-
.regex(/[^A-Za-z0-9]/, "Password must contain special character"),
|
|
69
|
-
password_confirmation: z.string()
|
|
70
|
-
})
|
|
71
|
-
.superRefine((data, ctx) => {
|
|
72
|
-
if (data.password !== data.password_confirmation) {
|
|
73
|
-
ctx.addIssue({
|
|
74
|
-
code: "custom",
|
|
75
|
-
path: ["password_confirmation"],
|
|
76
|
-
message: "Password confirmation does not match"
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
export const RefreshTokenSchema = z.object({
|
|
82
|
-
refresh_token: z.string().min(1)
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
export const VerifyEmailSchema = z.object({
|
|
86
|
-
email: z.email(),
|
|
87
|
-
token: z.string().min(1)
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
export const AuthResponseSchema = z.object({
|
|
91
|
-
message: z.string(),
|
|
92
|
-
data: z.object({
|
|
93
|
-
user: UserSchema,
|
|
94
|
-
access_token: z.string(),
|
|
95
|
-
refresh_token: z.string(),
|
|
96
|
-
token_type: z.literal("Bearer")
|
|
97
|
-
})
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
export const IdParamsSchema = z.object({
|
|
101
|
-
id: z.coerce.number()
|
|
102
|
-
});
|
|
1
|
+
import { z } from "@/framework/facade.js";
|
|
2
|
+
|
|
3
|
+
export const MessageSchema = z.object({
|
|
4
|
+
message: z.string()
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export const RoleSchema = z.object({
|
|
8
|
+
id: z.number(),
|
|
9
|
+
name: z.string(),
|
|
10
|
+
createdAt: z.string().nullable().optional(),
|
|
11
|
+
updatedAt: z.string().nullable().optional()
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export const UserSchema = z.object({
|
|
15
|
+
id: z.number(),
|
|
16
|
+
name: z.string(),
|
|
17
|
+
email: z.email(),
|
|
18
|
+
roleId: z.number().nullable().optional(),
|
|
19
|
+
role: RoleSchema.nullable().optional(),
|
|
20
|
+
emailVerifiedAt: z.string().nullable().optional(),
|
|
21
|
+
createdAt: z.string().nullable().optional(),
|
|
22
|
+
updatedAt: z.string().nullable().optional()
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const RegisterSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
name: z.string().min(2).max(100),
|
|
28
|
+
email: z.email(),
|
|
29
|
+
password: z
|
|
30
|
+
.string()
|
|
31
|
+
.min(6)
|
|
32
|
+
.max(100)
|
|
33
|
+
.regex(/[a-z]/, "Password must contain lowercase letter")
|
|
34
|
+
.regex(/[A-Z]/, "Password must contain uppercase letter")
|
|
35
|
+
.regex(/[^A-Za-z0-9]/, "Password must contain special character"),
|
|
36
|
+
password_confirmation: z.string()
|
|
37
|
+
})
|
|
38
|
+
.superRefine((data, ctx) => {
|
|
39
|
+
if (data.password !== data.password_confirmation) {
|
|
40
|
+
ctx.addIssue({
|
|
41
|
+
code: "custom",
|
|
42
|
+
path: ["password_confirmation"],
|
|
43
|
+
message: "Password confirmation does not match"
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const LoginSchema = z.object({
|
|
49
|
+
email: z.email(),
|
|
50
|
+
password: z.string().min(1),
|
|
51
|
+
remember: z.boolean().optional().default(false)
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export const ForgotPasswordSchema = z.object({
|
|
55
|
+
email: z.email()
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export const ResetPasswordSchema = z
|
|
59
|
+
.object({
|
|
60
|
+
email: z.email(),
|
|
61
|
+
token: z.string().min(1),
|
|
62
|
+
password: z
|
|
63
|
+
.string()
|
|
64
|
+
.min(6)
|
|
65
|
+
.max(100)
|
|
66
|
+
.regex(/[a-z]/, "Password must contain lowercase letter")
|
|
67
|
+
.regex(/[A-Z]/, "Password must contain uppercase letter")
|
|
68
|
+
.regex(/[^A-Za-z0-9]/, "Password must contain special character"),
|
|
69
|
+
password_confirmation: z.string()
|
|
70
|
+
})
|
|
71
|
+
.superRefine((data, ctx) => {
|
|
72
|
+
if (data.password !== data.password_confirmation) {
|
|
73
|
+
ctx.addIssue({
|
|
74
|
+
code: "custom",
|
|
75
|
+
path: ["password_confirmation"],
|
|
76
|
+
message: "Password confirmation does not match"
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const RefreshTokenSchema = z.object({
|
|
82
|
+
refresh_token: z.string().min(1)
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const VerifyEmailSchema = z.object({
|
|
86
|
+
email: z.email(),
|
|
87
|
+
token: z.string().min(1)
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
export const AuthResponseSchema = z.object({
|
|
91
|
+
message: z.string(),
|
|
92
|
+
data: z.object({
|
|
93
|
+
user: UserSchema,
|
|
94
|
+
access_token: z.string(),
|
|
95
|
+
refresh_token: z.string(),
|
|
96
|
+
token_type: z.literal("Bearer")
|
|
97
|
+
})
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export const IdParamsSchema = z.object({
|
|
101
|
+
id: z.coerce.number()
|
|
102
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { and, eq, lt } from "drizzle-orm";
|
|
2
|
+
import { db, dispatchEvent, urls } from "@/framework/facade.js";
|
|
3
|
+
import { passwordResetTokens, users } from "@/modules/auth/database/models/user.js";
|
|
4
|
+
import { hashResetToken, makeResetToken } from "@/modules/auth/helpers/auth.js";
|
|
5
|
+
import type { ForgotPasswordInput } from "@/modules/auth/types/auth.js";
|
|
6
|
+
|
|
7
|
+
export type ForgotPasswordResult = {
|
|
8
|
+
kind: "sent";
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const TOKEN_TTL_MS = 15 * 60 * 1000;
|
|
13
|
+
|
|
14
|
+
export const forgotPasswordService = {
|
|
15
|
+
forgotPassword: async (body: ForgotPasswordInput): Promise<ForgotPasswordResult> => {
|
|
16
|
+
const now = new Date();
|
|
17
|
+
|
|
18
|
+
// Cleanup + replace happen atomically so a crash mid-flow can't leave
|
|
19
|
+
// the user without a valid token.
|
|
20
|
+
const issued = await db.transaction(async (tx) => {
|
|
21
|
+
// 1. Drop this email's expired tokens (housekeeping, not critical)
|
|
22
|
+
await tx.delete(passwordResetTokens).where(and(eq(passwordResetTokens.email, body.email), lt(passwordResetTokens.expiresAt, now)));
|
|
23
|
+
|
|
24
|
+
const user = await tx.query.users.findFirst({
|
|
25
|
+
where: eq(users.email, body.email)
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!user) return null;
|
|
29
|
+
|
|
30
|
+
// 2. Revoke any existing unexpired token for this email
|
|
31
|
+
await tx.delete(passwordResetTokens).where(eq(passwordResetTokens.email, body.email));
|
|
32
|
+
|
|
33
|
+
// 3. Issue a fresh one
|
|
34
|
+
const plainToken = makeResetToken();
|
|
35
|
+
const expiresAt = new Date(now.getTime() + TOKEN_TTL_MS);
|
|
36
|
+
|
|
37
|
+
await tx.insert(passwordResetTokens).values({
|
|
38
|
+
email: user.email,
|
|
39
|
+
token: hashResetToken(plainToken),
|
|
40
|
+
expiresAt,
|
|
41
|
+
createdAt: now
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return { user, plainToken };
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Same response whether or not the user exists (no enumeration).
|
|
48
|
+
if (!issued) {
|
|
49
|
+
return { kind: "sent", message: "Reset link has been sent" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const resetUrl = urls.url(`/reset-password?token=${issued.plainToken}&email=${encodeURIComponent(issued.user.email)}`);
|
|
53
|
+
|
|
54
|
+
await dispatchEvent("user:forget-password", { email: issued.user.email, name: issued.user.name, resetUrl }, { queue: "mail" });
|
|
55
|
+
|
|
56
|
+
return { kind: "sent", message: "Reset link has been sent" };
|
|
57
|
+
}
|
|
58
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { authConfig } from "@/config/index.js";
|
|
3
|
+
import { db, password } from "@/framework/facade.js";
|
|
4
|
+
import { users } from "@/modules/auth/database/models/user.js";
|
|
5
|
+
import { issueTokens, revokeCurrentRefreshToken, sanitizeUser } from "@/modules/auth/helpers/auth.js";
|
|
6
|
+
import type { LoginInput } from "@/modules/auth/types/auth.js";
|
|
7
|
+
|
|
8
|
+
export type LoginResult =
|
|
9
|
+
| { kind: "invalid_credentials"; message: string }
|
|
10
|
+
| { kind: "email_not_verified"; message: string }
|
|
11
|
+
| {
|
|
12
|
+
kind: "logged_in";
|
|
13
|
+
message: string;
|
|
14
|
+
data: {
|
|
15
|
+
user: ReturnType<typeof sanitizeUser>;
|
|
16
|
+
access_token: string;
|
|
17
|
+
refresh_token: string;
|
|
18
|
+
token_type: "Bearer";
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const loginService = {
|
|
23
|
+
login: async (c: any, body: LoginInput): Promise<LoginResult> => {
|
|
24
|
+
const user = await db.query.users.findFirst({
|
|
25
|
+
where: eq(users.email, body.email),
|
|
26
|
+
with: { role: true }
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Generic message for both "user missing" and "wrong password" —
|
|
30
|
+
// never tell an attacker which one it was.
|
|
31
|
+
const passwordOk = user && (await password.verifyPassword(body.password, user.password));
|
|
32
|
+
|
|
33
|
+
if (!user || !passwordOk) {
|
|
34
|
+
return { kind: "invalid_credentials", message: "Invalid credentials" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (authConfig.requireEmailVerification && !user.emailVerifiedAt) {
|
|
38
|
+
return {
|
|
39
|
+
kind: "email_not_verified",
|
|
40
|
+
message: "Please verify your email before logging in"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Rotate the current refresh token before issuing a new pair.
|
|
45
|
+
// If this fails, we do NOT want to leave stale tokens around.
|
|
46
|
+
await revokeCurrentRefreshToken(c);
|
|
47
|
+
|
|
48
|
+
const tokens = await issueTokens(c, user, { remember: !!body.remember });
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
kind: "logged_in",
|
|
52
|
+
message: "User logged in successfully",
|
|
53
|
+
data: {
|
|
54
|
+
user: sanitizeUser(user),
|
|
55
|
+
access_token: tokens.accessToken,
|
|
56
|
+
refresh_token: tokens.refreshToken,
|
|
57
|
+
token_type: "Bearer"
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { cookie, logger } from "@/framework/facade.js";
|
|
2
|
+
import { revokeCurrentRefreshToken } from "@/modules/auth/helpers/auth.js";
|
|
3
|
+
|
|
4
|
+
export type LogoutResult = {
|
|
5
|
+
kind: "logged_out";
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const logoutService = {
|
|
10
|
+
logout: async (c: any): Promise<LogoutResult> => {
|
|
11
|
+
try {
|
|
12
|
+
await revokeCurrentRefreshToken(c);
|
|
13
|
+
} catch (err) {
|
|
14
|
+
// Log and continue — the user is logging out either way.
|
|
15
|
+
// The server-side token will expire on its own.
|
|
16
|
+
logger.error("failed to revoke refresh token on logout", { err });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
cookie.deleteAuth(c);
|
|
20
|
+
cookie.deleteRefresh(c);
|
|
21
|
+
|
|
22
|
+
return { kind: "logged_out", message: "Logged out successfully" };
|
|
23
|
+
}
|
|
24
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { db } from "@/framework/facade.js";
|
|
3
|
+
import { refreshTokens } from "@/modules/auth/database/models/user.js";
|
|
4
|
+
|
|
5
|
+
export type LogoutAllDevicesResult = {
|
|
6
|
+
kind: "logged_out";
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const logoutAllDevicesService = {
|
|
11
|
+
logoutAllDevices: async (userId: number): Promise<LogoutAllDevicesResult> => {
|
|
12
|
+
await db.delete(refreshTokens).where(eq(refreshTokens.userId, userId));
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
kind: "logged_out",
|
|
16
|
+
message: "Logged out from all devices successfully"
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { db } from "@/framework/facade.js";
|
|
3
|
+
import { users } from "@/modules/auth/database/models/user.js";
|
|
4
|
+
import { sanitizeUser } from "@/modules/auth/helpers/auth.js";
|
|
5
|
+
|
|
6
|
+
export type MeResult =
|
|
7
|
+
| { kind: "not_found"; message: string }
|
|
8
|
+
| {
|
|
9
|
+
kind: "found";
|
|
10
|
+
message: string;
|
|
11
|
+
data: ReturnType<typeof sanitizeUser>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const meService = {
|
|
15
|
+
me: async (userId: number): Promise<MeResult> => {
|
|
16
|
+
const user = await db.query.users.findFirst({
|
|
17
|
+
where: eq(users.id, userId),
|
|
18
|
+
with: { role: true }
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!user) {
|
|
22
|
+
return { kind: "not_found", message: "User not found" };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
kind: "found",
|
|
27
|
+
message: "Authenticated user fetched successfully",
|
|
28
|
+
data: sanitizeUser(user)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { and, eq } from "drizzle-orm";
|
|
2
|
+
import { jwtConfig } from "@/config/index.js";
|
|
3
|
+
import { db, jwt } from "@/framework/facade.js";
|
|
4
|
+
import { refreshTokens, users } from "@/modules/auth/database/models/user.js";
|
|
5
|
+
import { sanitizeUser } from "@/modules/auth/helpers/auth.js";
|
|
6
|
+
import type { RefreshTokenInput } from "@/modules/auth/types/auth.js";
|
|
7
|
+
|
|
8
|
+
export type RefreshTokenResult =
|
|
9
|
+
| { kind: "invalid_token"; message: string }
|
|
10
|
+
| { kind: "revoked"; message: string }
|
|
11
|
+
| { kind: "expired"; message: string }
|
|
12
|
+
| { kind: "user_not_found"; message: string }
|
|
13
|
+
| {
|
|
14
|
+
kind: "refreshed";
|
|
15
|
+
message: string;
|
|
16
|
+
data: {
|
|
17
|
+
user: ReturnType<typeof sanitizeUser>;
|
|
18
|
+
access_token: string;
|
|
19
|
+
refresh_token: string;
|
|
20
|
+
token_type: "Bearer";
|
|
21
|
+
};
|
|
22
|
+
cookies: {
|
|
23
|
+
accessToken: string;
|
|
24
|
+
refreshToken: string;
|
|
25
|
+
refreshExpiry?: number;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const refreshTokenService = {
|
|
30
|
+
refreshToken: async (body: RefreshTokenInput): Promise<RefreshTokenResult> => {
|
|
31
|
+
const payload = await jwt.verifyToken(body.refresh_token, "refresh");
|
|
32
|
+
|
|
33
|
+
if (!payload?.jti || typeof payload.jti !== "string") {
|
|
34
|
+
return { kind: "invalid_token", message: "Invalid refresh token" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const userId = Number(payload.id);
|
|
38
|
+
if (!Number.isInteger(userId) || userId <= 0) {
|
|
39
|
+
return { kind: "invalid_token", message: "Invalid refresh token" };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const storedToken = await db.query.refreshTokens.findFirst({
|
|
43
|
+
where: eq(refreshTokens.jti, payload.jti)
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (!storedToken || storedToken.revoked === 1) {
|
|
47
|
+
return { kind: "revoked", message: "Refresh token revoked" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (storedToken.expiresAt.getTime() < Date.now()) {
|
|
51
|
+
await db.delete(refreshTokens).where(eq(refreshTokens.id, storedToken.id));
|
|
52
|
+
return { kind: "expired", message: "Refresh token expired" };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const user = await db.query.users.findFirst({
|
|
56
|
+
where: eq(users.id, userId),
|
|
57
|
+
with: { role: true }
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (!user) {
|
|
61
|
+
return { kind: "user_not_found", message: "User not found" };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const remember = !!payload.remember;
|
|
65
|
+
const refreshExpiry = remember ? jwtConfig.refreshRememberExpirySeconds : undefined;
|
|
66
|
+
|
|
67
|
+
const tokenPayload = {
|
|
68
|
+
id: user.id,
|
|
69
|
+
email: user.email,
|
|
70
|
+
roleId: user.role?.id,
|
|
71
|
+
role: user.role?.name,
|
|
72
|
+
remember
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const accessToken = await jwt.generateToken(tokenPayload, "access");
|
|
76
|
+
const newRefreshToken = await jwt.generateToken(tokenPayload, "refresh", refreshExpiry);
|
|
77
|
+
|
|
78
|
+
// Compare-and-swap: only rotate if the jti is still the one we read.
|
|
79
|
+
// Two concurrent requests with the same old token → exactly one wins.
|
|
80
|
+
const [rotated] = await db
|
|
81
|
+
.update(refreshTokens)
|
|
82
|
+
.set({
|
|
83
|
+
jti: newRefreshToken.jti,
|
|
84
|
+
expiresAt: new Date(newRefreshToken.exp * 1000),
|
|
85
|
+
revoked: 0
|
|
86
|
+
})
|
|
87
|
+
.where(and(eq(refreshTokens.id, storedToken.id), eq(refreshTokens.jti, payload.jti)))
|
|
88
|
+
.returning();
|
|
89
|
+
|
|
90
|
+
if (!rotated) {
|
|
91
|
+
return { kind: "revoked", message: "Refresh token revoked" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
kind: "refreshed",
|
|
96
|
+
message: "Token refreshed successfully",
|
|
97
|
+
data: {
|
|
98
|
+
user: sanitizeUser(user),
|
|
99
|
+
access_token: accessToken.token,
|
|
100
|
+
refresh_token: newRefreshToken.token,
|
|
101
|
+
token_type: "Bearer"
|
|
102
|
+
},
|
|
103
|
+
cookies: {
|
|
104
|
+
accessToken: accessToken.token,
|
|
105
|
+
refreshToken: newRefreshToken.token,
|
|
106
|
+
refreshExpiry
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { authConfig } from "@/config/index.js";
|
|
3
|
+
import { db, dispatchEvent, password, urls } from "@/framework/facade.js";
|
|
4
|
+
import { roles } from "@/modules/auth/database/models/role.js";
|
|
5
|
+
import { emailVerificationTokens, users } from "@/modules/auth/database/models/user.js";
|
|
6
|
+
import {
|
|
7
|
+
hashEmailVerificationToken,
|
|
8
|
+
issueTokens,
|
|
9
|
+
makeEmailVerificationToken,
|
|
10
|
+
revokeCurrentRefreshToken,
|
|
11
|
+
sanitizeUser
|
|
12
|
+
} from "@/modules/auth/helpers/auth.js";
|
|
13
|
+
import type { RegisterInput } from "@/modules/auth/types/auth.js";
|
|
14
|
+
|
|
15
|
+
export type RegisterResult =
|
|
16
|
+
| { kind: "email_exists"; message: string }
|
|
17
|
+
| { kind: "verification_required"; message: string }
|
|
18
|
+
| {
|
|
19
|
+
kind: "registered";
|
|
20
|
+
message: string;
|
|
21
|
+
data: {
|
|
22
|
+
user: ReturnType<typeof sanitizeUser>;
|
|
23
|
+
access_token: string;
|
|
24
|
+
refresh_token: string;
|
|
25
|
+
token_type: "Bearer";
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const VERIFY_TOKEN_TTL_MS = 24 * 60 * 60 * 1000;
|
|
30
|
+
|
|
31
|
+
export const registerService = {
|
|
32
|
+
register: async (c: any, body: RegisterInput): Promise<RegisterResult> => {
|
|
33
|
+
const defaultRole = await db.query.roles.findFirst({
|
|
34
|
+
where: eq(roles.name, "user")
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (!defaultRole) {
|
|
38
|
+
throw new Error('Default role "user" not found — check your seeds');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const existingUser = await db.query.users.findFirst({
|
|
42
|
+
where: eq(users.email, body.email)
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (existingUser) {
|
|
46
|
+
return { kind: "email_exists", message: "Email already exists" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const hashedPassword = await password.hashPassword(body.password);
|
|
50
|
+
const now = new Date();
|
|
51
|
+
|
|
52
|
+
const created = await db.transaction(async (tx) => {
|
|
53
|
+
const [inserted] = await tx
|
|
54
|
+
.insert(users)
|
|
55
|
+
.values({
|
|
56
|
+
name: body.name,
|
|
57
|
+
email: body.email,
|
|
58
|
+
password: hashedPassword,
|
|
59
|
+
roleId: defaultRole.id
|
|
60
|
+
})
|
|
61
|
+
.returning({ id: users.id });
|
|
62
|
+
|
|
63
|
+
if (!inserted) throw new Error("Failed to insert user");
|
|
64
|
+
|
|
65
|
+
const user = await tx.query.users.findFirst({
|
|
66
|
+
where: eq(users.id, inserted.id),
|
|
67
|
+
with: { role: true }
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (!user) throw new Error("Inserted user not found");
|
|
71
|
+
|
|
72
|
+
let plainVerifyToken: string | null = null;
|
|
73
|
+
|
|
74
|
+
if (authConfig.requireEmailVerification) {
|
|
75
|
+
plainVerifyToken = makeEmailVerificationToken();
|
|
76
|
+
const expiresAt = new Date(now.getTime() + VERIFY_TOKEN_TTL_MS);
|
|
77
|
+
|
|
78
|
+
// One active verification token per email
|
|
79
|
+
await tx.delete(emailVerificationTokens).where(eq(emailVerificationTokens.email, user.email));
|
|
80
|
+
|
|
81
|
+
await tx.insert(emailVerificationTokens).values({
|
|
82
|
+
email: user.email,
|
|
83
|
+
token: hashEmailVerificationToken(plainVerifyToken),
|
|
84
|
+
expiresAt,
|
|
85
|
+
createdAt: now
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { user, plainVerifyToken };
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ---- Side effects AFTER commit ----
|
|
93
|
+
|
|
94
|
+
if (created.plainVerifyToken) {
|
|
95
|
+
const verifyUrl = urls.url(`/verify-email?token=${created.plainVerifyToken}&email=${encodeURIComponent(created.user.email)}`);
|
|
96
|
+
|
|
97
|
+
await dispatchEvent("user:verify-email", { email: created.user.email, name: created.user.name, verifyUrl }, { queue: "mail" });
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
kind: "verification_required",
|
|
101
|
+
message: "User registered successfully. Please verify your email before logging in."
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await revokeCurrentRefreshToken(c);
|
|
106
|
+
const tokens = await issueTokens(c, created.user, {
|
|
107
|
+
remember: !!body.remember
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
await dispatchEvent(
|
|
111
|
+
"user:signup",
|
|
112
|
+
{
|
|
113
|
+
userId: created.user.id,
|
|
114
|
+
email: created.user.email,
|
|
115
|
+
name: created.user.name
|
|
116
|
+
// NO password — see explanation above
|
|
117
|
+
},
|
|
118
|
+
{ queue: "mail" }
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
kind: "registered",
|
|
123
|
+
message: "User registered successfully",
|
|
124
|
+
data: {
|
|
125
|
+
user: sanitizeUser(created.user),
|
|
126
|
+
access_token: tokens.accessToken,
|
|
127
|
+
refresh_token: tokens.refreshToken,
|
|
128
|
+
token_type: "Bearer"
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|