-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathauth.ts
More file actions
110 lines (98 loc) · 3.82 KB
/
auth.ts
File metadata and controls
110 lines (98 loc) · 3.82 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
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import bcrypt from "bcryptjs";
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
import Google from "next-auth/providers/google";
import { db } from "@/lib/db";
import { getUserByEmail } from "@/lib/db/queries";
import { accounts, sessions, users, verificationTokens } from "@/lib/db/schema";
export const { handlers, auth, signIn, signOut } = NextAuth({
trustHost: true,
// ── Adapter ────────────────────────────────────────────────────────────────
// Stores users, accounts, sessions in Neon via Drizzle.
adapter: DrizzleAdapter(db, {
usersTable: users,
accountsTable: accounts,
sessionsTable: sessions,
verificationTokensTable: verificationTokens,
}),
// ── Session strategy ───────────────────────────────────────────────────────
// JWT so it works in Vercel Edge and doesn't need a DB hit on every request.
session: { strategy: "jwt" },
// ── Providers ──────────────────────────────────────────────────────────────
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID!,
clientSecret: process.env.AUTH_GOOGLE_SECRET!,
}),
Credentials({
name: "Email & Password",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null;
const user = await getUserByEmail(credentials.email as string);
if (!user?.password) return null;
const valid = await bcrypt.compare(
credentials.password as string,
user.password,
);
if (!valid) return null;
return {
id: user.id,
name: user.name,
email: user.email,
image: user.image,
role: user.role,
mustChangePassword: user.mustChangePassword ?? false,
};
},
}),
],
// ── Callbacks ──────────────────────────────────────────────────────────────
callbacks: {
// Inject role into the JWT on sign-in
async jwt({ token, user }) {
if (user) {
token.id = user.id;
token.role = (user as { role?: string }).role ?? "passenger";
token.mustChangePassword =
(user as { mustChangePassword?: boolean }).mustChangePassword ??
false;
}
if (token.email) {
const fresh = await getUserByEmail(token.email as string);
if (fresh) {
token.mustChangePassword = fresh.mustChangePassword ?? false;
if (
!token.roleCheckedAt ||
Date.now() - (token.roleCheckedAt as number) > 5 * 60 * 1000
) {
token.role = fresh.role;
token.id = fresh.id;
token.roleCheckedAt = Date.now();
}
}
}
return token;
},
// Expose id + role on the session object so client components can read it
async session({ session, token }) {
if (token) {
session.user.id = (token.id as string) ?? "";
session.user.role =
(token.role as "passenger" | "operator" | "admin") ?? "passenger";
(session.user as { mustChangePassword?: boolean }).mustChangePassword =
(token.mustChangePassword as boolean) ?? false;
}
return session;
},
},
// ── Pages ──────────────────────────────────────────────────────────────────
pages: {
signIn: "/auth",
error: "/auth",
},
});