397 lines
11 KiB
TypeScript

import { compare } from "bcrypt-ts";
import NextAuth, { type DefaultSession } from "next-auth";
import type { DefaultJWT } from "next-auth/jwt";
import Credentials from "next-auth/providers/credentials";
import Google from "next-auth/providers/google";
import { headers } from "next/headers";
import { DUMMY_PASSWORD } from "@/lib/constants";
import { createGuestUser, getAuthSettings, getUser } from "@/lib/db/queries";
import { buildAuthConfig } from "./auth.config";
import { db } from "@/lib/db";
import { loginToken, user as userTable } from "@/lib/db/schema";
import { eq, and, gt } from "drizzle-orm";
import {
establishSingleActiveSession,
getValidatedSessionUser,
isManagedSessionUserType,
requiresSingleActiveSession,
revokeSingleActiveSession,
} from "@/lib/auth/session-control";
import { recordAccessIpEventFromHeaders } from "@/lib/security/access-log";
export type UserType = "guest" | "regular" | "admin" | "agent_admin";
declare module "next-auth" {
interface Session extends DefaultSession {
user: {
id: string;
type: UserType;
name?: string | null;
sessionKey?: string;
} & DefaultSession["user"];
}
interface User {
id?: string;
email?: string | null;
name?: string | null;
type?: UserType;
}
}
declare module "next-auth/jwt" {
interface JWT extends DefaultJWT {
id: string;
type: UserType;
name?: string | null;
sessionKey?: string;
}
}
async function getOrCreateGoogleUser(email: string, name?: string | null) {
const existingUsers = await getUser(email);
if (existingUsers.length > 0) {
return existingUsers[0];
}
const [createdUser] = await db
.insert(userTable)
.values({
email,
password: "",
name: (name || email.split("@")[0] || "").slice(0, 100),
role: "regular",
isActive: true,
})
.returning();
return createdUser || null;
}
function createBaseProviders(googleClientId?: string, googleClientSecret?: string) {
const providers: any[] = [
Credentials({
credentials: {},
async authorize(credentials: any) {
// 1. DingTalk Token 登录逻辑
if (credentials.token) {
const { token } = credentials;
const now = new Date();
const [validToken] = await db
.select()
.from(loginToken)
.where(and(eq(loginToken.token, token), gt(loginToken.expiresAt, now)))
.limit(1);
if (!validToken) {
return null;
}
const users = await getUser(validToken.email);
if (users.length === 0) {
return null;
}
const user = users[0];
// 检查用户是否被激活
if (!user.isActive) {
return null;
}
await db.delete(loginToken).where(eq(loginToken.id, validToken.id));
const userType: UserType = (user.role as UserType) || "regular";
return { id: user.id, email: user.email, name: user.name, type: userType };
}
// 2. 邮箱密码登录逻辑
const email = String(credentials?.email || "").trim().toLowerCase();
const password = String(credentials?.password || "");
if (!email || !password) {
await compare(password || "", DUMMY_PASSWORD);
return null;
}
const users = await getUser(email);
if (users.length === 0) {
await compare(password, DUMMY_PASSWORD);
return null;
}
const [user] = users;
if (!user.password) {
await compare(password, DUMMY_PASSWORD);
return null;
}
const passwordsMatch = await compare(password, user.password);
if (!passwordsMatch) {
return null;
}
// 检查用户是否被激活
if (!user.isActive) {
return null;
}
// 从数据库读取用户角色,默认为 regular
const userType: UserType = (user.role as UserType) || "regular";
return { id: user.id, email: user.email, name: user.name, type: userType };
},
}),
Credentials({
id: "guest",
credentials: {},
async authorize() {
const [guestUser] = await createGuestUser();
return { ...guestUser, type: "guest" };
},
}),
];
if (googleClientId && googleClientSecret) {
providers.push(
Google({
clientId: googleClientId,
clientSecret: googleClientSecret,
})
);
}
return providers;
}
function normalizeBaseUrl(input?: string | null) {
if (!input) return "";
try {
const parsed = new URL(input);
return `${parsed.protocol}//${parsed.host}`;
} catch {
return "";
}
}
async function resolveRuntimeAuthUrl(settingsAuthUrl?: string | null) {
const fromEnv =
normalizeBaseUrl(process.env.AUTH_URL) ||
normalizeBaseUrl(process.env.NEXTAUTH_URL);
if (fromEnv) return fromEnv;
const fromSettings = normalizeBaseUrl(settingsAuthUrl);
if (fromSettings) return fromSettings;
const requestHeaders = await headers();
const forwardedHost = requestHeaders.get("x-forwarded-host");
const host = requestHeaders.get("host");
const resolvedHost = forwardedHost || host || "";
if (!resolvedHost) return "";
const forwardedProto = requestHeaders.get("x-forwarded-proto");
const protocol = forwardedProto || "https";
return `${protocol}://${resolvedHost}`;
}
const nextAuth = NextAuth(async () => {
const runtimeAuthSettings =
process.env.CANVAS_E2E_LOCAL === "true"
? null
: await getAuthSettings().catch(() => null as any);
const resolvedAuthUrl =
(await resolveRuntimeAuthUrl(runtimeAuthSettings?.authUrl)) || undefined;
const googleClientId =
runtimeAuthSettings?.googleClientId || process.env.GOOGLE_CLIENT_ID || "";
const googleClientSecret =
runtimeAuthSettings?.googleClientSecret || process.env.GOOGLE_CLIENT_SECRET || "";
return {
...buildAuthConfig(resolvedAuthUrl),
providers: createBaseProviders(googleClientId, googleClientSecret),
callbacks: {
async signIn({ account, user, profile }) {
if (account?.provider !== "google") {
return true;
}
const authSettings = await getAuthSettings();
if (!authSettings.isGoogleEnabled) {
return false;
}
const email = (user?.email || (profile as any)?.email || "").trim().toLowerCase();
if (!email) {
return false;
}
const dbUser = await getOrCreateGoogleUser(
email,
user?.name || (profile as any)?.name || null
);
return Boolean(dbUser?.isActive);
},
async jwt({ token, user, account, profile }) {
let resolvedUserId =
typeof token.id === "string" && token.id.length > 0 ? token.id : undefined;
let resolvedUserType =
typeof token.type === "string" && isManagedSessionUserType(token.type)
? token.type
: undefined;
if (user?.id && user?.type) {
token.id = user.id as string;
token.type = user.type;
token.name = user.name;
resolvedUserId = token.id;
resolvedUserType = token.type;
}
if (account?.provider === "google") {
const email = String(
user?.email || (profile as any)?.email || token.email || ""
)
.trim()
.toLowerCase();
if (email) {
const dbUser = await getOrCreateGoogleUser(
email,
user?.name || (profile as any)?.name || null
);
if (dbUser) {
token.id = dbUser.id;
token.type = (dbUser.role as UserType) || "regular";
token.name = dbUser.name;
token.email = dbUser.email;
resolvedUserId = dbUser.id;
resolvedUserType = (dbUser.role as UserType) || "regular";
}
}
}
if (account && resolvedUserId && requiresSingleActiveSession(resolvedUserType)) {
const { sessionKey } = await establishSingleActiveSession({
userId: resolvedUserId,
});
token.sessionKey = sessionKey;
} else if (!requiresSingleActiveSession(resolvedUserType)) {
delete token.sessionKey;
}
if (account && resolvedUserId) {
await recordAccessIpEventFromHeaders({
eventType: "login_success",
userId: resolvedUserId,
userEmail:
typeof token.email === "string"
? token.email
: typeof user?.email === "string"
? user.email
: null,
userRole: resolvedUserType || "regular",
method: "POST",
path: `/api/auth/${account.provider}`,
routeGroup: "auth",
statusCode: 200,
outcome: "login_success",
severity: "info",
reasons: ["登录成功"],
metadata: {
provider: account.provider,
},
}).catch((error) => {
console.warn("[Auth] Failed to record login access event:", error);
});
}
return token;
},
session({ session, token }) {
if (session.user) {
session.user.id = token.id;
session.user.type = token.type || "regular";
session.user.name = token.name as string;
session.user.sessionKey =
typeof token.sessionKey === "string" ? token.sessionKey : undefined;
}
return session;
},
},
};
});
const {
handlers: { GET, POST },
auth: rawAuth,
signIn,
signOut: rawSignOut,
} = nextAuth;
export { GET, POST, signIn };
export async function auth() {
const session = await rawAuth();
if (!session?.user?.id) {
return session;
}
if (!requiresSingleActiveSession(session.user.type)) {
return session;
}
const sessionKey = session.user.sessionKey;
if (!sessionKey) {
return null;
}
try {
const validatedUser = await getValidatedSessionUser({
userId: session.user.id,
sessionKey,
});
if (!validatedUser) {
return null;
}
return {
...session,
user: {
...session.user,
email: validatedUser.email,
name: validatedUser.name ?? session.user.name,
type: validatedUser.role,
sessionKey,
},
};
} catch (error) {
console.error("[Auth] Failed to validate active session:", error);
return null;
}
}
export async function signOut(options?: Parameters<typeof rawSignOut>[0]) {
const session = await auth().catch(() => null);
if (session?.user?.id && requiresSingleActiveSession(session.user.type)) {
await revokeSingleActiveSession({
userId: session.user.id,
sessionKey: session.user.sessionKey,
}).catch((error) => {
console.error("[Auth] Failed to revoke active session during sign out:", error);
});
}
return rawSignOut(options);
}