feat: add Auth.js Credentials login config + route

This commit is contained in:
2026-07-31 11:55:58 +07:00
parent 43a20d8530
commit a662ed8d6a
6 changed files with 166 additions and 0 deletions
+6
View File
@@ -8,3 +8,9 @@
# see CLAUDE.md's "Môi trường" section for the exact docker compose/run # see CLAUDE.md's "Môi trường" section for the exact docker compose/run
# commands used to run the app and tests on this Windows host. # commands used to run the app and tests on this Windows host.
DATABASE_URL=postgres://asset_license_mgmt:change-me-local-dev@db:5432/asset_license_mgmt DATABASE_URL=postgres://asset_license_mgmt:change-me-local-dev@db:5432/asset_license_mgmt
# Auth.js v5 (next-auth@beta) — required. Random value used to sign/encrypt
# session JWTs. Generate one with `npx auth secret` (writes straight to
# .env/.env.local) or `openssl rand -base64 33`, then copy the value here.
# Do NOT reuse this placeholder — must be a real per-environment secret.
AUTH_SECRET=change-me-generate-a-real-secret
+94
View File
@@ -15,6 +15,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^1.28.0", "lucide-react": "^1.28.0",
"next": "14.2.35", "next": "14.2.35",
"next-auth": "^5.0.0-beta.32",
"radix-ui": "^1.6.7", "radix-ui": "^1.6.7",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
@@ -53,6 +54,35 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@auth/core": {
"version": "0.41.3",
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.3.tgz",
"integrity": "sha512-sJ3JMHHkXMD3aOjopv7mOBTO1Ocw4b0fAEXJBz6k7YHLpYQI6C40jCUPc5fNvUKxXRXNE1/sRISA15UrwWJBTw==",
"license": "ISC",
"dependencies": {
"@panva/hkdf": "^1.2.1",
"jose": "^6.0.6",
"oauth4webapi": "^3.3.0",
"preact": "10.24.3",
"preact-render-to-string": "6.5.11"
},
"peerDependencies": {
"@simplewebauthn/browser": "^9.0.1",
"@simplewebauthn/server": "^9.0.2",
"nodemailer": "^7.0.7 || ^8.0.5"
},
"peerDependenciesMeta": {
"@simplewebauthn/browser": {
"optional": true
},
"@simplewebauthn/server": {
"optional": true
},
"nodemailer": {
"optional": true
}
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.29.7", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
@@ -1707,6 +1737,15 @@
"node": ">=12.4.0" "node": ">=12.4.0"
} }
}, },
"node_modules/@panva/hkdf": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz",
"integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -9256,6 +9295,33 @@
} }
} }
}, },
"node_modules/next-auth": {
"version": "5.0.0-beta.32",
"resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.32.tgz",
"integrity": "sha512-CGlChIEWZ6LltNVxrE5yiySMID+Idpmry47JYA5lLwgD8Sx02a8M65VL0TWVz9nbnOioS/tCW/rP/0+mE7Qp4Q==",
"license": "ISC",
"dependencies": {
"@auth/core": "0.41.3"
},
"peerDependencies": {
"@simplewebauthn/browser": "^9.0.1",
"@simplewebauthn/server": "^9.0.2",
"next": "^14.0.0-0 || ^15.0.0 || ^16.0.0",
"nodemailer": "^7.0.7 || ^8.0.5",
"react": "^18.2.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@simplewebauthn/browser": {
"optional": true
},
"@simplewebauthn/server": {
"optional": true
},
"nodemailer": {
"optional": true
}
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -9350,6 +9416,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/oauth4webapi": {
"version": "3.8.6",
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.6.tgz",
"integrity": "sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -10148,6 +10223,25 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/preact": {
"version": "10.24.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz",
"integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/preact-render-to-string": {
"version": "6.5.11",
"resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz",
"integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==",
"license": "MIT",
"peerDependencies": {
"preact": ">=10"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+1
View File
@@ -19,6 +19,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^1.28.0", "lucide-react": "^1.28.0",
"next": "14.2.35", "next": "14.2.35",
"next-auth": "^5.0.0-beta.32",
"radix-ui": "^1.6.7", "radix-ui": "^1.6.7",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1", "react-dom": "18.3.1",
+3
View File
@@ -0,0 +1,3 @@
import { handlers } from "@/auth";
export const { GET, POST } = handlers;
+48
View File
@@ -0,0 +1,48 @@
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import { verifyUserCredentials } from "@/lib/users";
// Minimal internal auth — Auth.js v5 (next-auth@beta) Credentials provider
// only, no OAuth/SSO providers (company SSO was explicitly ruled out — see
// prisma/schema.prisma's User model comment / CLAUDE.md). JWT session
// strategy, not database sessions, so no adapter is configured here — the
// Credentials provider's authorize() talks to Postgres directly via
// verifyUserCredentials() (src/lib/users.ts), and the User table has no
// Account/Session/VerificationToken rows to manage.
//
// Permission model is flat: any logged-in user can do anything, so the
// session only needs to carry the user's id (used to identify who did what,
// not to gate what they can do) — see the module augmentation in
// src/types/next-auth.d.ts.
export const { handlers, signIn, signOut, auth } = NextAuth({
session: { strategy: "jwt" },
pages: {
signIn: "/login",
},
providers: [
Credentials({
credentials: {
email: {},
password: {},
},
authorize: async (credentials) => {
const user = await verifyUserCredentials(credentials?.email, credentials?.password);
return user;
},
}),
],
callbacks: {
jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
session({ session, token }) {
if (session.user) {
session.user.id = token.id as string;
}
return session;
},
},
});
+14
View File
@@ -0,0 +1,14 @@
import type { DefaultSession } from "next-auth";
// Auth.js v5's default Session["user"] shape (name/email/image) doesn't
// include `id`. src/auth.ts's jwt/session callbacks copy the user id from
// the Credentials provider's authorize() result through to the session, so
// callers can identify who's logged in — this augmentation makes that
// typed instead of requiring a cast everywhere it's read.
declare module "next-auth" {
interface Session {
user: {
id: string;
} & DefaultSession["user"];
}
}