-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathauth.ts
More file actions
217 lines (200 loc) · 7.29 KB
/
auth.ts
File metadata and controls
217 lines (200 loc) · 7.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
/**
* NextAuth Configuration for University Library Management System
*
* This file handles user authentication using NextAuth.js with:
* - Credentials-based authentication (email/password)
* - SHA-256 password hashing with salt
* - JWT session strategy
* - Lazy imports to support Edge runtime (middleware compatibility)
*
* IMPORTANT: This file uses lazy imports for database operations because:
* - Next.js middleware runs in Edge runtime (doesn't support Node.js modules like 'pg')
* - Database modules are only loaded when actually needed (in Node.js runtime contexts)
* - This prevents "crypto module not supported" errors in Edge runtime
*/
import NextAuth, { User } from "next-auth";
import { sha256 } from "@noble/hashes/sha256";
import CredentialsProvider from "next-auth/providers/credentials";
/**
* Helper function to concatenate two Uint8Arrays
* Used for password hashing: combines password bytes with salt
* @param a - First array (password bytes)
* @param b - Second array (salt)
* @returns Combined Uint8Array
*/
function concatUint8Arrays(a: Uint8Array, b: Uint8Array): Uint8Array {
const c = new Uint8Array(a.length + b.length);
c.set(a, 0);
c.set(b, a.length);
return c;
}
/**
* Lazy import pattern for database connection
*
* WHY LAZY IMPORTS?
* - This file is imported by middleware.ts which runs in Edge runtime
* - Edge runtime doesn't support Node.js modules (like 'pg' for PostgreSQL)
* - By using dynamic imports, we only load the database when actually needed
* - Database operations only happen in Node.js runtime (authorize/jwt callbacks)
*
* This prevents: "The edge runtime does not support Node.js 'crypto' module" errors
*/
async function getDb() {
const { db } = await import("@/database/drizzle");
return db;
}
async function getUsersSchema() {
const { users } = await import("@/database/schema");
return users;
}
async function getEq() {
const { eq } = await import("drizzle-orm");
return eq;
}
/**
* NextAuth configuration export
* Provides: handlers (for API routes), signIn, signOut, and auth (for server components)
*/
export const { handlers, signIn, signOut, auth } = NextAuth({
session: {
strategy: "jwt", // Use JWT tokens instead of database sessions (faster, stateless)
},
providers: [
/**
* Credentials Provider - Email/Password Authentication
*
* Flow:
* 1. User submits email/password
* 2. Look up user in database by email
* 3. Verify password using SHA-256 with salt
* 4. Return user object if valid, null if invalid
*/
CredentialsProvider({
async authorize(credentials) {
// Validate input
if (!credentials?.email || !credentials?.password) {
return null;
}
/**
* Lazy load database only when authorize is called (Node.js runtime)
* This is safe because authorize() only runs in API routes (Node.js runtime)
* Not in middleware (Edge runtime)
*/
const db = await getDb();
const users = await getUsersSchema();
const eq = await getEq();
// Query user by email
const user = await db
.select()
.from(users)
.where(eq(users.email, credentials.email.toString()))
.limit(1);
if (user.length === 0) return null;
/**
* Password Verification Process:
*
* Stored format: "salt:hash" (both base64 encoded)
* 1. Split stored password into salt and hash
* 2. Decode both from base64
* 3. Hash the provided password with the stored salt
* 4. Compare computed hash with stored hash
*
* This uses SHA-256 with salt for security:
* - Salt prevents rainbow table attacks
* - Each password has unique salt
* - Even same passwords have different hashes
*/
const [saltB64, hashB64] = user[0].password.split(":");
const salt = Uint8Array.from(Buffer.from(saltB64, "base64"));
const expectedHash = Buffer.from(hashB64, "base64");
// Hash the provided password with the stored salt
const passwordBytes = new TextEncoder().encode(
credentials.password.toString()
);
const hashBuffer = sha256(concatUint8Arrays(passwordBytes, salt));
const isPasswordValid = Buffer.from(hashBuffer).equals(expectedHash);
if (!isPasswordValid) return null;
// Return user object for NextAuth (will be stored in JWT token)
// CRITICAL: Include role for admin authorization checks
return {
id: user[0].id.toString(),
email: user[0].email,
name: user[0].fullName,
role: user[0].role, // Include role for authorization
} as User & { role: string };
},
}),
],
pages: {
signIn: "/sign-in",
},
callbacks: {
/**
* JWT Callback - Called when JWT token is created or updated
*
* This runs in Node.js runtime (API routes), so database access is safe
*
* Flow:
* 1. When user signs in, 'user' object is provided
* 2. Store user.id and user.name in JWT token
* 3. Update last_login timestamp in database
* 4. Return token (will be sent to client as cookie)
*/
async jwt({ token, user }) {
// Only runs on initial sign-in (when 'user' is provided)
if (user) {
// Store user data in JWT token
token.id = user.id;
token.name = user.name;
// CRITICAL: Store role in JWT token for authorization checks
token.role = (user as User & { role?: string }).role;
/**
* Update last_login timestamp when user signs in
* This helps track user activity for analytics and security
*/
try {
/**
* Lazy load database only when jwt callback is called (Node.js runtime)
* Safe because this callback only runs in API routes, not middleware
*/
const db = await getDb();
const users = await getUsersSchema();
const eq = await getEq();
// Type guard: user.id is guaranteed to exist here (from authorize callback)
if (user.id) {
await db
.update(users)
.set({ lastLogin: new Date() })
.where(eq(users.id, user.id));
}
} catch (error) {
// Don't fail authentication if last_login update fails
console.error("Failed to update last_login:", error);
}
}
return token;
},
/**
* Session Callback - Called whenever session is accessed
*
* This transforms the JWT token into the session object
* that's available in Server Components via auth()
*
* Flow:
* 1. Extract data from JWT token
* 2. Add to session.user object
* 3. Return session (available in getServerSession(), auth(), etc.)
*/
async session({ session, token }) {
if (session.user) {
// Add user ID and name from JWT token to session
session.user.id = token.id as string;
session.user.name = token.name as string;
// CRITICAL: Add role to session for authorization checks
// Type assertion needed because NextAuth types don't include role by default
(session.user as { role?: string }).role = token.role as string;
}
return session;
},
},
});