create-nexwire 4.3.0 → 4.4.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 +10 -5
- package/express/src/framework/facade.ts +1 -0
- package/express/src/framework/http/logger.ts +13 -4
- package/express/src/framework/maker-cli/runtime/core.mjs +1 -2
- package/express/src/framework/notification/channels/mail.ts +8 -6
- package/express/src/middlewares/auth-middleware.ts +1 -2
- package/express/src/middlewares/role-middleware.ts +3 -13
- package/express/src/modules/auth/routes/index.ts +1 -2
- package/express/src/providers/notification.ts +2 -2
- package/express/src/resources/src/components/Switch.vue +60 -65
- package/express/src/resources/src/pages/auth/forgetPassword.vue +23 -30
- package/express/src/resources/src/pages/auth/login.vue +25 -28
- package/express/src/resources/src/pages/auth/register.vue +35 -39
- package/express/src/resources/src/pages/auth/resetPassword.vue +29 -37
- package/express/src/resources/src/pages/auth/verifyEmail.vue +15 -19
- package/express/src/resources/src/plugins/gum.ts +519 -203
- package/express/src/resources/src/stores/auth.ts +9 -6
- package/express/src/storage/database/nexwire.sqlite +0 -0
- package/fastify/.dockerignore +14 -0
- package/fastify/.env +44 -0
- package/fastify/.env.example +42 -0
- package/fastify/.vscode/extensions.json +6 -0
- package/fastify/.vscode/settings.json +37 -0
- package/fastify/LICENSE +21 -0
- package/fastify/README.md +232 -0
- package/fastify/biome.json +99 -0
- package/fastify/drizzle.config.ts +25 -0
- package/fastify/gitignore-stub +31 -0
- package/fastify/package.json +130 -0
- package/fastify/public/.gitkeep +0 -0
- package/fastify/src/config/app.ts +41 -0
- package/fastify/src/config/auth.ts +12 -0
- package/fastify/src/config/cache.ts +16 -0
- package/fastify/src/config/cookie.ts +15 -0
- package/fastify/src/config/cors.ts +12 -0
- package/fastify/src/config/database.ts +48 -0
- package/fastify/src/config/index.ts +83 -0
- package/fastify/src/config/jwt.ts +19 -0
- package/fastify/src/config/logging.ts +13 -0
- package/fastify/src/config/mail.ts +24 -0
- package/fastify/src/config/openapi.ts +36 -0
- package/fastify/src/config/queue.ts +30 -0
- package/fastify/src/config/rateLimit.ts +17 -0
- package/fastify/src/config/realtime.ts +15 -0
- package/fastify/src/config/redis.ts +18 -0
- package/fastify/src/config/security.ts +44 -0
- package/fastify/src/config/session.ts +17 -0
- package/fastify/src/config/storage.ts +22 -0
- package/fastify/src/config/validate.ts +50 -0
- package/fastify/src/database/migrations/sqlite/0000_init.sql +48 -0
- package/fastify/src/database/migrations/sqlite/meta/0000_snapshot.json +324 -0
- package/fastify/src/database/migrations/sqlite/meta/_journal.json +13 -0
- package/fastify/src/database/schema.ts +2 -0
- package/fastify/src/env.ts +76 -0
- package/fastify/src/framework/cache/cache.ts +114 -0
- package/fastify/src/framework/circuit-breaker/cb.ts +88 -0
- package/fastify/src/framework/database/app-schema.ts +37 -0
- package/fastify/src/framework/database/connection.ts +223 -0
- package/fastify/src/framework/database/migrate-hooks.ts +87 -0
- package/fastify/src/framework/database/paginate.ts +226 -0
- package/fastify/src/framework/database/schema.ts +20 -0
- package/fastify/src/framework/database/seed.ts +152 -0
- package/fastify/src/framework/events/dispatcher.ts +61 -0
- package/fastify/src/framework/facade.ts +96 -0
- package/fastify/src/framework/http/app.ts +176 -0
- package/fastify/src/framework/http/cors.ts +13 -0
- package/fastify/src/framework/http/fastify.d.ts +13 -0
- package/fastify/src/framework/http/logger.ts +110 -0
- package/fastify/src/framework/http/metrics.ts +22 -0
- package/fastify/src/framework/http/openapi.ts +48 -0
- package/fastify/src/framework/http/ratelimiter.ts +47 -0
- package/fastify/src/framework/http/security.ts +25 -0
- package/fastify/src/framework/http/static.ts +83 -0
- package/fastify/src/framework/http/validation.ts +49 -0
- package/fastify/src/framework/kernel.ts +43 -0
- package/fastify/src/framework/maker-cli/db/core.mjs +291 -0
- package/fastify/src/framework/maker-cli/db/index.mjs +204 -0
- package/fastify/src/framework/maker-cli/deploy/core.mjs +1046 -0
- package/fastify/src/framework/maker-cli/deploy/index.mjs +91 -0
- package/fastify/src/framework/maker-cli/index.mjs +65 -0
- package/fastify/src/framework/maker-cli/middleware/core.mjs +41 -0
- package/fastify/src/framework/maker-cli/middleware/index.mjs +17 -0
- package/fastify/src/framework/maker-cli/module/core.mjs +1066 -0
- package/fastify/src/framework/maker-cli/module/index.mjs +211 -0
- package/fastify/src/framework/maker-cli/provider/core.mjs +49 -0
- package/fastify/src/framework/maker-cli/provider/index.mjs +12 -0
- package/fastify/src/framework/maker-cli/runtime/build-ui.mjs +16 -0
- package/fastify/src/framework/maker-cli/runtime/core.mjs +560 -0
- package/fastify/src/framework/maker-cli/runtime/index.mjs +120 -0
- package/fastify/src/framework/maker-cli/runtime/redis-commander-patch.cjs +20 -0
- package/fastify/src/framework/maker-cli/stubs/controller/openapi.ts.stub +74 -0
- package/fastify/src/framework/maker-cli/stubs/controller/plain.ts.stub +77 -0
- package/fastify/src/framework/maker-cli/stubs/controller/standalone.ts.stub +51 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.bun.stub +52 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.pnpm.stub +55 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.stub +51 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/Dockerfile.yarn.stub +55 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/README.stub +59 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/compose/mysql.server.stub +45 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/compose/postgres.server.stub +45 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/compose/sqlite.stub +45 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/env/mysql.server.stub +68 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/env/postgres.server.stub +68 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/env/sqlite.stub +67 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/scripts/auto-migrate.sh.stub +41 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/README.stub +78 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/compose/noredis.stub +120 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/compose/redis.dev.stub +133 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/compose/redis.stub +131 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/env/local.example.stub +11 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/env/noredis.stub +25 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/env/redis.stub +25 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/README.stub +16 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/nginx-vhost/app.example.com.stub +13 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/pgadmin/servers.stub +14 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/server/redis/redis.conf.stub +7 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/supervisor/noredis.stub +54 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/supervisor/redis.stub +70 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/workflow/local.json.stub +25 -0
- package/fastify/src/framework/maker-cli/stubs/deploy/workflow/remote.json.stub +24 -0
- package/fastify/src/framework/maker-cli/stubs/facade/name.ts.stub +12 -0
- package/fastify/src/framework/maker-cli/stubs/helper/name.ts.stub +14 -0
- package/fastify/src/framework/maker-cli/stubs/job/name.ts.stub +25 -0
- package/fastify/src/framework/maker-cli/stubs/middleware/name.local.ts.stub +15 -0
- package/fastify/src/framework/maker-cli/stubs/middleware/name.ts.stub +15 -0
- package/fastify/src/framework/maker-cli/stubs/model/name.mysql.ts.stub +10 -0
- package/fastify/src/framework/maker-cli/stubs/model/name.postgresql.ts.stub +10 -0
- package/fastify/src/framework/maker-cli/stubs/model/name.sqlite.ts.stub +10 -0
- package/fastify/src/framework/maker-cli/stubs/notification/controller.ts.stub +79 -0
- package/fastify/src/framework/maker-cli/stubs/notification/job.ts.stub +11 -0
- package/fastify/src/framework/maker-cli/stubs/notification/model.ts.stub +29 -0
- package/fastify/src/framework/maker-cli/stubs/notification/provider.ts.stub +174 -0
- package/fastify/src/framework/maker-cli/stubs/notification/route.api.ts.stub +71 -0
- package/fastify/src/framework/maker-cli/stubs/notification/route.plain.ts.stub +15 -0
- package/fastify/src/framework/maker-cli/stubs/notification/schema.plain.ts.stub +26 -0
- package/fastify/src/framework/maker-cli/stubs/notification/schema.ts.stub +26 -0
- package/fastify/src/framework/maker-cli/stubs/provider/module-barrel.ts.stub +19 -0
- package/fastify/src/framework/maker-cli/stubs/provider/module.ts.stub +21 -0
- package/fastify/src/framework/maker-cli/stubs/provider/name.ts.stub +21 -0
- package/fastify/src/framework/maker-cli/stubs/route/api.ts.stub +101 -0
- package/fastify/src/framework/maker-cli/stubs/route/plain.ts.stub +23 -0
- package/fastify/src/framework/maker-cli/stubs/route/standalone.api.ts.stub +120 -0
- package/fastify/src/framework/maker-cli/stubs/route/standalone.plain.ts.stub +50 -0
- package/fastify/src/framework/maker-cli/stubs/schedule/name.ts.stub +36 -0
- package/fastify/src/framework/maker-cli/stubs/schema/name.plain.ts.stub +14 -0
- package/fastify/src/framework/maker-cli/stubs/schema/name.ts.stub +33 -0
- package/fastify/src/framework/maker-cli/stubs/seeder/name.ts.stub +18 -0
- package/fastify/src/framework/maker-cli/stubs/seeder/standalone.ts.stub +22 -0
- package/fastify/src/framework/maker-cli/stubs/service/name.plain.ts.stub +67 -0
- package/fastify/src/framework/maker-cli/stubs/service/name.ts.stub +65 -0
- package/fastify/src/framework/maker-cli/stubs/test/unit.ts.stub +22 -0
- package/fastify/src/framework/maker-cli/stubs/type/name.ts.stub +22 -0
- package/fastify/src/framework/maker-cli/utils/env-config.mjs +44 -0
- package/fastify/src/framework/maker-cli/utils/env-db.mjs +61 -0
- package/fastify/src/framework/maker-cli/utils/file-ops.mjs +30 -0
- package/fastify/src/framework/maker-cli/utils/flags.mjs +14 -0
- package/fastify/src/framework/maker-cli/utils/help.mjs +379 -0
- package/fastify/src/framework/maker-cli/utils/naming.mjs +119 -0
- package/fastify/src/framework/maker-cli/utils/process.mjs +52 -0
- package/fastify/src/framework/maker-cli/utils/project-commands.mjs +201 -0
- package/fastify/src/framework/modules/discover.ts +57 -0
- package/fastify/src/framework/modules/routes.ts +31 -0
- package/fastify/src/framework/notification/channels/broadcast.ts +15 -0
- package/fastify/src/framework/notification/channels/mail.ts +42 -0
- package/fastify/src/framework/notification/contracts.ts +29 -0
- package/fastify/src/framework/notification/index.ts +59 -0
- package/fastify/src/framework/notification/registry.ts +15 -0
- package/fastify/src/framework/providers/adapter.ts +149 -0
- package/fastify/src/framework/providers/contracts.ts +126 -0
- package/fastify/src/framework/providers/loader.ts +105 -0
- package/fastify/src/framework/providers/registry.ts +88 -0
- package/fastify/src/framework/queue/clear.ts +22 -0
- package/fastify/src/framework/queue/queue.ts +401 -0
- package/fastify/src/framework/queue/ui.ts +194 -0
- package/fastify/src/framework/queue/worker.ts +61 -0
- package/fastify/src/framework/realtime/broadcast.ts +70 -0
- package/fastify/src/framework/realtime/index.ts +2 -0
- package/fastify/src/framework/realtime/socket-cookie.ts +72 -0
- package/fastify/src/framework/realtime/socket.ts +149 -0
- package/fastify/src/framework/realtime/types.ts +6 -0
- package/fastify/src/framework/realtime/ui.ts +16 -0
- package/fastify/src/framework/redis/client.ts +148 -0
- package/fastify/src/framework/runtime/runtime.ts +23 -0
- package/fastify/src/framework/runtime/server-adapter.ts +33 -0
- package/fastify/src/framework/scheduler/lock.ts +128 -0
- package/fastify/src/framework/scheduler/run.ts +48 -0
- package/fastify/src/framework/scheduler/scheduler.ts +155 -0
- package/fastify/src/framework/server.ts +162 -0
- package/fastify/src/framework/session/session.ts +160 -0
- package/fastify/src/framework/storage/storage.ts +744 -0
- package/fastify/src/framework/support/cookie.ts +71 -0
- package/fastify/src/framework/support/jwt.ts +33 -0
- package/fastify/src/framework/support/lifecycle.ts +35 -0
- package/fastify/src/framework/support/logger.ts +148 -0
- package/fastify/src/framework/support/mail.ts +56 -0
- package/fastify/src/framework/support/password.ts +23 -0
- package/fastify/src/framework/support/status-codes.ts +75 -0
- package/fastify/src/framework/support/upload.ts +181 -0
- package/fastify/src/framework/support/url.ts +41 -0
- package/fastify/src/middlewares/auth-middleware.ts +134 -0
- package/fastify/src/middlewares/role-middleware.ts +19 -0
- package/fastify/src/modules/auth/__tests__/forgetpass.test.ts +200 -0
- package/fastify/src/modules/auth/__tests__/login.test.ts +171 -0
- package/fastify/src/modules/auth/__tests__/logout.test.ts +60 -0
- package/fastify/src/modules/auth/__tests__/logoutalldevices.test.ts +49 -0
- package/fastify/src/modules/auth/__tests__/me.test.ts +91 -0
- package/fastify/src/modules/auth/__tests__/refreshtoken.test.ts +305 -0
- package/fastify/src/modules/auth/__tests__/register.test.ts +291 -0
- package/fastify/src/modules/auth/__tests__/resetpass.test.ts +206 -0
- package/fastify/src/modules/auth/__tests__/verifyemail.test.ts +183 -0
- package/fastify/src/modules/auth/controllers/auth.ts +252 -0
- package/fastify/src/modules/auth/controllers/role.ts +35 -0
- package/fastify/src/modules/auth/database/models/role.ts +14 -0
- package/fastify/src/modules/auth/database/models/user.ts +46 -0
- package/fastify/src/modules/auth/database/seeders/role.ts +18 -0
- package/fastify/src/modules/auth/database/seeders/user.ts +35 -0
- package/fastify/src/modules/auth/facade.ts +12 -0
- package/fastify/src/modules/auth/helpers/auth.ts +115 -0
- package/fastify/src/modules/auth/jobs/forgetpass.ts +20 -0
- package/fastify/src/modules/auth/jobs/registeruser.ts +23 -0
- package/fastify/src/modules/auth/jobs/verifyemail.ts +18 -0
- package/fastify/src/modules/auth/routes/index.ts +185 -0
- package/fastify/src/modules/auth/routes/role.ts +56 -0
- package/fastify/src/modules/auth/schemas/auth.ts +116 -0
- package/fastify/src/modules/auth/services/forgetpassword.ts +66 -0
- package/fastify/src/modules/auth/services/login.ts +63 -0
- package/fastify/src/modules/auth/services/logout.ts +25 -0
- package/fastify/src/modules/auth/services/logoutalldevices.ts +19 -0
- package/fastify/src/modules/auth/services/me.ts +31 -0
- package/fastify/src/modules/auth/services/refreshtoken.ts +110 -0
- package/fastify/src/modules/auth/services/register.ts +141 -0
- package/fastify/src/modules/auth/services/resetpassword.ts +69 -0
- package/fastify/src/modules/auth/services/verifyemail.ts +77 -0
- package/fastify/src/modules/auth/types/auth.ts +31 -0
- package/fastify/src/providers/README.md +107 -0
- package/fastify/src/providers/notification.ts +4 -0
- package/fastify/src/resources/index.html +17 -0
- package/fastify/src/resources/src/App.vue +5 -0
- package/fastify/src/resources/src/assets/css/styles.css +14761 -0
- package/fastify/src/resources/src/assets/css/styles.css.map +1 -0
- package/fastify/src/resources/src/assets/fonts/public-sans/public-sans-latin-ext.woff2 +0 -0
- package/fastify/src/resources/src/assets/fonts/public-sans/public-sans-latin.woff2 +0 -0
- package/fastify/src/resources/src/assets/images/favicon/favicon.ico +0 -0
- package/fastify/src/resources/src/assets/images/logo.png +0 -0
- package/fastify/src/resources/src/assets/scss/custom.css +217 -0
- package/fastify/src/resources/src/assets/scss/custom.css.map +1 -0
- package/fastify/src/resources/src/assets/scss/custom.scss +1346 -0
- package/fastify/src/resources/src/components/Button.vue +32 -0
- package/fastify/src/resources/src/components/Checkbox.vue +32 -0
- package/fastify/src/resources/src/components/Datepicker.vue +106 -0
- package/fastify/src/resources/src/components/FeatureButton.vue +53 -0
- package/fastify/src/resources/src/components/FloatButton.vue +33 -0
- package/fastify/src/resources/src/components/Href.vue +32 -0
- package/fastify/src/resources/src/components/Input.vue +243 -0
- package/fastify/src/resources/src/components/InputGroup.vue +147 -0
- package/fastify/src/resources/src/components/InputPasswordToggle.vue +229 -0
- package/fastify/src/resources/src/components/Modal.vue +71 -0
- package/fastify/src/resources/src/components/Pagebar.vue +26 -0
- package/fastify/src/resources/src/components/Refresh.vue +19 -0
- package/fastify/src/resources/src/components/Select.vue +384 -0
- package/fastify/src/resources/src/components/Spinner.vue +42 -0
- package/fastify/src/resources/src/components/Switch.vue +60 -0
- package/fastify/src/resources/src/components/TextArea.vue +117 -0
- package/fastify/src/resources/src/components/Toast.vue +56 -0
- package/fastify/src/resources/src/components/datatable/DataTableSkeleton.vue +98 -0
- package/fastify/src/resources/src/components/datatable/Pagination.vue +147 -0
- package/fastify/src/resources/src/components/datatable/SelectOpption.vue +64 -0
- package/fastify/src/resources/src/components/datatable/index.vue +199 -0
- package/fastify/src/resources/src/composables/useAuth.ts +52 -0
- package/fastify/src/resources/src/helpers/assets.ts +51 -0
- package/fastify/src/resources/src/helpers/nformatter.ts +10 -0
- package/fastify/src/resources/src/helpers/utils.ts +75 -0
- package/fastify/src/resources/src/layouts/AuthLayout.vue +20 -0
- package/fastify/src/resources/src/layouts/Layout/Footer.vue +23 -0
- package/fastify/src/resources/src/layouts/Layout/Header.vue +124 -0
- package/fastify/src/resources/src/layouts/Layout/Sidebar.vue +137 -0
- package/fastify/src/resources/src/layouts/Layout/index.vue +87 -0
- package/fastify/src/resources/src/main.ts +28 -0
- package/fastify/src/resources/src/pages/auth/forgetPassword.vue +76 -0
- package/fastify/src/resources/src/pages/auth/login.vue +96 -0
- package/fastify/src/resources/src/pages/auth/register.vue +129 -0
- package/fastify/src/resources/src/pages/auth/resetPassword.vue +110 -0
- package/fastify/src/resources/src/pages/auth/verifyEmail.vue +64 -0
- package/fastify/src/resources/src/pages/dashboard/index.vue +66 -0
- package/fastify/src/resources/src/plugins/axios.ts +31 -0
- package/fastify/src/resources/src/plugins/browserDetect.ts +55 -0
- package/fastify/src/resources/src/plugins/dialog.ts +216 -0
- package/fastify/src/resources/src/plugins/gum.ts +683 -0
- package/fastify/src/resources/src/plugins/pulse.ts +157 -0
- package/fastify/src/resources/src/plugins/routeProgress.ts +87 -0
- package/fastify/src/resources/src/router/index.ts +85 -0
- package/fastify/src/resources/src/stores/admin-ui.ts +148 -0
- package/fastify/src/resources/src/stores/auth.ts +187 -0
- package/fastify/src/resources/tsconfig.json +19 -0
- package/fastify/src/resources/vite.config.ts +64 -0
- package/fastify/src/storage/app/private/.gitkeep +0 -0
- package/fastify/src/storage/app/public/.gitkeep +0 -0
- package/fastify/src/storage/app/tmp/generated/.gitkeep +0 -0
- package/fastify/src/storage/database/nexwire.sqlite +0 -0
- package/fastify/src/storage/logs/.gitkeep +0 -0
- package/fastify/src/storage/logs/app.log +0 -0
- package/fastify/src/storage/logs/fatal.log +0 -0
- package/fastify/src/storage/tmp/.gitkeep +0 -0
- package/fastify/src/storage/trash/modules/.gitkeep +0 -0
- package/fastify/src/types/global.d.ts +32 -0
- package/fastify/tsconfig.json +19 -0
- package/fastify/vitest.config.ts +38 -0
- package/hono/src/framework/facade.ts +1 -0
- package/hono/src/framework/http/logger.ts +86 -81
- package/hono/src/framework/maker-cli/runtime/core.mjs +1 -2
- package/hono/src/framework/notification/channels/mail.ts +8 -6
- package/hono/src/middlewares/auth-middleware.ts +1 -2
- package/hono/src/middlewares/role-middleware.ts +3 -13
- package/hono/src/modules/auth/routes/index.ts +1 -2
- package/hono/src/providers/notification.ts +2 -2
- package/hono/src/resources/src/components/Switch.vue +60 -65
- package/hono/src/resources/src/pages/auth/forgetPassword.vue +23 -29
- package/hono/src/resources/src/pages/auth/login.vue +26 -29
- package/hono/src/resources/src/pages/auth/register.vue +36 -40
- package/hono/src/resources/src/pages/auth/resetPassword.vue +29 -37
- package/hono/src/resources/src/pages/auth/verifyEmail.vue +15 -19
- package/hono/src/resources/src/plugins/gum.ts +519 -203
- package/hono/src/resources/src/stores/auth.ts +9 -6
- package/hono/src/storage/database/nexwire.sqlite +0 -0
- package/package.json +3 -2
- package/src/index.js +11 -8
|
@@ -0,0 +1,66 @@
|
|
|
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(
|
|
55
|
+
"user:forget-password",
|
|
56
|
+
{
|
|
57
|
+
email: issued.user.email,
|
|
58
|
+
name: issued.user.name,
|
|
59
|
+
resetUrl
|
|
60
|
+
},
|
|
61
|
+
{ queue: "mail" }
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return { kind: "sent", message: "Reset link has been sent" };
|
|
65
|
+
}
|
|
66
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
3
|
+
import { authConfig } from "@/config/index.js";
|
|
4
|
+
import { db, password } from "@/framework/facade.js";
|
|
5
|
+
import { users } from "@/modules/auth/database/models/user.js";
|
|
6
|
+
import { issueTokens, revokeCurrentRefreshToken, sanitizeUser } from "@/modules/auth/helpers/auth.js";
|
|
7
|
+
import type { LoginInput } from "@/modules/auth/types/auth.js";
|
|
8
|
+
|
|
9
|
+
export type LoginResult =
|
|
10
|
+
| { kind: "invalid_credentials"; message: string }
|
|
11
|
+
| { kind: "email_not_verified"; message: string }
|
|
12
|
+
| {
|
|
13
|
+
kind: "logged_in";
|
|
14
|
+
message: string;
|
|
15
|
+
data: {
|
|
16
|
+
user: ReturnType<typeof sanitizeUser>;
|
|
17
|
+
access_token: string;
|
|
18
|
+
refresh_token: string;
|
|
19
|
+
token_type: "Bearer";
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const loginService = {
|
|
24
|
+
login: async (req: FastifyRequest, reply: FastifyReply, body: LoginInput): Promise<LoginResult> => {
|
|
25
|
+
const user = await db.query.users.findFirst({
|
|
26
|
+
where: eq(users.email, body.email),
|
|
27
|
+
with: { role: true }
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Generic message for both "user missing" and "wrong password" —
|
|
31
|
+
// never tell an attacker which one it was.
|
|
32
|
+
const passwordOk = user && (await password.verifyPassword(body.password, user.password));
|
|
33
|
+
|
|
34
|
+
if (!user || !passwordOk) {
|
|
35
|
+
return { kind: "invalid_credentials", message: "Invalid credentials" };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (authConfig.requireEmailVerification && !user.emailVerifiedAt) {
|
|
39
|
+
return {
|
|
40
|
+
kind: "email_not_verified",
|
|
41
|
+
message: "Please verify your email before logging in"
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Rotate the current refresh token before issuing a new pair.
|
|
46
|
+
await revokeCurrentRefreshToken(req);
|
|
47
|
+
|
|
48
|
+
const tokens = await issueTokens(req, reply, user, {
|
|
49
|
+
remember: !!body.remember
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
kind: "logged_in",
|
|
54
|
+
message: "User logged in successfully",
|
|
55
|
+
data: {
|
|
56
|
+
user: sanitizeUser(user),
|
|
57
|
+
access_token: tokens.accessToken,
|
|
58
|
+
refresh_token: tokens.refreshToken,
|
|
59
|
+
token_type: "Bearer"
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import { cookie, logger } from "@/framework/facade.js";
|
|
3
|
+
import { revokeCurrentRefreshToken } from "@/modules/auth/helpers/auth.js";
|
|
4
|
+
|
|
5
|
+
export type LogoutResult = {
|
|
6
|
+
kind: "logged_out";
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const logoutService = {
|
|
11
|
+
logout: async (req: FastifyRequest, reply: FastifyReply): Promise<LogoutResult> => {
|
|
12
|
+
try {
|
|
13
|
+
await revokeCurrentRefreshToken(req);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
// Log and continue — the user is logging out either way.
|
|
16
|
+
// The server-side token will expire on its own.
|
|
17
|
+
logger.error("failed to revoke refresh token on logout", { err });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
cookie.deleteAuth(reply);
|
|
21
|
+
cookie.deleteRefresh(reply);
|
|
22
|
+
|
|
23
|
+
return { kind: "logged_out", message: "Logged out successfully" };
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -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,141 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
3
|
+
import { authConfig } from "@/config/index.js";
|
|
4
|
+
import { db, dispatchEvent, password, urls } from "@/framework/facade.js";
|
|
5
|
+
import { roles } from "@/modules/auth/database/models/role.js";
|
|
6
|
+
import { emailVerificationTokens, users } from "@/modules/auth/database/models/user.js";
|
|
7
|
+
import {
|
|
8
|
+
hashEmailVerificationToken,
|
|
9
|
+
issueTokens,
|
|
10
|
+
makeEmailVerificationToken,
|
|
11
|
+
revokeCurrentRefreshToken,
|
|
12
|
+
sanitizeUser
|
|
13
|
+
} from "@/modules/auth/helpers/auth.js";
|
|
14
|
+
import type { RegisterInput } from "@/modules/auth/types/auth.js";
|
|
15
|
+
|
|
16
|
+
export type RegisterResult =
|
|
17
|
+
| { kind: "email_exists"; message: string }
|
|
18
|
+
| { kind: "verification_required"; message: string }
|
|
19
|
+
| {
|
|
20
|
+
kind: "registered";
|
|
21
|
+
message: string;
|
|
22
|
+
data: {
|
|
23
|
+
user: ReturnType<typeof sanitizeUser>;
|
|
24
|
+
access_token: string;
|
|
25
|
+
refresh_token: string;
|
|
26
|
+
token_type: "Bearer";
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const VERIFY_TOKEN_TTL_MS = 24 * 60 * 60 * 1000;
|
|
31
|
+
|
|
32
|
+
export const registerService = {
|
|
33
|
+
register: async (req: FastifyRequest, reply: FastifyReply, body: RegisterInput): Promise<RegisterResult> => {
|
|
34
|
+
const defaultRole = await db.query.roles.findFirst({
|
|
35
|
+
where: eq(roles.name, "user")
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (!defaultRole) {
|
|
39
|
+
throw new Error('Default role "user" not found — check your seeds');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const existingUser = await db.query.users.findFirst({
|
|
43
|
+
where: eq(users.email, body.email)
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (existingUser) {
|
|
47
|
+
return { kind: "email_exists", message: "Email already exists" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const hashedPassword = await password.hashPassword(body.password);
|
|
51
|
+
const now = new Date();
|
|
52
|
+
|
|
53
|
+
const created = await db.transaction(async (tx: any) => {
|
|
54
|
+
const [inserted] = await tx
|
|
55
|
+
.insert(users)
|
|
56
|
+
.values({
|
|
57
|
+
name: body.name,
|
|
58
|
+
email: body.email,
|
|
59
|
+
password: hashedPassword,
|
|
60
|
+
roleId: defaultRole.id
|
|
61
|
+
})
|
|
62
|
+
.returning({ id: users.id });
|
|
63
|
+
|
|
64
|
+
if (!inserted) throw new Error("Failed to insert user");
|
|
65
|
+
|
|
66
|
+
const user = await tx.query.users.findFirst({
|
|
67
|
+
where: eq(users.id, inserted.id),
|
|
68
|
+
with: { role: true }
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (!user) throw new Error("Inserted user not found");
|
|
72
|
+
|
|
73
|
+
let plainVerifyToken: string | null = null;
|
|
74
|
+
|
|
75
|
+
if (authConfig.requireEmailVerification) {
|
|
76
|
+
plainVerifyToken = makeEmailVerificationToken();
|
|
77
|
+
const expiresAt = new Date(now.getTime() + VERIFY_TOKEN_TTL_MS);
|
|
78
|
+
|
|
79
|
+
// One active verification token per email
|
|
80
|
+
await tx.delete(emailVerificationTokens).where(eq(emailVerificationTokens.email, user.email));
|
|
81
|
+
|
|
82
|
+
await tx.insert(emailVerificationTokens).values({
|
|
83
|
+
email: user.email,
|
|
84
|
+
token: hashEmailVerificationToken(plainVerifyToken),
|
|
85
|
+
expiresAt,
|
|
86
|
+
createdAt: now
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { user, plainVerifyToken };
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ---- Side effects AFTER commit ----
|
|
94
|
+
|
|
95
|
+
if (created.plainVerifyToken) {
|
|
96
|
+
const verifyUrl = urls.url(`/verify-email?token=${created.plainVerifyToken}&email=${encodeURIComponent(created.user.email)}`);
|
|
97
|
+
|
|
98
|
+
await dispatchEvent(
|
|
99
|
+
"user:verify-email",
|
|
100
|
+
{
|
|
101
|
+
email: created.user.email,
|
|
102
|
+
name: created.user.name,
|
|
103
|
+
verifyUrl
|
|
104
|
+
},
|
|
105
|
+
{ queue: "mail" }
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
kind: "verification_required",
|
|
110
|
+
message: "User registered successfully. Please verify your email before logging in."
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await revokeCurrentRefreshToken(req);
|
|
115
|
+
const tokens = await issueTokens(req, reply, created.user, {
|
|
116
|
+
remember: !!body.remember
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await dispatchEvent(
|
|
120
|
+
"user:signup",
|
|
121
|
+
{
|
|
122
|
+
userId: created.user.id,
|
|
123
|
+
email: created.user.email,
|
|
124
|
+
name: created.user.name
|
|
125
|
+
// NO password — see explanation below
|
|
126
|
+
},
|
|
127
|
+
{ queue: "mail" }
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
kind: "registered",
|
|
132
|
+
message: "User registered successfully",
|
|
133
|
+
data: {
|
|
134
|
+
user: sanitizeUser(created.user),
|
|
135
|
+
access_token: tokens.accessToken,
|
|
136
|
+
refresh_token: tokens.refreshToken,
|
|
137
|
+
token_type: "Bearer"
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { and, eq, gt } from "drizzle-orm";
|
|
2
|
+
import { db, password } from "@/framework/facade.js";
|
|
3
|
+
import { passwordResetTokens, refreshTokens, users } from "@/modules/auth/database/models/user.js";
|
|
4
|
+
import { hashResetToken } from "@/modules/auth/helpers/auth.js";
|
|
5
|
+
import type { ResetPasswordInput } from "@/modules/auth/types/auth.js";
|
|
6
|
+
|
|
7
|
+
export type ResetPasswordResult = { kind: "invalid_token"; message: string } | { kind: "reset"; message: string };
|
|
8
|
+
|
|
9
|
+
export const resetPasswordService = {
|
|
10
|
+
resetPassword: async (body: ResetPasswordInput): Promise<ResetPasswordResult> => {
|
|
11
|
+
const user = await db.query.users.findFirst({
|
|
12
|
+
where: eq(users.email, body.email)
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (!user) {
|
|
16
|
+
return {
|
|
17
|
+
kind: "invalid_token",
|
|
18
|
+
message: "Invalid or expired reset token"
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const hashedPassword = await password.hashPassword(body.password);
|
|
23
|
+
const hashedToken = hashResetToken(body.token);
|
|
24
|
+
const now = new Date();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const consumed = await db.transaction(async (tx) => {
|
|
28
|
+
// Consume the token atomically inside the transaction.
|
|
29
|
+
// If anything below fails, the delete rolls back and the
|
|
30
|
+
// user can retry with the same token.
|
|
31
|
+
const [row] = await tx
|
|
32
|
+
.delete(passwordResetTokens)
|
|
33
|
+
.where(
|
|
34
|
+
and(
|
|
35
|
+
eq(passwordResetTokens.email, body.email),
|
|
36
|
+
eq(passwordResetTokens.token, hashedToken),
|
|
37
|
+
gt(passwordResetTokens.expiresAt, now)
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
.returning();
|
|
41
|
+
|
|
42
|
+
if (!row) return false;
|
|
43
|
+
|
|
44
|
+
await tx.update(users).set({ password: hashedPassword, updatedAt: now }).where(eq(users.id, user.id));
|
|
45
|
+
|
|
46
|
+
await tx.update(refreshTokens).set({ revoked: 1 }).where(eq(refreshTokens.userId, user.id));
|
|
47
|
+
|
|
48
|
+
return true;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!consumed) {
|
|
52
|
+
return {
|
|
53
|
+
kind: "invalid_token",
|
|
54
|
+
message: "Invalid or expired reset token"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { kind: "reset", message: "Password reset successfully" };
|
|
59
|
+
} catch (err) {
|
|
60
|
+
// Token was NOT consumed — the transaction rolled back, so the
|
|
61
|
+
// user can retry with the same token. Log so failures aren't silent.
|
|
62
|
+
// logger.error({ err, email: body.email }, "password reset failed");
|
|
63
|
+
return {
|
|
64
|
+
kind: "invalid_token",
|
|
65
|
+
message: "Invalid or expired reset token"
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { and, eq, gt } from "drizzle-orm";
|
|
2
|
+
import { authConfig } from "@/config/index.js";
|
|
3
|
+
import { db } from "@/framework/facade.js";
|
|
4
|
+
import { emailVerificationTokens, users } from "@/modules/auth/database/models/user.js";
|
|
5
|
+
import { hashEmailVerificationToken } from "@/modules/auth/helpers/auth.js";
|
|
6
|
+
import type { VerifyEmailInput } from "@/modules/auth/types/auth.js";
|
|
7
|
+
|
|
8
|
+
export type VerifyEmailResult =
|
|
9
|
+
| { kind: "not_required"; message: string }
|
|
10
|
+
| { kind: "invalid_token"; message: string }
|
|
11
|
+
| { kind: "verified"; message: string };
|
|
12
|
+
|
|
13
|
+
export const verifyEmailService = {
|
|
14
|
+
verifyEmail: async (body: VerifyEmailInput): Promise<VerifyEmailResult> => {
|
|
15
|
+
if (!authConfig.requireEmailVerification) {
|
|
16
|
+
return {
|
|
17
|
+
kind: "not_required",
|
|
18
|
+
message: "Email verification is not required"
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const user = await db.query.users.findFirst({
|
|
23
|
+
where: eq(users.email, body.email)
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!user) {
|
|
27
|
+
return {
|
|
28
|
+
kind: "invalid_token",
|
|
29
|
+
message: "Invalid or expired verification token"
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const hashedToken = hashEmailVerificationToken(body.token);
|
|
34
|
+
const now = new Date();
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const verified = await db.transaction(async (tx) => {
|
|
38
|
+
// Consume the token atomically INSIDE the transaction.
|
|
39
|
+
// If the update below fails, the delete rolls back and the
|
|
40
|
+
// user can retry with the same token.
|
|
41
|
+
const [consumed] = await tx
|
|
42
|
+
.delete(emailVerificationTokens)
|
|
43
|
+
.where(
|
|
44
|
+
and(
|
|
45
|
+
eq(emailVerificationTokens.email, body.email),
|
|
46
|
+
eq(emailVerificationTokens.token, hashedToken),
|
|
47
|
+
gt(emailVerificationTokens.expiresAt, now)
|
|
48
|
+
)
|
|
49
|
+
)
|
|
50
|
+
.returning();
|
|
51
|
+
|
|
52
|
+
if (!consumed) return false;
|
|
53
|
+
|
|
54
|
+
await tx.update(users).set({ emailVerifiedAt: now, updatedAt: now }).where(eq(users.id, user.id));
|
|
55
|
+
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (!verified) {
|
|
60
|
+
return {
|
|
61
|
+
kind: "invalid_token",
|
|
62
|
+
message: "Invalid or expired verification token"
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { kind: "verified", message: "Email verified successfully" };
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// Token was NOT consumed — the transaction rolled back, so the
|
|
69
|
+
// user can retry with the same token. Log so failures aren't silent.
|
|
70
|
+
// logger.error({ err, email: body.email }, "email verification failed");
|
|
71
|
+
return {
|
|
72
|
+
kind: "invalid_token",
|
|
73
|
+
message: "Invalid or expired verification token"
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type RegisterInput = {
|
|
2
|
+
name: string;
|
|
3
|
+
email: string;
|
|
4
|
+
password: string;
|
|
5
|
+
remember?: boolean;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type LoginInput = {
|
|
9
|
+
email: string;
|
|
10
|
+
password: string;
|
|
11
|
+
remember?: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type ForgotPasswordInput = {
|
|
15
|
+
email: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type ResetPasswordInput = {
|
|
19
|
+
email: string;
|
|
20
|
+
token: string;
|
|
21
|
+
password: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type VerifyEmailInput = {
|
|
25
|
+
email: string;
|
|
26
|
+
token: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type RefreshTokenInput = {
|
|
30
|
+
refresh_token: string;
|
|
31
|
+
};
|