feat(auth): integrate auth.js v5 with credentials provider and bcrypt hashing
This commit is contained in:
parent
6eab20eaa4
commit
413c47aa87
8 changed files with 335 additions and 2 deletions
|
|
@ -6,6 +6,7 @@ A modern, high-performance real-time chat application, designed for seamless com
|
|||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
|
@ -14,7 +15,7 @@ A modern, high-performance real-time chat application, designed for seamless com
|
|||
- **Framework:** [Next.js 16](https://nextjs.org/) (App Router, Route Groups)
|
||||
- **UI & Styling:** [React 19](https://react.dev/), [Tailwind CSS v4](https://tailwindcss.com/)
|
||||
- **Database & ORM:** [PostgreSQL](https://www.postgresql.org/) managed via [Drizzle ORM](https://orm.drizzle.team/) & Drizzle Kit (Studio included)
|
||||
- **Authentication:** [Auth.js v5 (NextAuth)](https://authjs.dev/) with Credentials Provider & bcrypt hashing _(planned)_
|
||||
- **Authentication:** [Auth.js v5 (NextAuth)](https://authjs.dev/) with Credentials Provider & bcrypt hashing
|
||||
- **DevOps & Infrastructure:** Docker & Docker Compose, Caddy Reverse Proxy, Forgejo Actions _(planned)_
|
||||
|
||||
## 📂 Architecture & Structure
|
||||
|
|
@ -22,12 +23,14 @@ A modern, high-performance real-time chat application, designed for seamless com
|
|||
The project uses Next.js Route Groups without a `src/` directory to maintain a clean root layout:
|
||||
|
||||
- `app/(app)/` — Application routes (Layout, Home, Components)
|
||||
- `app/api/` — Backend API endpoints & Auth handlers (`[...nextauth]`)
|
||||
- `components/` — Modular UI components (Chat, Navigation, Sidebars)
|
||||
- `db/` — Database schema definitions, migrations, and Drizzle configuration (`drizzle.config.ts`)
|
||||
- `lib/` — Centralized core logic folder containing:
|
||||
- `store/` — State management stores
|
||||
- `types/` — Global TypeScript interfaces and type definitions
|
||||
- `public/` — Static assets (images, icons, fonts)
|
||||
|
||||
## 🎯 Current Status
|
||||
|
||||
_In Progress_ — Application scaffold initialized with Next.js 16, React 19, Tailwind CSS v4, and TypeScript. Database layer fully set up with PostgreSQL and Drizzle ORM schemas. Authentication and DevOps infrastructure will be implemented in subsequent phases.
|
||||
_In Progress_ — Application scaffold initialized with Next.js 16, React 19, Tailwind CSS v4, and TypeScript. Database layer fully set up with PostgreSQL and Drizzle ORM schemas, full authentication (Auth.js v5). DevOps infrastructure will be implemented in subsequent phases.
|
||||
|
|
|
|||
11
app/api/auth/[...nextauth]/route.ts
Normal file
11
app/api/auth/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* @file app/api/auth/[...nextauth]/route.ts
|
||||
* @description NextAuth API route handlers for HTTP GET and POST authentication requests.
|
||||
*/
|
||||
|
||||
import { handlers } from "@/auth";
|
||||
|
||||
/**
|
||||
* HTTP GET and POST request handlers exported from NextAuth configuration.
|
||||
*/
|
||||
export const { GET, POST } = handlers;
|
||||
96
auth.ts
Normal file
96
auth.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* @file auth.ts
|
||||
* @description NextAuth configuration defining authentication providers, credentials verification, JWT callbacks, and session handling.
|
||||
*/
|
||||
|
||||
import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import { db } from "@/db";
|
||||
import { users } from "@/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { verifyPassword } from "@/lib/password";
|
||||
|
||||
/**
|
||||
* NextAuth handlers, authentication methods, and auth utility exports.
|
||||
*/
|
||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
providers: [
|
||||
Credentials({
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
/**
|
||||
* Authorizes user credentials by checking against database records and verifying the password.
|
||||
*
|
||||
* @param {Record<string, unknown> | undefined} credentials - The incoming sign-in credentials containing email and password.
|
||||
* @returns {Promise<Object | null>} The authenticated user object containing id, name, email, and color, or null if validation fails.
|
||||
*/
|
||||
authorize: async (credentials) => {
|
||||
const email = credentials?.email as string | undefined;
|
||||
const password = credentials?.password as string | undefined;
|
||||
|
||||
if (!email || !password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [user] = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, email))
|
||||
.limit(1);
|
||||
|
||||
if (!user || !user.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isValid = await verifyPassword(password, user.password);
|
||||
if (!isValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.username,
|
||||
email: user.email,
|
||||
color: user.color,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
/**
|
||||
* Callback triggered when a JSON Web Token is created or updated.
|
||||
*
|
||||
* @param {Object} params - Callback parameters.
|
||||
* @param {Object} params.token - The current JWT token payload.
|
||||
* @param {Object} [params.user] - The authenticated user object available on initial sign in.
|
||||
* @returns {Object} The updated JWT token containing custom user claims.
|
||||
*/
|
||||
jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
token.color = user.color;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
/**
|
||||
* Callback triggered whenever a session is checked or accessed.
|
||||
*
|
||||
* @param {Object} params - Callback parameters.
|
||||
* @param {Object} params.session - The current session object.
|
||||
* @param {Object} params.token - The decoded JWT token payload.
|
||||
* @returns {Object} The updated session object populated with custom token attributes.
|
||||
*/
|
||||
session({ session, token }) {
|
||||
if (token && session.user) {
|
||||
session.user.id = token.id as string;
|
||||
session.user.color = token.color as string;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
},
|
||||
});
|
||||
30
lib/password.ts
Normal file
30
lib/password.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* @file lib/password.ts
|
||||
* @description Utility functions for securely hashing and verifying passwords using bcryptjs.
|
||||
*/
|
||||
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
/**
|
||||
* Hashes a plain text password using bcrypt with a salt round factor of 10.
|
||||
*
|
||||
* @param {string} password - The plain text password to be hashed.
|
||||
* @returns {Promise<string>} A promise that resolves to the generated password hash string.
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
return await bcrypt.hash(password, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a plain text password against a stored bcrypt hash to verify its validity.
|
||||
*
|
||||
* @param {string} password - The plain text password to verify.
|
||||
* @param {string} hash - The bcrypt hash to compare against.
|
||||
* @returns {Promise<boolean>} A promise that resolves to true if the password matches the hash, or false otherwise.
|
||||
*/
|
||||
export async function verifyPassword(
|
||||
password: string,
|
||||
hash: string,
|
||||
): Promise<boolean> {
|
||||
return await bcrypt.compare(password, hash);
|
||||
}
|
||||
35
lib/types/next-auth.d.ts
vendored
Normal file
35
lib/types/next-auth.d.ts
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* @file lib/types/next-auth.d.ts
|
||||
* @description TypeScript module augmentation for NextAuth types to add custom user and session attributes.
|
||||
*/
|
||||
|
||||
import { DefaultSession } from "next-auth";
|
||||
|
||||
declare module "next-auth" {
|
||||
/**
|
||||
* Extends the default NextAuth Session interface to include custom user properties.
|
||||
*
|
||||
* @interface Session
|
||||
* @property {Object} user - The session user object.
|
||||
* @property {string} user.id - The unique identifier of the user.
|
||||
* @property {string} user.color - The custom user interface color preference or avatar fallback color.
|
||||
*/
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
color: string;
|
||||
} & DefaultSession["user"];
|
||||
}
|
||||
|
||||
/**
|
||||
* Extends the default NextAuth User interface to include custom properties.
|
||||
*
|
||||
* @interface User
|
||||
* @property {string} [id] - The optional unique identifier of the user.
|
||||
* @property {string} [color] - The optional custom color attribute associated with the user.
|
||||
*/
|
||||
interface User {
|
||||
id?: string;
|
||||
color?: string;
|
||||
}
|
||||
}
|
||||
34
middleware.ts
Normal file
34
middleware.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* @file middleware.ts
|
||||
* @description NextAuth middleware protecting routes, managing authentication state, and redirecting unauthenticated or authenticated users accordingly.
|
||||
*/
|
||||
|
||||
import { auth } from "@/auth";
|
||||
|
||||
/**
|
||||
* Middleware function that checks authentication status on incoming requests and handles route redirection.
|
||||
*
|
||||
* @param {Request & { auth?: unknown, nextUrl: URL }} req - The incoming request object extended with NextAuth authentication details and URL helper.
|
||||
* @returns {Response | undefined} Redirection response if authentication criteria are not met, or undefined to allow request processing.
|
||||
*/
|
||||
export default auth((req) => {
|
||||
const isLoggedIn = !!req.auth;
|
||||
const isAuthPage =
|
||||
req.nextUrl.pathname.startsWith("/login") ||
|
||||
req.nextUrl.pathname.startsWith("/register");
|
||||
|
||||
if (!isLoggedIn && !isAuthPage) {
|
||||
return Response.redirect(new URL("/login", req.nextUrl));
|
||||
}
|
||||
|
||||
if (isLoggedIn && isAuthPage) {
|
||||
return Response.redirect(new URL("/", req.nextUrl));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Middleware execution matcher configuration.
|
||||
*/
|
||||
export const config = {
|
||||
matcher: ["/((?!api|_next/static|_next/image|favicon.ico|logo.png).*)"],
|
||||
};
|
||||
121
package-lock.json
generated
121
package-lock.json
generated
|
|
@ -8,11 +8,13 @@
|
|||
"name": "waveform",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^17.4.2",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"lucide-react": "^1.35.0",
|
||||
"next": "16.3.3",
|
||||
"next-auth": "^5.0.0-beta.32",
|
||||
"postgres": "^3.4.9",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
|
|
@ -20,6 +22,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.3.3",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "^20",
|
||||
"@types/pg": "^8.23.1",
|
||||
"@types/react": "^19",
|
||||
|
|
@ -44,6 +47,35 @@
|
|||
"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": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
|
|
@ -2270,6 +2302,15 @@
|
|||
"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/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
|
|
@ -2580,6 +2621,13 @@
|
|||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bcryptjs": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz",
|
||||
"integrity": "sha512-9xlo6R2qDs5uixm0bcIqCeMCE6HiQsIyel9KQySStiyqNl2tnj2mP3DX1Nf56MD6KMenNNlBBsy3LJ7gUEQPXQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
|
||||
|
|
@ -3597,6 +3645,15 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bcryptjs": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz",
|
||||
"integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"bin": {
|
||||
"bcrypt": "bin/bcrypt"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
|
|
@ -5831,6 +5888,15 @@
|
|||
"jiti": "lib/jiti-cli.mjs"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "6.2.10",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.10.tgz",
|
||||
"integrity": "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
@ -6464,6 +6530,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": {
|
||||
"version": "8.5.23",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
|
||||
|
|
@ -6521,6 +6614,15 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/oauth4webapi": {
|
||||
"version": "3.8.7",
|
||||
"resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.7.tgz",
|
||||
"integrity": "sha512-4RxcKxXjuItDFZ20RRPf4YTw3kpeXJyCgJFxVzJ068A7PNJ18st2Dg90tlC1LkSDS0GecroagCLHYEIVUhCAkw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
|
|
@ -6901,6 +7003,25 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
|
|
|||
|
|
@ -13,11 +13,13 @@
|
|||
"db:studio": "drizzle-kit studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcryptjs": "^3.0.3",
|
||||
"clsx": "^2.1.1",
|
||||
"dotenv": "^17.4.2",
|
||||
"drizzle-orm": "^0.45.2",
|
||||
"lucide-react": "^1.35.0",
|
||||
"next": "16.3.3",
|
||||
"next-auth": "^5.0.0-beta.32",
|
||||
"postgres": "^3.4.9",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
|
|
@ -25,6 +27,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.3.3",
|
||||
"@types/bcryptjs": "^2.4.6",
|
||||
"@types/node": "^20",
|
||||
"@types/pg": "^8.23.1",
|
||||
"@types/react": "^19",
|
||||
|
|
|
|||
Loading…
Reference in a new issue