first commit
This commit is contained in:
2
server/index.ts
Normal file
2
server/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { createAuthModule } from "./module.js";
|
||||
export { registerAuthApiRoutes } from "./routes.js";
|
||||
293
server/module.ts
Normal file
293
server/module.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
import { ExpressAuth } from "@auth/express";
|
||||
import Credentials from "@auth/express/providers/credentials";
|
||||
import Google from "@auth/express/providers/google";
|
||||
import Slack from "@auth/express/providers/slack";
|
||||
import { PrismaAdapter } from "@auth/prisma-adapter";
|
||||
import type { Request, RequestHandler } from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
type CredentialUser = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
image?: string | null;
|
||||
passwordHash: string | null;
|
||||
};
|
||||
|
||||
type CreateAuthModuleOptions<TAuthUser> = {
|
||||
prisma: any;
|
||||
clientUrl: string;
|
||||
sessionCookieName: string;
|
||||
sessionCookieSecure: boolean;
|
||||
authUrl?: string;
|
||||
authDebug?: boolean;
|
||||
authSecret?: string;
|
||||
trustHost?: boolean;
|
||||
extraSessionCookieNames?: string[];
|
||||
signInPath?: string;
|
||||
authenticatedRedirectPath?: string;
|
||||
googleClientId?: string;
|
||||
googleClientSecret?: string;
|
||||
slackClientId?: string;
|
||||
slackClientSecret?: string;
|
||||
findCredentialsUserByEmail: (email: string) => Promise<CredentialUser | null>;
|
||||
comparePassword: (password: string, passwordHash: string) => Promise<boolean>;
|
||||
sessionUserSelect: Record<string, boolean>;
|
||||
mapSessionUser: (user: any) => TAuthUser;
|
||||
onSessionValidated?: (user: TAuthUser) => Promise<void> | void;
|
||||
};
|
||||
|
||||
function parseBoolean(value: string | undefined, fallback: boolean): boolean {
|
||||
if (value === undefined) {
|
||||
return fallback;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (["1", "true", "yes", "on"].includes(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (["0", "false", "no", "off"].includes(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function createAuthModule<TAuthUser>(options: CreateAuthModuleOptions<TAuthUser>) {
|
||||
const signInPath = options.signInPath ?? "/login";
|
||||
const authenticatedRedirectPath = options.authenticatedRedirectPath ?? "/chat";
|
||||
const googleAuthEnabled = Boolean(options.googleClientId && options.googleClientSecret);
|
||||
const slackAuthEnabled = Boolean(options.slackClientId && options.slackClientSecret);
|
||||
|
||||
const providers: any[] = [
|
||||
Credentials({
|
||||
name: "Email et mot de passe",
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
password: { label: "Password", type: "password" }
|
||||
},
|
||||
authorize: async (rawCredentials) => {
|
||||
const parsed = z
|
||||
.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8)
|
||||
})
|
||||
.safeParse(rawCredentials);
|
||||
|
||||
if (!parsed.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await options.findCredentialsUserByEmail(parsed.data.email);
|
||||
if (!user?.passwordHash) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const valid = await options.comparePassword(parsed.data.password, user.passwordHash);
|
||||
if (!valid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
image: user.image ?? null
|
||||
};
|
||||
}
|
||||
})
|
||||
];
|
||||
|
||||
if (googleAuthEnabled) {
|
||||
providers.push(
|
||||
Google({
|
||||
clientId: options.googleClientId!,
|
||||
clientSecret: options.googleClientSecret!,
|
||||
allowDangerousEmailAccountLinking: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (slackAuthEnabled) {
|
||||
providers.push(
|
||||
Slack({
|
||||
clientId: options.slackClientId!,
|
||||
clientSecret: options.slackClientSecret!,
|
||||
allowDangerousEmailAccountLinking: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const authConfig = {
|
||||
adapter: PrismaAdapter(options.prisma),
|
||||
trustHost: options.trustHost ?? parseBoolean(process.env.AUTH_TRUST_HOST, true),
|
||||
debug: options.authDebug ?? parseBoolean(process.env.AUTH_DEBUG, false),
|
||||
logger: options.authDebug
|
||||
? {
|
||||
error(error: Error) {
|
||||
console.error("[authjs:error]", error.name, error.message, error.cause ?? "");
|
||||
},
|
||||
warn(code: string) {
|
||||
console.warn("[authjs:warn]", code);
|
||||
},
|
||||
debug(message: string, metadata?: unknown) {
|
||||
console.log("[authjs:debug]", message, metadata ?? "");
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
session: { strategy: "database" as const },
|
||||
secret: options.authSecret ?? process.env.AUTH_SECRET,
|
||||
cookies: {
|
||||
sessionToken: {
|
||||
name: options.sessionCookieName,
|
||||
options: {
|
||||
httpOnly: true,
|
||||
sameSite: "lax" as const,
|
||||
path: "/",
|
||||
secure: options.sessionCookieSecure
|
||||
}
|
||||
}
|
||||
},
|
||||
providers,
|
||||
pages: {
|
||||
signIn: signInPath
|
||||
},
|
||||
callbacks: {
|
||||
redirect: async ({ url, baseUrl }: { url: string; baseUrl: string }) => {
|
||||
const clientOrigin = new URL(options.clientUrl).origin;
|
||||
const successUrl = new URL(authenticatedRedirectPath, clientOrigin).toString();
|
||||
|
||||
const shouldForceChat = (pathname: string, searchParams: URLSearchParams): boolean => {
|
||||
if (pathname !== "/" && pathname !== signInPath) {
|
||||
return false;
|
||||
}
|
||||
return !searchParams.has("error");
|
||||
};
|
||||
|
||||
if (url.startsWith("/")) {
|
||||
const relative = new URL(url, clientOrigin);
|
||||
if (shouldForceChat(relative.pathname, relative.searchParams)) {
|
||||
return successUrl;
|
||||
}
|
||||
return `${clientOrigin}${relative.pathname}${relative.search}${relative.hash}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const target = new URL(url);
|
||||
const base = new URL(baseUrl);
|
||||
|
||||
if (target.origin === clientOrigin) {
|
||||
if (shouldForceChat(target.pathname, target.searchParams)) {
|
||||
return successUrl;
|
||||
}
|
||||
return target.toString();
|
||||
}
|
||||
|
||||
if (target.origin === base.origin) {
|
||||
if (shouldForceChat(target.pathname, target.searchParams)) {
|
||||
return successUrl;
|
||||
}
|
||||
return `${clientOrigin}${target.pathname}${target.search}${target.hash}`;
|
||||
}
|
||||
} catch {
|
||||
return successUrl;
|
||||
}
|
||||
|
||||
return successUrl;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const authHandler = ExpressAuth(authConfig);
|
||||
|
||||
const requireSession: RequestHandler = async (req, res, next) => {
|
||||
const token = extractSessionToken(req.headers.cookie);
|
||||
const authDebug = options.authDebug ?? parseBoolean(process.env.AUTH_DEBUG, false);
|
||||
|
||||
if (!token) {
|
||||
const payload: { error: string; reason?: string } = { error: "Unauthorized" };
|
||||
if (authDebug) {
|
||||
payload.reason = "missing_session_cookie";
|
||||
}
|
||||
return res.status(401).json(payload);
|
||||
}
|
||||
|
||||
const session = await options.prisma.session.findUnique({
|
||||
where: { sessionToken: token },
|
||||
include: {
|
||||
user: {
|
||||
select: options.sessionUserSelect
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!session || session.expires <= new Date()) {
|
||||
const payload: { error: string; reason?: string } = { error: "Unauthorized" };
|
||||
if (authDebug) {
|
||||
payload.reason = !session ? "session_not_found" : "session_expired";
|
||||
}
|
||||
return res.status(401).json(payload);
|
||||
}
|
||||
|
||||
const authUser = options.mapSessionUser(session.user);
|
||||
(req as Request & { authUser?: TAuthUser }).authUser = authUser;
|
||||
await options.onSessionValidated?.(authUser);
|
||||
next();
|
||||
};
|
||||
|
||||
const extractSessionToken = (cookieHeader: string | undefined): string | null => {
|
||||
if (!cookieHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cookies = cookieHeader
|
||||
.split(";")
|
||||
.map((part) => part.trim())
|
||||
.map((part) => {
|
||||
const index = part.indexOf("=");
|
||||
if (index < 0) {
|
||||
return null;
|
||||
}
|
||||
return [part.slice(0, index), decodeURIComponent(part.slice(index + 1))] as const;
|
||||
})
|
||||
.filter((entry): entry is readonly [string, string] => entry !== null);
|
||||
|
||||
const possibleNames = [
|
||||
options.sessionCookieName,
|
||||
...(options.extraSessionCookieNames ?? []),
|
||||
"__Secure-authjs.session-token",
|
||||
"authjs.session-token",
|
||||
"__Secure-next-auth.session-token",
|
||||
"next-auth.session-token"
|
||||
];
|
||||
|
||||
for (const name of possibleNames) {
|
||||
const exact = cookies.find(([cookieName]) => cookieName === name);
|
||||
if (exact) {
|
||||
return exact[1];
|
||||
}
|
||||
|
||||
const chunks = cookies
|
||||
.filter(([cookieName]) => cookieName.startsWith(`${name}.`))
|
||||
.map(([cookieName, value]) => {
|
||||
const suffix = cookieName.slice(name.length + 1);
|
||||
return [Number.parseInt(suffix, 10), value] as const;
|
||||
})
|
||||
.filter(([index]) => Number.isInteger(index))
|
||||
.sort((left, right) => left[0] - right[0]);
|
||||
|
||||
if (chunks.length > 0) {
|
||||
return chunks.map(([, value]) => value).join("");
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return {
|
||||
authConfig,
|
||||
authHandler,
|
||||
requireSession,
|
||||
extractSessionToken,
|
||||
googleAuthEnabled,
|
||||
slackAuthEnabled
|
||||
};
|
||||
}
|
||||
381
server/routes.ts
Normal file
381
server/routes.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
import { createHash, randomBytes } from "node:crypto";
|
||||
import type { Express, RequestHandler } from "express";
|
||||
import { z } from "zod";
|
||||
|
||||
type RegisterAuthApiRoutesOptions = {
|
||||
app: Express;
|
||||
prisma: any;
|
||||
authHandler: RequestHandler;
|
||||
requireSession: RequestHandler;
|
||||
extractSessionToken: (cookieHeader: string | undefined) => string | null;
|
||||
providersAvailability: Record<string, boolean>;
|
||||
sessionCookieName: string;
|
||||
sessionCookieSecure: boolean;
|
||||
extraCookieNamesToClear?: string[];
|
||||
messages?: Partial<AuthRouteMessages>;
|
||||
authBasePath?: string;
|
||||
authApiBasePath?: string;
|
||||
mePath?: string;
|
||||
normalizeEmail?: (email: string) => string;
|
||||
passwordHasher?: (password: string) => Promise<string>;
|
||||
passwordComparator?: (password: string, passwordHash: string) => Promise<boolean>;
|
||||
passwordReset?: {
|
||||
enabled: boolean;
|
||||
tokenTtlMs?: number;
|
||||
identifierPrefix?: string;
|
||||
buildResetUrl: (token: string) => string;
|
||||
sendMessage: (input: {
|
||||
user: { id: string; email: string; name: string | null; passwordHash: string | null };
|
||||
resetUrl: string;
|
||||
isPasswordCreation: boolean;
|
||||
expiresAt: Date;
|
||||
}) => Promise<void>;
|
||||
};
|
||||
onUserRegistered?: (user: { id: string; email: string | null; name: string | null }) => Promise<void> | void;
|
||||
onPasswordResetConfirmed?: (user: { id: string; email: string | null; name: string | null }) => Promise<void> | void;
|
||||
};
|
||||
|
||||
type AuthRouteMessages = {
|
||||
invalidPayload: string;
|
||||
emailAlreadyUsed: string;
|
||||
accountNotFound: string;
|
||||
externalAccountOnly: string;
|
||||
invalidPassword: string;
|
||||
passwordResetUnavailable: string;
|
||||
invalidResetLink: string;
|
||||
expiredResetLink: string;
|
||||
};
|
||||
|
||||
const defaultNormalizeEmail = (email: string) => email.trim();
|
||||
const defaultPasswordResetIdentifierPrefix = "password-reset:";
|
||||
const defaultMessages: AuthRouteMessages = {
|
||||
invalidPayload: "Invalid payload",
|
||||
emailAlreadyUsed: "Email already used",
|
||||
accountNotFound: "Account not found",
|
||||
externalAccountOnly: "This account uses an external sign-in provider.",
|
||||
invalidPassword: "Invalid password",
|
||||
passwordResetUnavailable: "Email service is not configured.",
|
||||
invalidResetLink: "Invalid reset link",
|
||||
expiredResetLink: "Invalid or expired reset link"
|
||||
};
|
||||
|
||||
function hashPasswordResetToken(token: string): string {
|
||||
return createHash("sha256").update(token).digest("hex");
|
||||
}
|
||||
|
||||
function buildPasswordResetIdentifier(prefix: string, userId: string): string {
|
||||
return `${prefix}${userId}`;
|
||||
}
|
||||
|
||||
export function registerAuthApiRoutes(options: RegisterAuthApiRoutesOptions): void {
|
||||
const authBasePath = options.authBasePath ?? "/auth";
|
||||
const authApiBasePath = options.authApiBasePath ?? "/api/auth";
|
||||
const mePath = options.mePath ?? "/api/me";
|
||||
const normalizeEmail = options.normalizeEmail ?? defaultNormalizeEmail;
|
||||
const passwordHasher = options.passwordHasher ?? ((password: string) => Promise.resolve(password));
|
||||
const passwordComparator = options.passwordComparator ?? ((password: string, hash: string) => Promise.resolve(password === hash));
|
||||
const passwordResetIdentifierPrefix = options.passwordReset?.identifierPrefix ?? defaultPasswordResetIdentifierPrefix;
|
||||
const messages = { ...defaultMessages, ...(options.messages ?? {}) };
|
||||
|
||||
const findUserByEmail = async (email: string) => {
|
||||
const normalized = normalizeEmail(email);
|
||||
const lowered = normalized.toLowerCase();
|
||||
|
||||
return options.prisma.user.findFirst({
|
||||
where: {
|
||||
OR: lowered === normalized ? [{ email: normalized }] : [{ email: normalized }, { email: lowered }]
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
image: true,
|
||||
passwordHash: true,
|
||||
emailVerified: true
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getPasswordResetContext = async (
|
||||
rawToken: string
|
||||
): Promise<
|
||||
| {
|
||||
verificationToken: { identifier: string; expires: Date };
|
||||
user: { id: string; email: string | null; name: string | null; passwordHash: string | null; emailVerified: Date | null };
|
||||
}
|
||||
| null
|
||||
> => {
|
||||
const verificationToken = await options.prisma.verificationToken.findUnique({
|
||||
where: { token: hashPasswordResetToken(rawToken) },
|
||||
select: { identifier: true, expires: true }
|
||||
});
|
||||
|
||||
if (
|
||||
!verificationToken ||
|
||||
verificationToken.expires <= new Date() ||
|
||||
!verificationToken.identifier.startsWith(passwordResetIdentifierPrefix)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userId = verificationToken.identifier.slice(passwordResetIdentifierPrefix.length);
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await options.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
passwordHash: true,
|
||||
emailVerified: true
|
||||
}
|
||||
});
|
||||
|
||||
if (!user?.email) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { verificationToken, user };
|
||||
};
|
||||
|
||||
options.app.use(authBasePath, options.authHandler);
|
||||
|
||||
options.app.get(`${authApiBasePath}/providers`, (_req, res) => {
|
||||
res.json(options.providersAvailability);
|
||||
});
|
||||
|
||||
options.app.post(`${authApiBasePath}/register`, async (req, res) => {
|
||||
const parsed = z
|
||||
.object({
|
||||
name: z.string().min(2).max(60),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8)
|
||||
})
|
||||
.safeParse(req.body);
|
||||
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: messages.invalidPayload });
|
||||
}
|
||||
|
||||
const email = normalizeEmail(parsed.data.email);
|
||||
const exists = await findUserByEmail(email);
|
||||
if (exists) {
|
||||
return res.status(409).json({ error: messages.emailAlreadyUsed });
|
||||
}
|
||||
|
||||
const passwordHash = await passwordHasher(parsed.data.password);
|
||||
const created = await options.prisma.user.create({
|
||||
data: {
|
||||
name: parsed.data.name,
|
||||
email,
|
||||
passwordHash
|
||||
},
|
||||
select: { id: true, email: true, name: true }
|
||||
});
|
||||
|
||||
await options.onUserRegistered?.(created);
|
||||
return res.status(201).json(created);
|
||||
});
|
||||
|
||||
options.app.post(`${authApiBasePath}/login`, async (req, res) => {
|
||||
const parsed = z
|
||||
.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8)
|
||||
})
|
||||
.safeParse(req.body);
|
||||
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: messages.invalidPayload });
|
||||
}
|
||||
|
||||
const email = normalizeEmail(parsed.data.email);
|
||||
const user = await findUserByEmail(email);
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: messages.accountNotFound });
|
||||
}
|
||||
|
||||
if (!user.passwordHash) {
|
||||
return res.status(400).json({ error: messages.externalAccountOnly });
|
||||
}
|
||||
|
||||
const valid = await passwordComparator(parsed.data.password, user.passwordHash);
|
||||
if (!valid) {
|
||||
return res.status(401).json({ error: messages.invalidPassword });
|
||||
}
|
||||
|
||||
const sessionToken = randomBytes(32).toString("hex");
|
||||
const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
|
||||
await options.prisma.session.create({
|
||||
data: {
|
||||
sessionToken,
|
||||
userId: user.id,
|
||||
expires
|
||||
}
|
||||
});
|
||||
|
||||
res.cookie(options.sessionCookieName, sessionToken, {
|
||||
httpOnly: true,
|
||||
sameSite: "lax",
|
||||
secure: options.sessionCookieSecure,
|
||||
path: "/",
|
||||
expires
|
||||
});
|
||||
|
||||
return res.status(200).json({ ok: true });
|
||||
});
|
||||
|
||||
options.app.post(`${authApiBasePath}/password-reset/request`, async (req, res) => {
|
||||
const parsed = z
|
||||
.object({
|
||||
email: z.string().email()
|
||||
})
|
||||
.safeParse(req.body);
|
||||
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: messages.invalidPayload });
|
||||
}
|
||||
|
||||
if (!options.passwordReset?.enabled) {
|
||||
return res.status(503).json({ error: messages.passwordResetUnavailable });
|
||||
}
|
||||
|
||||
const email = normalizeEmail(parsed.data.email);
|
||||
const user = await findUserByEmail(email);
|
||||
if (!user?.email) {
|
||||
return res.status(200).json({ ok: true });
|
||||
}
|
||||
|
||||
const rawToken = randomBytes(32).toString("hex");
|
||||
const identifier = buildPasswordResetIdentifier(passwordResetIdentifierPrefix, user.id);
|
||||
const expiresAt = new Date(Date.now() + (options.passwordReset.tokenTtlMs ?? 2 * 60 * 60 * 1000));
|
||||
const resetUrl = options.passwordReset.buildResetUrl(rawToken);
|
||||
const isPasswordCreation = !user.passwordHash;
|
||||
|
||||
await options.prisma.verificationToken.deleteMany({
|
||||
where: {
|
||||
OR: [{ identifier }, { expires: { lt: new Date() } }]
|
||||
}
|
||||
});
|
||||
|
||||
await options.prisma.verificationToken.create({
|
||||
data: {
|
||||
identifier,
|
||||
token: hashPasswordResetToken(rawToken),
|
||||
expires: expiresAt
|
||||
}
|
||||
});
|
||||
|
||||
await options.passwordReset.sendMessage({
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
passwordHash: user.passwordHash
|
||||
},
|
||||
resetUrl,
|
||||
isPasswordCreation,
|
||||
expiresAt
|
||||
});
|
||||
|
||||
return res.status(200).json({ ok: true });
|
||||
});
|
||||
|
||||
options.app.get(`${authApiBasePath}/password-reset/validate`, async (req, res) => {
|
||||
if (!options.passwordReset?.enabled) {
|
||||
return res.status(400).json({ error: messages.expiredResetLink });
|
||||
}
|
||||
|
||||
const parsed = z.object({ token: z.string().min(1) }).safeParse({
|
||||
token: Array.isArray(req.query.token) ? req.query.token[0] : req.query.token
|
||||
});
|
||||
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: messages.invalidResetLink });
|
||||
}
|
||||
|
||||
const context = await getPasswordResetContext(parsed.data.token);
|
||||
if (!context) {
|
||||
return res.status(400).json({ error: messages.expiredResetLink });
|
||||
}
|
||||
|
||||
return res.status(200).json({
|
||||
ok: true,
|
||||
email: context.user.email,
|
||||
mode: context.user.passwordHash ? "reset" : "create"
|
||||
});
|
||||
});
|
||||
|
||||
options.app.post(`${authApiBasePath}/password-reset/confirm`, async (req, res) => {
|
||||
if (!options.passwordReset?.enabled) {
|
||||
return res.status(400).json({ error: messages.expiredResetLink });
|
||||
}
|
||||
|
||||
const parsed = z
|
||||
.object({
|
||||
token: z.string().min(1),
|
||||
password: z.string().min(8)
|
||||
})
|
||||
.safeParse(req.body);
|
||||
|
||||
if (!parsed.success) {
|
||||
return res.status(400).json({ error: messages.invalidPayload });
|
||||
}
|
||||
|
||||
const context = await getPasswordResetContext(parsed.data.token);
|
||||
if (!context) {
|
||||
return res.status(400).json({ error: messages.expiredResetLink });
|
||||
}
|
||||
|
||||
const passwordHash = await passwordHasher(parsed.data.password);
|
||||
await options.prisma.$transaction([
|
||||
options.prisma.verificationToken.deleteMany({
|
||||
where: { identifier: context.verificationToken.identifier }
|
||||
}),
|
||||
options.prisma.session.deleteMany({
|
||||
where: { userId: context.user.id }
|
||||
}),
|
||||
options.prisma.user.update({
|
||||
where: { id: context.user.id },
|
||||
data: {
|
||||
passwordHash,
|
||||
emailVerified: context.user.emailVerified ?? new Date()
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
await options.onPasswordResetConfirmed?.(context.user);
|
||||
return res.status(200).json({ ok: true });
|
||||
});
|
||||
|
||||
options.app.post(`${authApiBasePath}/logout`, async (req, res) => {
|
||||
const token = options.extractSessionToken(req.headers.cookie);
|
||||
|
||||
if (token) {
|
||||
await options.prisma.session.deleteMany({ where: { sessionToken: token } });
|
||||
}
|
||||
|
||||
const cookieNamesToClear = [
|
||||
options.sessionCookieName,
|
||||
...(options.extraCookieNamesToClear ?? []),
|
||||
"authjs.session-token",
|
||||
"__Secure-authjs.session-token",
|
||||
"next-auth.session-token",
|
||||
"__Secure-next-auth.session-token"
|
||||
];
|
||||
|
||||
for (const cookieName of cookieNamesToClear) {
|
||||
res.clearCookie(cookieName, { path: "/" });
|
||||
}
|
||||
|
||||
return res.status(200).json({ ok: true });
|
||||
});
|
||||
|
||||
options.app.get(mePath, options.requireSession, async (req, res) => {
|
||||
res.json({ user: (req as { authUser?: unknown }).authUser });
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user