create-nexgen 1.0.4
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/package.json +26 -0
- package/src/index.js +108 -0
- package/template/.dockerignore +14 -0
- package/template/.env +58 -0
- package/template/.env.example +59 -0
- package/template/.prettierignore +5 -0
- package/template/.prettierrc +8 -0
- package/template/README.md +447 -0
- package/template/drizzle.config.ts +29 -0
- package/template/eslint.config.js +52 -0
- package/template/gitignore-stub +24 -0
- package/template/package.json +96 -0
- package/template/public/assets/AuthLayout-CbswhpjJ.js +1 -0
- package/template/public/assets/Button-_7aQ7gHL.js +1 -0
- package/template/public/assets/Input-CLNJXmKc.css +1 -0
- package/template/public/assets/Input-z8GI8Aqo.js +1 -0
- package/template/public/assets/InputPasswordToggle-BxlzVGp3.js +1 -0
- package/template/public/assets/InputPasswordToggle-C77FI9Eg.css +1 -0
- package/template/public/assets/Layout-DotR1sQC.js +1 -0
- package/template/public/assets/Refresh-BdqsPPBC.js +1 -0
- package/template/public/assets/admin-ui-CU34rLdN.js +1 -0
- package/template/public/assets/bootstrap-icons-BeopsB42.woff +0 -0
- package/template/public/assets/bootstrap-icons-mSm7cUeB.woff2 +0 -0
- package/template/public/assets/dashboard-CwybEyLc.js +1 -0
- package/template/public/assets/dashboard-Dc4d-Pi7.css +1 -0
- package/template/public/assets/forgetPassword-CKEJaXsq.js +1 -0
- package/template/public/assets/index-Bleyx5dm.js +64 -0
- package/template/public/assets/index-DUw8E6Yg.css +1 -0
- package/template/public/assets/login-DC7PTlQF.js +1 -0
- package/template/public/assets/realtime-test-BPQdrFym.css +1 -0
- package/template/public/assets/realtime-test-tQZ0rBEJ.js +1 -0
- package/template/public/assets/register-3O7Qs28C.js +1 -0
- package/template/public/assets/resetPassword-A5AzMWKs.js +1 -0
- package/template/public/assets/verifyEmail-DDBEQHOv.js +1 -0
- package/template/public/index.html +17 -0
- package/template/src/database/migrations/mysql/0000_init.sql +73 -0
- package/template/src/database/migrations/mysql/meta/0000_snapshot.json +484 -0
- package/template/src/database/migrations/mysql/meta/_journal.json +13 -0
- package/template/src/database/schema.ts +4 -0
- package/template/src/env.ts +107 -0
- package/template/src/framework/cache/cache.ts +81 -0
- package/template/src/framework/database/connection.ts +168 -0
- package/template/src/framework/database/optional-db-drivers.d.ts +9 -0
- package/template/src/framework/database/paginate.ts +200 -0
- package/template/src/framework/database/schema.ts +26 -0
- package/template/src/framework/database/seed.ts +33 -0
- package/template/src/framework/events/dispatcher.ts +57 -0
- package/template/src/framework/facade.ts +27 -0
- package/template/src/framework/http/app.ts +61 -0
- package/template/src/framework/http/cors.ts +19 -0
- package/template/src/framework/http/logger.ts +85 -0
- package/template/src/framework/http/openapi.ts +34 -0
- package/template/src/framework/http/ratelimiter.ts +13 -0
- package/template/src/framework/http/router.ts +76 -0
- package/template/src/framework/http/static.ts +33 -0
- package/template/src/framework/http/validation.ts +24 -0
- package/template/src/framework/kernel.ts +40 -0
- package/template/src/framework/maker-cli/src/index.mjs +51 -0
- package/template/src/framework/maker-cli/src/levels/level-1/env-db.mjs +57 -0
- package/template/src/framework/maker-cli/src/levels/level-1/file-ops.mjs +30 -0
- package/template/src/framework/maker-cli/src/levels/level-1/flags.mjs +16 -0
- package/template/src/framework/maker-cli/src/levels/level-1/help.mjs +24 -0
- package/template/src/framework/maker-cli/src/levels/level-1/naming.mjs +13 -0
- package/template/src/framework/maker-cli/src/levels/level-1/process.mjs +47 -0
- package/template/src/framework/maker-cli/src/levels/level-2/db/core.mjs +299 -0
- package/template/src/framework/maker-cli/src/levels/level-2/db/index.mjs +177 -0
- package/template/src/framework/maker-cli/src/levels/level-2/deploy/core.mjs +635 -0
- package/template/src/framework/maker-cli/src/levels/level-2/deploy/index.mjs +145 -0
- package/template/src/framework/maker-cli/src/levels/level-2/module/core.mjs +707 -0
- package/template/src/framework/maker-cli/src/levels/level-2/module/index.mjs +116 -0
- package/template/src/framework/maker-cli/src/levels/level-2/runtime/build-frontend.mjs +16 -0
- package/template/src/framework/maker-cli/src/levels/level-2/runtime/core.mjs +311 -0
- package/template/src/framework/maker-cli/src/levels/level-2/runtime/index.mjs +71 -0
- package/template/src/framework/maker-cli/stubs/controller/openapi.ts.stub +55 -0
- package/template/src/framework/maker-cli/stubs/controller/openapi.with-model.ts.stub +56 -0
- package/template/src/framework/maker-cli/stubs/controller/plain.ts.stub +57 -0
- package/template/src/framework/maker-cli/stubs/controller/schema.plain.ts.stub +13 -0
- package/template/src/framework/maker-cli/stubs/controller/schema.ts.stub +32 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.bun.stub +49 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.pnpm.stub +53 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.stub +49 -0
- package/template/src/framework/maker-cli/stubs/deploy/Dockerfile.yarn.stub +53 -0
- package/template/src/framework/maker-cli/stubs/deploy/README.stub +55 -0
- package/template/src/framework/maker-cli/stubs/deploy/compose/mysql.server.stub +29 -0
- package/template/src/framework/maker-cli/stubs/deploy/compose/postgres.server.stub +29 -0
- package/template/src/framework/maker-cli/stubs/deploy/compose/sqlite.stub +29 -0
- package/template/src/framework/maker-cli/stubs/deploy/env/mysql.server.stub +73 -0
- package/template/src/framework/maker-cli/stubs/deploy/env/postgres.server.stub +73 -0
- package/template/src/framework/maker-cli/stubs/deploy/env/sqlite.stub +72 -0
- package/template/src/framework/maker-cli/stubs/deploy/scripts/auto-migrate.sh.stub +15 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/README.stub +77 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/compose/noredis.stub +118 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.dev.stub +131 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/compose/redis.stub +129 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/env/local.example.stub +10 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/env/noredis.stub +24 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/env/redis.stub +24 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/README.stub +15 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/app.example.com.stub +12 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/pgadmin/servers.stub +13 -0
- package/template/src/framework/maker-cli/stubs/deploy/server/redis/redis.conf.stub +6 -0
- package/template/src/framework/maker-cli/stubs/deploy/supervisor/noredis.stub +53 -0
- package/template/src/framework/maker-cli/stubs/deploy/supervisor/redis.stub +69 -0
- package/template/src/framework/maker-cli/stubs/deploy/workflow/local.json.stub +24 -0
- package/template/src/framework/maker-cli/stubs/deploy/workflow/remote.json.stub +20 -0
- package/template/src/framework/maker-cli/stubs/example/console.ts.stub +33 -0
- package/template/src/framework/maker-cli/stubs/example/controller.ts.stub +503 -0
- package/template/src/framework/maker-cli/stubs/example/job.ts.stub +74 -0
- package/template/src/framework/maker-cli/stubs/example/route.api.ts.stub +206 -0
- package/template/src/framework/maker-cli/stubs/example/schema.ts.stub +41 -0
- package/template/src/framework/maker-cli/stubs/job/name.ts.stub +24 -0
- package/template/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +8 -0
- package/template/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +8 -0
- package/template/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +8 -0
- package/template/src/framework/maker-cli/stubs/notification/NotificationBell.vue.stub +218 -0
- package/template/src/framework/maker-cli/stubs/notification/controller.ts.stub +85 -0
- package/template/src/framework/maker-cli/stubs/notification/index.vue.stub +211 -0
- package/template/src/framework/maker-cli/stubs/notification/job.ts.stub +12 -0
- package/template/src/framework/maker-cli/stubs/notification/route.api.ts.stub +49 -0
- package/template/src/framework/maker-cli/stubs/notification/schema.ts.stub +25 -0
- package/template/src/framework/maker-cli/stubs/route/api.ts.stub +79 -0
- package/template/src/framework/maker-cli/stubs/route/plain.ts.stub +10 -0
- package/template/src/framework/maker-cli/stubs/schedule/name.ts.stub +35 -0
- package/template/src/framework/maker-cli/stubs/seeder/name.ts.stub +17 -0
- package/template/src/framework/modules/discover.ts +54 -0
- package/template/src/framework/modules/routes.ts +26 -0
- package/template/src/framework/notification/index.ts +109 -0
- package/template/src/framework/queue/clear.ts +20 -0
- package/template/src/framework/queue/queue.ts +213 -0
- package/template/src/framework/queue/ui.ts +104 -0
- package/template/src/framework/queue/worker.ts +33 -0
- package/template/src/framework/realtime/broadcast.ts +27 -0
- package/template/src/framework/realtime/index.ts +1 -0
- package/template/src/framework/realtime/socket-cookie.ts +65 -0
- package/template/src/framework/realtime/socket.ts +132 -0
- package/template/src/framework/realtime/types.ts +6 -0
- package/template/src/framework/realtime/ui.ts +16 -0
- package/template/src/framework/redis/client.ts +126 -0
- package/template/src/framework/scheduler/lock.ts +124 -0
- package/template/src/framework/scheduler/run.ts +26 -0
- package/template/src/framework/scheduler/scheduler.ts +82 -0
- package/template/src/framework/server.ts +147 -0
- package/template/src/framework/session/session.ts +116 -0
- package/template/src/framework/storage/storage.ts +743 -0
- package/template/src/framework/support/cookie.ts +78 -0
- package/template/src/framework/support/jwt.ts +45 -0
- package/template/src/framework/support/lifecycle.ts +35 -0
- package/template/src/framework/support/logger.ts +102 -0
- package/template/src/framework/support/mail.ts +43 -0
- package/template/src/framework/support/password.ts +23 -0
- package/template/src/framework/support/url.ts +25 -0
- package/template/src/middlewares/auth-middleware.ts +98 -0
- package/template/src/middlewares/role-middleware.ts +24 -0
- package/template/src/modules/auth/controllers/auth.controller.ts +445 -0
- package/template/src/modules/auth/controllers/auth.helpers.ts +110 -0
- package/template/src/modules/auth/controllers/auth.schema.ts +102 -0
- package/template/src/modules/auth/controllers/role.controller.ts +25 -0
- package/template/src/modules/auth/database/models/notifications.ts +22 -0
- package/template/src/modules/auth/database/models/role.ts +14 -0
- package/template/src/modules/auth/database/models/user.ts +46 -0
- package/template/src/modules/auth/database/seeders/role.ts +19 -0
- package/template/src/modules/auth/database/seeders/user.ts +33 -0
- package/template/src/modules/auth/jobs/forgetpass.ts +18 -0
- package/template/src/modules/auth/jobs/registeruser.ts +31 -0
- package/template/src/modules/auth/jobs/verifyemail.ts +18 -0
- package/template/src/modules/auth/routes/api.ts +151 -0
- package/template/src/modules/auth/routes/role.ts +39 -0
- package/template/src/modules/welcome/controllers/welcome.controller.ts +14 -0
- package/template/src/modules/welcome/controllers/welcome.schema.ts +6 -0
- package/template/src/modules/welcome/database/models/welcome.ts +6 -0
- package/template/src/modules/welcome/routes/api.ts +20 -0
- package/template/src/resources/index.html +16 -0
- package/template/src/resources/src/App.vue +5 -0
- package/template/src/resources/src/assets/css/styles.css +14934 -0
- package/template/src/resources/src/assets/css/styles.css.map +1 -0
- package/template/src/resources/src/assets/images/favicon/favicon.ico +0 -0
- package/template/src/resources/src/assets/images/favicon/favicon1.ico +0 -0
- package/template/src/resources/src/assets/images/logo-1.png +0 -0
- package/template/src/resources/src/assets/images/logo-dark-sm.png +0 -0
- package/template/src/resources/src/assets/images/logo-dark.png +0 -0
- package/template/src/resources/src/assets/images/logo-dark1.png +0 -0
- package/template/src/resources/src/assets/images/logo-sm.png +0 -0
- package/template/src/resources/src/assets/images/logo1.png +0 -0
- package/template/src/resources/src/assets/images/logo2.png +0 -0
- package/template/src/resources/src/assets/scss/custom.css +217 -0
- package/template/src/resources/src/assets/scss/custom.css.map +1 -0
- package/template/src/resources/src/assets/scss/custom.scss +1100 -0
- package/template/src/resources/src/components/Button.vue +35 -0
- package/template/src/resources/src/components/Checkbox.vue +29 -0
- package/template/src/resources/src/components/FloatButton.vue +36 -0
- package/template/src/resources/src/components/Href.vue +32 -0
- package/template/src/resources/src/components/Input.vue +227 -0
- package/template/src/resources/src/components/InputGroup.vue +153 -0
- package/template/src/resources/src/components/InputPasswordToggle.vue +226 -0
- package/template/src/resources/src/components/Modal.vue +102 -0
- package/template/src/resources/src/components/Pagebar.vue +28 -0
- package/template/src/resources/src/components/Refresh.vue +26 -0
- package/template/src/resources/src/components/Select.vue +390 -0
- package/template/src/resources/src/components/Spinner.vue +42 -0
- package/template/src/resources/src/components/Switch.vue +65 -0
- package/template/src/resources/src/components/TextArea.vue +121 -0
- package/template/src/resources/src/components/Toast.vue +56 -0
- package/template/src/resources/src/components/datatable/DataTableSkeleton.vue +99 -0
- package/template/src/resources/src/components/datatable/Pagination.vue +161 -0
- package/template/src/resources/src/components/datatable/SelectOpption.vue +54 -0
- package/template/src/resources/src/components/datatable/index.vue +237 -0
- package/template/src/resources/src/composables/useAuth.ts +52 -0
- package/template/src/resources/src/composables/useBrowserDetect.ts +5 -0
- package/template/src/resources/src/composables/useDialog.ts +5 -0
- package/template/src/resources/src/composables/useGum.ts +3 -0
- package/template/src/resources/src/composables/usePulse.ts +5 -0
- package/template/src/resources/src/env.d.ts +20 -0
- package/template/src/resources/src/helpers/nformatter.ts +10 -0
- package/template/src/resources/src/helpers/utils.ts +68 -0
- package/template/src/resources/src/layouts/AuthLayout.vue +20 -0
- package/template/src/resources/src/layouts/Layout/Footer.vue +23 -0
- package/template/src/resources/src/layouts/Layout/Header.vue +90 -0
- package/template/src/resources/src/layouts/Layout/Sidebar.vue +137 -0
- package/template/src/resources/src/layouts/Layout/index.vue +76 -0
- package/template/src/resources/src/main.ts +27 -0
- package/template/src/resources/src/pages/auth/forgetPassword.vue +76 -0
- package/template/src/resources/src/pages/auth/login.vue +93 -0
- package/template/src/resources/src/pages/auth/register.vue +130 -0
- package/template/src/resources/src/pages/auth/resetPassword.vue +119 -0
- package/template/src/resources/src/pages/auth/verifyEmail.vue +60 -0
- package/template/src/resources/src/pages/dashboard/index.vue +76 -0
- package/template/src/resources/src/plugins/axios.ts +33 -0
- package/template/src/resources/src/plugins/browserDetect.ts +55 -0
- package/template/src/resources/src/plugins/dialog.ts +167 -0
- package/template/src/resources/src/plugins/gum.ts +343 -0
- package/template/src/resources/src/plugins/pulse.ts +141 -0
- package/template/src/resources/src/plugins/routeProgress.ts +87 -0
- package/template/src/resources/src/router/index.ts +85 -0
- package/template/src/resources/src/stores/admin-ui.ts +148 -0
- package/template/src/resources/src/stores/auth.ts +151 -0
- package/template/src/resources/tsconfig.json +19 -0
- package/template/src/resources/vite.config.ts +43 -0
- package/template/src/storage/logs/app.log +20179 -0
- package/template/src/storage/logs/fatal.log +727 -0
- package/template/tsconfig.json +20 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import type { Handler } from "hono";
|
|
2
|
+
import { and, eq, gt, lt } from "drizzle-orm";
|
|
3
|
+
import { env } from "@/env.js";
|
|
4
|
+
import { HttpStatusCodes, cookie, db, dispatchEvent, jwt, password, urls } from "@/framework/facade.js";
|
|
5
|
+
import { roles } from "@/modules/auth/database/models/role.js";
|
|
6
|
+
import { emailVerificationTokens, passwordResetTokens, refreshTokens, users } from "@/modules/auth/database/models/user.js";
|
|
7
|
+
import { hashEmailVerificationToken, hashResetToken, issueTokens, makeEmailVerificationToken, makeResetToken, revokeCurrentRefreshToken, sanitizeUser } from "./auth.helpers.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Why: Creates a new user, issues tokens, and triggers signup side effects.
|
|
11
|
+
* When: Used on first-time account creation.
|
|
12
|
+
* Where: POST auth register route.
|
|
13
|
+
*/
|
|
14
|
+
export const register: Handler = async (c: any) => {
|
|
15
|
+
try {
|
|
16
|
+
const body = c.req.valid("json");
|
|
17
|
+
const defaultRole = await db.query.roles.findFirst({
|
|
18
|
+
where: eq(roles.name, "user")
|
|
19
|
+
});
|
|
20
|
+
const existingUser = await db.query.users.findFirst({
|
|
21
|
+
where: eq(users.email, body.email)
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (existingUser) {
|
|
25
|
+
return c.json({ message: "Email already exists" }, HttpStatusCodes.UNPROCESSABLE_ENTITY);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const insertResult = await db.insert(users).values({
|
|
29
|
+
name: body.name,
|
|
30
|
+
email: body.email,
|
|
31
|
+
password: await password.hashPassword(body.password),
|
|
32
|
+
roleId: defaultRole?.id ?? null
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const insertedId = Number((insertResult as any)[0]?.insertId ?? (insertResult as any).insertId);
|
|
36
|
+
if (!insertedId) {
|
|
37
|
+
throw new Error("Failed to resolve inserted user id");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const user = await db.query.users.findFirst({
|
|
41
|
+
where: eq(users.id, insertedId),
|
|
42
|
+
with: { role: true }
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
if (!user) throw new Error("Inserted user not found");
|
|
46
|
+
|
|
47
|
+
if (env.AUTH_REQUIRE_EMAIL_VERIFICATION) {
|
|
48
|
+
const plainToken = makeEmailVerificationToken();
|
|
49
|
+
const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
|
|
50
|
+
|
|
51
|
+
await db.delete(emailVerificationTokens).where(eq(emailVerificationTokens.email, user.email));
|
|
52
|
+
await db.insert(emailVerificationTokens).values({
|
|
53
|
+
email: user.email,
|
|
54
|
+
token: hashEmailVerificationToken(plainToken),
|
|
55
|
+
expiresAt,
|
|
56
|
+
createdAt: new Date()
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const verifyUrl = urls.url(`/verify-email?token=${plainToken}&email=${encodeURIComponent(user.email)}`);
|
|
60
|
+
await dispatchEvent(
|
|
61
|
+
"user:verify-email",
|
|
62
|
+
{ email: user.email, name: user.name, verifyUrl },
|
|
63
|
+
{ queue: "mail" }
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return c.json({ message: "User registered successfully. Please verify your email before logging in." }, HttpStatusCodes.CREATED);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
await revokeCurrentRefreshToken(c);
|
|
70
|
+
const tokens = await issueTokens(c, user, { remember: !!body.remember });
|
|
71
|
+
await dispatchEvent(
|
|
72
|
+
"user:signup",
|
|
73
|
+
{
|
|
74
|
+
userId: user.id,
|
|
75
|
+
email: user.email,
|
|
76
|
+
name: user.name,
|
|
77
|
+
password: body.password
|
|
78
|
+
},
|
|
79
|
+
{ queue: "mail" }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
return c.json(
|
|
83
|
+
{
|
|
84
|
+
message: "User registered successfully",
|
|
85
|
+
data: {
|
|
86
|
+
user: sanitizeUser(user),
|
|
87
|
+
access_token: tokens.accessToken,
|
|
88
|
+
refresh_token: tokens.refreshToken,
|
|
89
|
+
token_type: "Bearer"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
HttpStatusCodes.CREATED
|
|
93
|
+
);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error("Register error:", error);
|
|
96
|
+
return c.json({ message: "Failed to register user" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Why: Authenticates user credentials and rotates active login cookies/tokens.
|
|
102
|
+
* When: Used whenever a user signs in.
|
|
103
|
+
* Where: POST auth login route.
|
|
104
|
+
*/
|
|
105
|
+
export const login: Handler = async (c: any) => {
|
|
106
|
+
try {
|
|
107
|
+
const body = c.req.valid("json");
|
|
108
|
+
const user = await db.query.users.findFirst({
|
|
109
|
+
where: eq(users.email, body.email),
|
|
110
|
+
with: { role: true }
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!user || !(await password.verifyPassword(body.password, user.password))) {
|
|
114
|
+
return c.json({ message: "Invalid credentials" }, HttpStatusCodes.UNAUTHORIZED);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (env.AUTH_REQUIRE_EMAIL_VERIFICATION && !user.emailVerifiedAt) {
|
|
118
|
+
return c.json(
|
|
119
|
+
{ message: "Please verify your email before logging in" },
|
|
120
|
+
HttpStatusCodes.FORBIDDEN
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await revokeCurrentRefreshToken(c);
|
|
125
|
+
const tokens = await issueTokens(c, user);
|
|
126
|
+
|
|
127
|
+
return c.json(
|
|
128
|
+
{
|
|
129
|
+
message: "User logged in successfully",
|
|
130
|
+
data: {
|
|
131
|
+
user: sanitizeUser(user),
|
|
132
|
+
access_token: tokens.accessToken,
|
|
133
|
+
refresh_token: tokens.refreshToken,
|
|
134
|
+
token_type: "Bearer"
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
HttpStatusCodes.OK
|
|
138
|
+
);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error("Login error:", error);
|
|
141
|
+
return c.json({ message: "Failed to login" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Why: Returns the currently authenticated user profile.
|
|
147
|
+
* When: Used by clients to bootstrap session/user state.
|
|
148
|
+
* Where: GET auth me route.
|
|
149
|
+
*/
|
|
150
|
+
export const me: Handler = async (c: any) => {
|
|
151
|
+
try {
|
|
152
|
+
const auth = c.get("auth");
|
|
153
|
+
const user = await db.query.users.findFirst({
|
|
154
|
+
where: eq(users.id, auth.id),
|
|
155
|
+
with: { role: true }
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!user) return c.json({ message: "User not found" }, HttpStatusCodes.NOT_FOUND);
|
|
159
|
+
|
|
160
|
+
return c.json(
|
|
161
|
+
{
|
|
162
|
+
message: "Authenticated user fetched successfully",
|
|
163
|
+
data: sanitizeUser(user)
|
|
164
|
+
},
|
|
165
|
+
HttpStatusCodes.OK
|
|
166
|
+
);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
console.error("Me error:", error);
|
|
169
|
+
return c.json({ message: "Failed to fetch user" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Why: Revokes current refresh token and clears auth cookies.
|
|
175
|
+
* When: Used when the active device logs out.
|
|
176
|
+
* Where: POST auth logout route.
|
|
177
|
+
*/
|
|
178
|
+
export const logout: Handler = async (c: any) => {
|
|
179
|
+
try {
|
|
180
|
+
await revokeCurrentRefreshToken(c);
|
|
181
|
+
cookie.deleteAuth(c);
|
|
182
|
+
cookie.deleteRefresh(c);
|
|
183
|
+
return c.json({ message: "Logged out successfully" }, HttpStatusCodes.OK);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error("Logout error:", error);
|
|
186
|
+
cookie.deleteAuth(c);
|
|
187
|
+
cookie.deleteRefresh(c);
|
|
188
|
+
return c.json({ message: "Failed to logout" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Why: Starts password reset flow and queues email delivery event.
|
|
194
|
+
* When: Used when user requests a forgot-password link.
|
|
195
|
+
* Where: POST auth forgot-password route.
|
|
196
|
+
*/
|
|
197
|
+
export const forgotPassword: Handler = async (c: any) => {
|
|
198
|
+
try {
|
|
199
|
+
const body = c.req.valid("json");
|
|
200
|
+
await db
|
|
201
|
+
.delete(passwordResetTokens)
|
|
202
|
+
.where(
|
|
203
|
+
and(
|
|
204
|
+
eq(passwordResetTokens.email, body.email),
|
|
205
|
+
lt(passwordResetTokens.expiresAt, new Date())
|
|
206
|
+
)
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
const user = await db.query.users.findFirst({
|
|
210
|
+
where: eq(users.email, body.email)
|
|
211
|
+
});
|
|
212
|
+
if (!user)
|
|
213
|
+
return c.json(
|
|
214
|
+
{ message: "If this email exists, a reset link has been sent" },
|
|
215
|
+
HttpStatusCodes.OK
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const plainToken = makeResetToken();
|
|
219
|
+
const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
|
|
220
|
+
|
|
221
|
+
await db.delete(passwordResetTokens).where(eq(passwordResetTokens.email, user.email));
|
|
222
|
+
await db.insert(passwordResetTokens).values({
|
|
223
|
+
email: user.email,
|
|
224
|
+
token: hashResetToken(plainToken),
|
|
225
|
+
expiresAt,
|
|
226
|
+
createdAt: new Date()
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const resetUrl = urls.url(`/reset-password?token=${plainToken}&email=${encodeURIComponent(user.email)}`);
|
|
230
|
+
await dispatchEvent(
|
|
231
|
+
"user:forget-password",
|
|
232
|
+
{ email: user.email, name: user.name, resetUrl },
|
|
233
|
+
{ queue: "mail" }
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
return c.json(
|
|
237
|
+
{ message: "If this email exists, a reset link has been sent" },
|
|
238
|
+
HttpStatusCodes.OK
|
|
239
|
+
);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.error("Forgot password error:", error);
|
|
242
|
+
return c.json(
|
|
243
|
+
{ message: "Failed to process forgot password request" },
|
|
244
|
+
HttpStatusCodes.INTERNAL_SERVER_ERROR
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Why: Verifies reset token and persists the new password.
|
|
251
|
+
* When: Used after user submits reset token + new password.
|
|
252
|
+
* Where: POST auth reset-password route.
|
|
253
|
+
*/
|
|
254
|
+
export const resetPassword: Handler = async (c: any) => {
|
|
255
|
+
try {
|
|
256
|
+
const body = c.req.valid("json");
|
|
257
|
+
const record = await db.query.passwordResetTokens.findFirst({
|
|
258
|
+
where: and(
|
|
259
|
+
eq(passwordResetTokens.email, body.email),
|
|
260
|
+
eq(passwordResetTokens.token, hashResetToken(body.token)),
|
|
261
|
+
gt(passwordResetTokens.expiresAt, new Date())
|
|
262
|
+
)
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
if (!record) {
|
|
266
|
+
return c.json(
|
|
267
|
+
{ message: "Invalid or expired reset token" },
|
|
268
|
+
HttpStatusCodes.UNPROCESSABLE_ENTITY
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
await db
|
|
273
|
+
.update(users)
|
|
274
|
+
.set({
|
|
275
|
+
password: await password.hashPassword(body.password),
|
|
276
|
+
updatedAt: new Date()
|
|
277
|
+
})
|
|
278
|
+
.where(eq(users.email, body.email));
|
|
279
|
+
|
|
280
|
+
await db.delete(passwordResetTokens).where(eq(passwordResetTokens.email, body.email));
|
|
281
|
+
|
|
282
|
+
const user = await db.query.users.findFirst({
|
|
283
|
+
where: eq(users.email, body.email)
|
|
284
|
+
});
|
|
285
|
+
if (user)
|
|
286
|
+
await db.update(refreshTokens).set({ revoked: 1 }).where(eq(refreshTokens.userId, user.id));
|
|
287
|
+
|
|
288
|
+
return c.json({ message: "Password reset successfully" }, HttpStatusCodes.OK);
|
|
289
|
+
} catch (error) {
|
|
290
|
+
console.error("Reset password error:", error);
|
|
291
|
+
return c.json({ message: "Failed to reset password" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Why: Validates email verification token and marks user as verified.
|
|
297
|
+
* When: Used when user opens verification link from inbox.
|
|
298
|
+
* Where: POST auth verify-email route.
|
|
299
|
+
*/
|
|
300
|
+
export const verifyEmail: Handler = async (c: any) => {
|
|
301
|
+
try {
|
|
302
|
+
const body = c.req.valid("json");
|
|
303
|
+
|
|
304
|
+
if (!env.AUTH_REQUIRE_EMAIL_VERIFICATION) {
|
|
305
|
+
return c.json({ message: "Email verification is not required" }, HttpStatusCodes.OK);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const record = await db.query.emailVerificationTokens.findFirst({
|
|
309
|
+
where: and(
|
|
310
|
+
eq(emailVerificationTokens.email, body.email),
|
|
311
|
+
eq(emailVerificationTokens.token, hashEmailVerificationToken(body.token)),
|
|
312
|
+
gt(emailVerificationTokens.expiresAt, new Date())
|
|
313
|
+
)
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
if (!record) {
|
|
317
|
+
return c.json(
|
|
318
|
+
{ message: "Invalid or expired verification token" },
|
|
319
|
+
HttpStatusCodes.UNPROCESSABLE_ENTITY
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
await db
|
|
324
|
+
.update(users)
|
|
325
|
+
.set({ emailVerifiedAt: new Date(), updatedAt: new Date() })
|
|
326
|
+
.where(eq(users.email, body.email));
|
|
327
|
+
await db.delete(emailVerificationTokens).where(eq(emailVerificationTokens.email, body.email));
|
|
328
|
+
|
|
329
|
+
return c.json({ message: "Email verified successfully" }, HttpStatusCodes.OK);
|
|
330
|
+
} catch (error) {
|
|
331
|
+
console.error("Verify email error:", error);
|
|
332
|
+
return c.json({ message: "Failed to verify email" }, HttpStatusCodes.INTERNAL_SERVER_ERROR);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Why: Rotates refresh token and reissues access credentials.
|
|
338
|
+
* When: Used when access token expires but refresh token is still valid.
|
|
339
|
+
* Where: POST auth refresh-token route.
|
|
340
|
+
*/
|
|
341
|
+
export const refreshToken: Handler = async (c: any) => {
|
|
342
|
+
try {
|
|
343
|
+
const body = c.req.valid("json");
|
|
344
|
+
const payload = await jwt.verifyToken(body.refresh_token, "refresh");
|
|
345
|
+
|
|
346
|
+
if (!payload?.jti)
|
|
347
|
+
return c.json({ message: "Invalid refresh token" }, HttpStatusCodes.UNAUTHORIZED);
|
|
348
|
+
|
|
349
|
+
const storedToken = await db.query.refreshTokens.findFirst({
|
|
350
|
+
where: eq(refreshTokens.jti, payload.jti as string)
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
if (!storedToken || storedToken.revoked === 1) {
|
|
354
|
+
return c.json({ message: "Refresh token revoked" }, HttpStatusCodes.UNAUTHORIZED);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (storedToken.expiresAt.getTime() < Date.now()) {
|
|
358
|
+
await db.delete(refreshTokens).where(eq(refreshTokens.id, storedToken.id));
|
|
359
|
+
return c.json({ message: "Refresh token expired" }, HttpStatusCodes.UNAUTHORIZED);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const user = await db.query.users.findFirst({
|
|
363
|
+
where: eq(users.id, payload.id as number),
|
|
364
|
+
with: { role: true }
|
|
365
|
+
});
|
|
366
|
+
if (!user) return c.json({ message: "User not found" }, HttpStatusCodes.UNAUTHORIZED);
|
|
367
|
+
|
|
368
|
+
const remember = !!payload.remember;
|
|
369
|
+
const refreshExpiry = remember ? env.JWT_REFRESH_REMEMBER_EXPIRY : undefined;
|
|
370
|
+
const accessToken = await jwt.generateToken(
|
|
371
|
+
{
|
|
372
|
+
id: user.id,
|
|
373
|
+
email: user.email,
|
|
374
|
+
roleId: user.role?.id,
|
|
375
|
+
role: user.role?.name,
|
|
376
|
+
remember
|
|
377
|
+
},
|
|
378
|
+
"access"
|
|
379
|
+
);
|
|
380
|
+
const newRefreshToken = await jwt.generateToken(
|
|
381
|
+
{
|
|
382
|
+
id: user.id,
|
|
383
|
+
email: user.email,
|
|
384
|
+
roleId: user.role?.id,
|
|
385
|
+
role: user.role?.name,
|
|
386
|
+
remember
|
|
387
|
+
},
|
|
388
|
+
"refresh",
|
|
389
|
+
refreshExpiry
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
await db
|
|
393
|
+
.update(refreshTokens)
|
|
394
|
+
.set({
|
|
395
|
+
jti: newRefreshToken.jti as string,
|
|
396
|
+
expiresAt: new Date(newRefreshToken.exp * 1000),
|
|
397
|
+
revoked: 0
|
|
398
|
+
})
|
|
399
|
+
.where(eq(refreshTokens.id, storedToken.id));
|
|
400
|
+
|
|
401
|
+
cookie.setAuth(c, accessToken.token);
|
|
402
|
+
cookie.setRefresh(c, newRefreshToken.token, refreshExpiry);
|
|
403
|
+
|
|
404
|
+
return c.json(
|
|
405
|
+
{
|
|
406
|
+
message: "Token refreshed successfully",
|
|
407
|
+
data: {
|
|
408
|
+
user: sanitizeUser(user),
|
|
409
|
+
access_token: accessToken.token,
|
|
410
|
+
refresh_token: newRefreshToken.token,
|
|
411
|
+
token_type: "Bearer"
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
HttpStatusCodes.OK
|
|
415
|
+
);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
console.error("Refresh token error:", error);
|
|
418
|
+
return c.json({ message: "Invalid or expired refresh token" }, HttpStatusCodes.UNAUTHORIZED);
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Why: Revokes all refresh tokens for account-wide logout.
|
|
424
|
+
* When: Used for "logout from all devices" security action.
|
|
425
|
+
* Where: POST auth logout-all-devices route.
|
|
426
|
+
*/
|
|
427
|
+
export const logoutAllDevices: Handler = async (c: any) => {
|
|
428
|
+
try {
|
|
429
|
+
const auth = c.get("auth");
|
|
430
|
+
|
|
431
|
+
if (!auth) return c.json({ message: "Unauthorized" }, HttpStatusCodes.UNAUTHORIZED);
|
|
432
|
+
|
|
433
|
+
await db.delete(refreshTokens).where(eq(refreshTokens.userId, auth.id));
|
|
434
|
+
cookie.deleteAuth(c);
|
|
435
|
+
cookie.deleteRefresh(c);
|
|
436
|
+
|
|
437
|
+
return c.json({ message: "Logged out from all devices successfully" }, HttpStatusCodes.OK);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
console.error("Logout all devices error:", error);
|
|
440
|
+
return c.json(
|
|
441
|
+
{ message: "Failed to logout from all devices" },
|
|
442
|
+
HttpStatusCodes.INTERNAL_SERVER_ERROR
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
import { env } from "@/env.js";
|
|
4
|
+
import { cookie, db, jwt } from "@/framework/facade.js";
|
|
5
|
+
import { refreshTokens } from "@/modules/auth/database/models/user.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Why: Removes sensitive/internal fields before returning user payload.
|
|
9
|
+
* When: Used in auth responses like register/login/me.
|
|
10
|
+
* Where: Called by auth.controller handlers.
|
|
11
|
+
*/
|
|
12
|
+
export function sanitizeUser(user: any) {
|
|
13
|
+
return {
|
|
14
|
+
id: user.id,
|
|
15
|
+
name: user.name,
|
|
16
|
+
email: user.email,
|
|
17
|
+
emailVerifiedAt: user.emailVerifiedAt ? String(user.emailVerifiedAt) : null,
|
|
18
|
+
roleId: user.roleId ?? null,
|
|
19
|
+
role: user.role || null,
|
|
20
|
+
createdAt: user.createdAt ? String(user.createdAt) : null,
|
|
21
|
+
updatedAt: user.updatedAt ? String(user.updatedAt) : null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Why: Creates a cryptographically secure password reset token.
|
|
27
|
+
* When: Used during forgot-password flow before hashing/storing.
|
|
28
|
+
* Where: Called by forgotPassword handler.
|
|
29
|
+
*/
|
|
30
|
+
export function makeResetToken() {
|
|
31
|
+
return randomBytes(32).toString("hex");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function makeEmailVerificationToken() {
|
|
35
|
+
return randomBytes(32).toString("hex");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Why: Hashes reset token so plain token is never stored in DB.
|
|
40
|
+
* When: Used for insert and verification in reset flow.
|
|
41
|
+
* Where: Called by forgotPassword/resetPassword handlers.
|
|
42
|
+
*/
|
|
43
|
+
export function hashResetToken(token: string) {
|
|
44
|
+
return createHash("sha256").update(token).digest("hex");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function hashEmailVerificationToken(token: string) {
|
|
48
|
+
return createHash("sha256").update(token).digest("hex");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Why: Revokes currently active refresh token from cookie context.
|
|
53
|
+
* When: Used before issuing new tokens or during logout.
|
|
54
|
+
* Where: Called by register/login/logout flows.
|
|
55
|
+
*/
|
|
56
|
+
export async function revokeCurrentRefreshToken(c: any) {
|
|
57
|
+
const token = cookie.getRefresh(c);
|
|
58
|
+
if (!token) return;
|
|
59
|
+
|
|
60
|
+
const payload = await jwt.verifyToken(token, "refresh");
|
|
61
|
+
if (payload?.jti) {
|
|
62
|
+
await db.delete(refreshTokens).where(eq(refreshTokens.jti, payload.jti as string));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Why: Issues access+refresh tokens, persists refresh token, sets cookies.
|
|
68
|
+
* When: Used after successful auth actions (register/login/refresh patterns).
|
|
69
|
+
* Where: Called by auth.controller handlers.
|
|
70
|
+
*/
|
|
71
|
+
export async function issueTokens(c: any, user: any, options?: { remember?: boolean; }) {
|
|
72
|
+
const remember = !!options?.remember;
|
|
73
|
+
const refreshExpiry = remember ? env.JWT_REFRESH_REMEMBER_EXPIRY : env.JWT_REFRESH_EXPIRY;
|
|
74
|
+
const role = user.role || null;
|
|
75
|
+
const accessToken = await jwt.generateToken(
|
|
76
|
+
{
|
|
77
|
+
id: user.id,
|
|
78
|
+
email: user.email,
|
|
79
|
+
roleId: role?.id ?? null,
|
|
80
|
+
role: role?.name ?? null,
|
|
81
|
+
remember
|
|
82
|
+
},
|
|
83
|
+
"access"
|
|
84
|
+
);
|
|
85
|
+
const refreshToken = await jwt.generateToken(
|
|
86
|
+
{
|
|
87
|
+
id: user.id,
|
|
88
|
+
email: user.email,
|
|
89
|
+
roleId: role?.id ?? null,
|
|
90
|
+
role: role?.name ?? null,
|
|
91
|
+
remember
|
|
92
|
+
},
|
|
93
|
+
"refresh",
|
|
94
|
+
refreshExpiry
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
if (refreshToken.jti) {
|
|
98
|
+
await db.insert(refreshTokens).values({
|
|
99
|
+
userId: user.id,
|
|
100
|
+
jti: refreshToken.jti,
|
|
101
|
+
revoked: 0,
|
|
102
|
+
expiresAt: new Date(refreshToken.exp * 1000)
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
cookie.setAuth(c, accessToken.token);
|
|
107
|
+
cookie.setRefresh(c, refreshToken.token, refreshExpiry);
|
|
108
|
+
|
|
109
|
+
return { accessToken: accessToken.token, refreshToken: refreshToken.token };
|
|
110
|
+
}
|
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Handler } from "hono";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
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
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { relations } from "drizzle-orm";
|
|
2
|
+
import { mysqlTable, int, varchar, text, timestamp } from "drizzle-orm/mysql-core";
|
|
3
|
+
import { users } from "@/modules/auth/database/models/user.js";
|
|
4
|
+
|
|
5
|
+
export const notifications = mysqlTable("notifications", {
|
|
6
|
+
id: int("id").autoincrement().primaryKey(),
|
|
7
|
+
userId: int("user_id").notNull().references(() => users.id, { onUpdate: "cascade", onDelete: "cascade" }),
|
|
8
|
+
type: varchar("type", { length: 100 }).notNull(),
|
|
9
|
+
title: varchar("title", { length: 255 }).notNull(),
|
|
10
|
+
body: text("body"),
|
|
11
|
+
data: text("data"),
|
|
12
|
+
link: varchar("link", { length: 500 }),
|
|
13
|
+
readAt: timestamp("read_at"),
|
|
14
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const notificationsRelations = relations(notifications, ({ one }) => ({
|
|
18
|
+
user: one(users, {
|
|
19
|
+
fields: [notifications.userId],
|
|
20
|
+
references: [users.id]
|
|
21
|
+
})
|
|
22
|
+
}));
|