first commit

This commit is contained in:
2026-04-01 15:17:46 +02:00
commit d50ab5f7bf
19 changed files with 2554 additions and 0 deletions

135
dist/react/index.d.ts vendored Normal file
View File

@@ -0,0 +1,135 @@
import * as react_jsx_runtime from 'react/jsx-runtime';
import { ReactNode } from 'react';
type AuthGuardProps = {
children: React.ReactNode;
fetchCurrentUser: () => Promise<unknown>;
redirectTo?: string;
loadingFallback?: React.ReactNode;
authenticatedWrapper?: (children: React.ReactNode) => React.ReactNode;
};
declare function AuthGuard({ children, fetchCurrentUser, redirectTo, loadingFallback, authenticatedWrapper }: AuthGuardProps): react_jsx_runtime.JSX.Element;
type AuthProviderKey = "google" | "slack" | (string & {});
type AuthProviderAvailability = Partial<Record<AuthProviderKey, boolean>>;
type LoginMode = "signIn" | "register";
type AuthSubmitValues = {
name: string;
email: string;
password: string;
passwordConfirm: string;
};
type PasswordResetMode = "reset" | "create";
type PasswordResetTokenState = {
status: "loading";
} | {
status: "invalid";
error: string;
} | {
status: "valid";
email: string;
mode: PasswordResetMode;
};
type CreateAuthClientOptions = {
apiUrl: (path: string) => string;
authUrl?: (path: string) => string;
fetchImpl?: typeof fetch;
credentials?: RequestCredentials;
defaultOAuthCallbackUrl?: string | (() => string);
};
type LoginInput = {
email: string;
password: string;
};
type RegisterInput = LoginInput & {
name: string;
};
declare function createAuthClient(options: CreateAuthClientOptions): {
getProviders(): Promise<AuthProviderAvailability>;
getCurrentUser<TUser>(): Promise<TUser>;
register(input: RegisterInput): Promise<void>;
login(input: LoginInput): Promise<void>;
requestPasswordReset(email: string): Promise<void>;
validatePasswordResetToken(token: string): Promise<{
email: string;
mode: PasswordResetMode;
}>;
confirmPasswordReset(input: {
token: string;
password: string;
}): Promise<void>;
logout(): Promise<void>;
startOAuthSignIn(provider: string, callbackUrl?: string): Promise<void>;
};
type LoginFormTexts = {
nameLabel: string;
emailLabel: string;
passwordLabel: string;
passwordConfirmLabel: string;
submitRegisterLabel: string;
submitSignInLabel: string;
toggleToRegisterLabel: string;
toggleToSignInLabel: string;
forgotPasswordLabel: string;
googleLabel: string;
slackLabel: string;
};
type LoginFormProps = {
mode: LoginMode;
texts: LoginFormTexts;
onSubmit: (values: AuthSubmitValues) => void | Promise<void>;
onModeToggle: () => void;
loading?: boolean;
oauthLoadingProvider?: AuthProviderKey | null;
providers?: AuthProviderAvailability;
onOAuthSignIn?: (provider: AuthProviderKey) => void | Promise<void>;
errorMessage?: string | null;
successMessage?: string | null;
footer?: ReactNode;
forgotPasswordLink?: ReactNode;
emailPlaceholder?: string;
namePlaceholder?: string;
};
declare function LoginForm({ mode, texts, onSubmit, onModeToggle, loading, oauthLoadingProvider, providers, onOAuthSignIn, errorMessage, successMessage, footer, forgotPasswordLink, emailPlaceholder, namePlaceholder }: LoginFormProps): react_jsx_runtime.JSX.Element;
type PasswordResetRequestTexts = {
emailLabel: string;
submitLabel: string;
requestSentMessage: string;
};
type PasswordResetRequestFormProps = {
texts: PasswordResetRequestTexts;
helperText: ReactNode;
loading?: boolean;
requestSent?: boolean;
onSubmit: (values: {
email: string;
}) => void | Promise<void>;
emailPlaceholder?: string;
};
type PasswordResetConfirmTexts = {
loadingLabel: string;
passwordLabel: string;
passwordConfirmLabel: string;
invalidLinkLabel: string;
resetSubmitLabel: string;
createSubmitLabel: string;
resetSuccessLabel: string;
createSuccessLabel: string;
};
type PasswordResetConfirmFormProps = {
texts: PasswordResetConfirmTexts;
tokenState: PasswordResetTokenState;
loading?: boolean;
completedMode?: PasswordResetMode | null;
onSubmit: (values: {
password: string;
passwordConfirm: string;
}) => void | Promise<void>;
};
declare function PasswordResetRequestForm({ texts, helperText, loading, requestSent, onSubmit, emailPlaceholder }: PasswordResetRequestFormProps): react_jsx_runtime.JSX.Element;
declare function PasswordResetConfirmForm({ texts, tokenState, loading, completedMode, onSubmit }: PasswordResetConfirmFormProps): react_jsx_runtime.JSX.Element;
export { AuthGuard, type AuthProviderAvailability, type AuthProviderKey, type AuthSubmitValues, LoginForm, type LoginMode, PasswordResetConfirmForm, type PasswordResetMode, PasswordResetRequestForm, type PasswordResetTokenState, createAuthClient };

371
dist/react/index.js vendored Normal file
View File

@@ -0,0 +1,371 @@
// react/AuthGuard.tsx
import { useEffect, useState } from "react";
import { Center, Spinner } from "@chakra-ui/react";
import { Navigate } from "react-router-dom";
import { Fragment, jsx } from "react/jsx-runtime";
function AuthGuard({
children,
fetchCurrentUser,
redirectTo = "/login",
loadingFallback,
authenticatedWrapper
}) {
const [state, setState] = useState({
loading: true,
authenticated: false
});
useEffect(() => {
let cancelled = false;
fetchCurrentUser().then(() => {
if (!cancelled) {
setState({ loading: false, authenticated: true });
}
}).catch(() => {
if (!cancelled) {
setState({ loading: false, authenticated: false });
}
});
return () => {
cancelled = true;
};
}, [fetchCurrentUser]);
if (state.loading) {
return /* @__PURE__ */ jsx(Fragment, { children: loadingFallback ?? /* @__PURE__ */ jsx(Center, { h: "var(--app-height)", children: /* @__PURE__ */ jsx(Spinner, { size: "xl" }) }) });
}
if (!state.authenticated) {
return /* @__PURE__ */ jsx(Navigate, { to: redirectTo, replace: true });
}
return /* @__PURE__ */ jsx(Fragment, { children: authenticatedWrapper ? authenticatedWrapper(children) : children });
}
// react/client.ts
async function readJsonError(response, fallback) {
const payload = await response.json().catch(() => null);
return new Error(payload?.error ?? fallback);
}
function createAuthClient(options) {
const fetchImpl = options.fetchImpl ?? fetch;
const authUrl = options.authUrl ?? options.apiUrl;
const credentials = options.credentials ?? "include";
function resolveDefaultOAuthCallbackUrl() {
const configured = options.defaultOAuthCallbackUrl;
if (typeof configured === "function") {
return configured();
}
if (typeof configured === "string" && configured.trim().length > 0) {
return configured;
}
return `${window.location.origin}/chat`;
}
async function request(path, init) {
return fetchImpl(options.apiUrl(path), {
...init,
credentials,
headers: {
"Content-Type": "application/json",
...init?.headers ?? {}
}
});
}
return {
async getProviders() {
const response = await request("/api/auth/providers");
if (!response.ok) {
throw await readJsonError(response, "providers_unavailable");
}
return await response.json();
},
async getCurrentUser() {
const response = await request("/api/me");
if (!response.ok) {
throw await readJsonError(response, "Unauthorized");
}
const payload = await response.json();
return payload.user;
},
async register(input) {
const response = await request("/api/auth/register", {
method: "POST",
body: JSON.stringify(input)
});
if (!response.ok) {
throw await readJsonError(response, "Registration failed");
}
},
async login(input) {
const response = await request("/api/auth/login", {
method: "POST",
body: JSON.stringify(input)
});
if (!response.ok) {
throw await readJsonError(response, "Sign in failed");
}
},
async requestPasswordReset(email) {
const response = await request("/api/auth/password-reset/request", {
method: "POST",
body: JSON.stringify({ email })
});
if (!response.ok) {
throw await readJsonError(response, "Password reset request failed");
}
},
async validatePasswordResetToken(token) {
const response = await request(`/api/auth/password-reset/validate?token=${encodeURIComponent(token)}`, {
headers: {}
});
const payload = await response.json().catch(() => null);
if (!response.ok || !payload?.email || payload.mode !== "reset" && payload.mode !== "create") {
throw new Error(payload?.error ?? "Invalid reset link");
}
return {
email: payload.email,
mode: payload.mode
};
},
async confirmPasswordReset(input) {
const response = await request("/api/auth/password-reset/confirm", {
method: "POST",
body: JSON.stringify(input)
});
if (!response.ok) {
throw await readJsonError(response, "Invalid reset link");
}
},
async logout() {
const response = await request("/api/auth/logout", {
method: "POST"
});
if (!response.ok) {
throw await readJsonError(response, "Logout failed");
}
},
async startOAuthSignIn(provider, callbackUrl = resolveDefaultOAuthCallbackUrl()) {
const response = await fetchImpl(authUrl("/auth/csrf"), {
credentials
});
if (!response.ok) {
throw await readJsonError(response, "Sign in failed");
}
const payload = await response.json();
if (!payload.csrfToken) {
throw new Error("Sign in failed");
}
const form = document.createElement("form");
form.method = "POST";
form.action = authUrl(`/auth/signin/${provider}`);
form.style.display = "none";
const csrfInput = document.createElement("input");
csrfInput.type = "hidden";
csrfInput.name = "csrfToken";
csrfInput.value = payload.csrfToken;
form.appendChild(csrfInput);
const callbackInput = document.createElement("input");
callbackInput.type = "hidden";
callbackInput.name = "callbackUrl";
callbackInput.value = callbackUrl;
form.appendChild(callbackInput);
document.body.appendChild(form);
form.submit();
}
};
}
// react/LoginForm.tsx
import { Alert, AlertDescription, AlertIcon, Button, Center as Center2, FormControl, FormLabel, HStack, Icon, Input, Stack } from "@chakra-ui/react";
import { FcGoogle } from "react-icons/fc";
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
function LoginForm({
mode,
texts,
onSubmit,
onModeToggle,
loading = false,
oauthLoadingProvider = null,
providers,
onOAuthSignIn,
errorMessage,
successMessage,
footer,
forgotPasswordLink,
emailPlaceholder = "you@example.com",
namePlaceholder = "Jane Doe"
}) {
const registerMode = mode === "register";
function handleSubmit(event) {
event.preventDefault();
const form = new FormData(event.currentTarget);
void onSubmit({
name: String(form.get("name") ?? ""),
email: String(form.get("email") ?? ""),
password: String(form.get("password") ?? ""),
passwordConfirm: String(form.get("passwordConfirm") ?? "")
});
}
return /* @__PURE__ */ jsxs(Stack, { spacing: 5, children: [
errorMessage ? /* @__PURE__ */ jsxs(Alert, { status: "error", borderRadius: "md", children: [
/* @__PURE__ */ jsx2(AlertIcon, {}),
/* @__PURE__ */ jsx2(AlertDescription, { children: errorMessage })
] }) : null,
successMessage ? /* @__PURE__ */ jsxs(Alert, { status: "success", borderRadius: "md", children: [
/* @__PURE__ */ jsx2(AlertIcon, {}),
/* @__PURE__ */ jsx2(AlertDescription, { children: successMessage })
] }) : null,
/* @__PURE__ */ jsx2("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs(Stack, { spacing: 4, children: [
registerMode ? /* @__PURE__ */ jsxs(FormControl, { isRequired: true, children: [
/* @__PURE__ */ jsx2(FormLabel, { children: texts.nameLabel }),
/* @__PURE__ */ jsx2(Input, { name: "name", placeholder: namePlaceholder })
] }) : null,
/* @__PURE__ */ jsxs(FormControl, { isRequired: true, children: [
/* @__PURE__ */ jsx2(FormLabel, { children: texts.emailLabel }),
/* @__PURE__ */ jsx2(Input, { name: "email", type: "email", placeholder: emailPlaceholder })
] }),
/* @__PURE__ */ jsxs(FormControl, { isRequired: true, children: [
/* @__PURE__ */ jsx2(FormLabel, { children: texts.passwordLabel }),
/* @__PURE__ */ jsx2(Input, { name: "password", type: "password", minLength: 8 })
] }),
!registerMode && forgotPasswordLink ? /* @__PURE__ */ jsx2(Stack, { align: "flex-end", children: forgotPasswordLink }) : null,
registerMode ? /* @__PURE__ */ jsxs(FormControl, { isRequired: true, children: [
/* @__PURE__ */ jsx2(FormLabel, { children: texts.passwordConfirmLabel }),
/* @__PURE__ */ jsx2(Input, { name: "passwordConfirm", type: "password", minLength: 8 })
] }) : null,
/* @__PURE__ */ jsx2(Button, { type: "submit", isLoading: loading, children: registerMode ? texts.submitRegisterLabel : texts.submitSignInLabel })
] }) }),
registerMode || !onOAuthSignIn || !providers?.google && !providers?.slack ? null : /* @__PURE__ */ jsxs(HStack, { children: [
providers.google ? /* @__PURE__ */ jsx2(
Button,
{
flex: 1,
bg: "gray.200",
color: "gray.900",
borderRadius: "full",
justifyContent: "flex-start",
h: "56px",
px: 3,
fontSize: { base: "md", md: "lg" },
fontWeight: "semibold",
iconSpacing: 4,
leftIcon: /* @__PURE__ */ jsx2(Center2, { boxSize: "40px", bg: "white", borderRadius: "full", boxShadow: "sm", children: /* @__PURE__ */ jsx2(Icon, { as: FcGoogle, boxSize: 6 }) }),
_hover: { bg: "gray.300" },
_active: { bg: "gray.300" },
isLoading: oauthLoadingProvider === "google",
onClick: () => void onOAuthSignIn("google"),
children: texts.googleLabel
}
) : null,
providers.slack ? /* @__PURE__ */ jsx2(
Button,
{
flex: 1,
variant: "outline",
isLoading: oauthLoadingProvider === "slack",
onClick: () => void onOAuthSignIn("slack"),
children: texts.slackLabel
}
) : null
] }),
/* @__PURE__ */ jsx2(Button, { variant: "ghost", onClick: onModeToggle, children: registerMode ? texts.toggleToSignInLabel : texts.toggleToRegisterLabel }),
footer ?? null
] });
}
// react/PasswordResetForms.tsx
import {
Alert as Alert2,
AlertDescription as AlertDescription2,
AlertIcon as AlertIcon2,
Button as Button2,
FormControl as FormControl2,
FormLabel as FormLabel2,
Input as Input2,
Spinner as Spinner2,
Stack as Stack2,
Text
} from "@chakra-ui/react";
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
function PasswordResetRequestForm({
texts,
helperText,
loading = false,
requestSent = false,
onSubmit,
emailPlaceholder = "you@example.com"
}) {
function handleSubmit(event) {
event.preventDefault();
const form = new FormData(event.currentTarget);
void onSubmit({
email: String(form.get("email") ?? "")
});
}
return /* @__PURE__ */ jsxs2(Stack2, { spacing: 5, children: [
requestSent ? /* @__PURE__ */ jsxs2(Alert2, { status: "success", borderRadius: "md", children: [
/* @__PURE__ */ jsx3(AlertIcon2, {}),
/* @__PURE__ */ jsx3(AlertDescription2, { children: texts.requestSentMessage })
] }) : null,
/* @__PURE__ */ jsx3("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs2(Stack2, { spacing: 4, children: [
/* @__PURE__ */ jsxs2(FormControl2, { isRequired: true, children: [
/* @__PURE__ */ jsx3(FormLabel2, { children: texts.emailLabel }),
/* @__PURE__ */ jsx3(Input2, { name: "email", type: "email", placeholder: emailPlaceholder })
] }),
/* @__PURE__ */ jsx3(Text, { fontSize: "sm", color: "gray.600", children: helperText }),
/* @__PURE__ */ jsx3(Button2, { type: "submit", isLoading: loading, children: texts.submitLabel })
] }) })
] });
}
function PasswordResetConfirmForm({
texts,
tokenState,
loading = false,
completedMode = null,
onSubmit
}) {
function handleSubmit(event) {
event.preventDefault();
const form = new FormData(event.currentTarget);
void onSubmit({
password: String(form.get("password") ?? ""),
passwordConfirm: String(form.get("passwordConfirm") ?? "")
});
}
if (tokenState.status === "loading") {
return /* @__PURE__ */ jsxs2(Stack2, { align: "center", py: 6, spacing: 3, children: [
/* @__PURE__ */ jsx3(Spinner2, {}),
/* @__PURE__ */ jsx3(Text, { color: "gray.600", children: texts.loadingLabel })
] });
}
if (tokenState.status === "invalid") {
return /* @__PURE__ */ jsxs2(Alert2, { status: "error", borderRadius: "md", children: [
/* @__PURE__ */ jsx3(AlertIcon2, {}),
/* @__PURE__ */ jsx3(AlertDescription2, { children: tokenState.error || texts.invalidLinkLabel })
] });
}
if (completedMode !== null) {
return /* @__PURE__ */ jsxs2(Alert2, { status: "success", borderRadius: "md", children: [
/* @__PURE__ */ jsx3(AlertIcon2, {}),
/* @__PURE__ */ jsx3(AlertDescription2, { children: completedMode === "create" ? texts.createSuccessLabel : texts.resetSuccessLabel })
] });
}
return /* @__PURE__ */ jsxs2(Stack2, { spacing: 4, children: [
/* @__PURE__ */ jsx3(Text, { fontSize: "sm", color: "gray.600", children: tokenState.email }),
/* @__PURE__ */ jsx3("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs2(Stack2, { spacing: 4, children: [
/* @__PURE__ */ jsxs2(FormControl2, { isRequired: true, children: [
/* @__PURE__ */ jsx3(FormLabel2, { children: texts.passwordLabel }),
/* @__PURE__ */ jsx3(Input2, { name: "password", type: "password", minLength: 8 })
] }),
/* @__PURE__ */ jsxs2(FormControl2, { isRequired: true, children: [
/* @__PURE__ */ jsx3(FormLabel2, { children: texts.passwordConfirmLabel }),
/* @__PURE__ */ jsx3(Input2, { name: "passwordConfirm", type: "password", minLength: 8 })
] }),
/* @__PURE__ */ jsx3(Button2, { type: "submit", isLoading: loading, children: tokenState.mode === "create" ? texts.createSubmitLabel : texts.resetSubmitLabel })
] }) })
] });
}
export {
AuthGuard,
LoginForm,
PasswordResetConfirmForm,
PasswordResetRequestForm,
createAuthClient
};
//# sourceMappingURL=index.js.map

1
dist/react/index.js.map vendored Normal file

File diff suppressed because one or more lines are too long

136
dist/server/index.d.ts vendored Normal file
View File

@@ -0,0 +1,136 @@
import * as qs from 'qs';
import * as express_serve_static_core from 'express-serve-static-core';
import * as express from 'express';
import { Request, RequestHandler, Express } from 'express';
import * as _auth_core_adapters from '@auth/core/adapters';
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;
};
declare function createAuthModule<TAuthUser>(options: CreateAuthModuleOptions<TAuthUser>): {
authConfig: {
adapter: _auth_core_adapters.Adapter;
trustHost: boolean;
debug: boolean;
logger: {
error(error: Error): void;
warn(code: string): void;
debug(message: string, metadata?: unknown): void;
} | undefined;
session: {
strategy: "database";
};
secret: string | undefined;
cookies: {
sessionToken: {
name: string;
options: {
httpOnly: boolean;
sameSite: "lax";
path: string;
secure: boolean;
};
};
};
providers: any[];
pages: {
signIn: string;
};
callbacks: {
redirect: ({ url, baseUrl }: {
url: string;
baseUrl: string;
}) => Promise<string>;
};
};
authHandler: (req: Request, res: express.Response, next: express.NextFunction) => Promise<void>;
requireSession: RequestHandler<express_serve_static_core.ParamsDictionary, any, any, qs.ParsedQs, Record<string, any>>;
extractSessionToken: (cookieHeader: string | undefined) => string | null;
googleAuthEnabled: boolean;
slackAuthEnabled: boolean;
};
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;
};
declare function registerAuthApiRoutes(options: RegisterAuthApiRoutesOptions): void;
export { createAuthModule, registerAuthApiRoutes };

489
dist/server/index.js vendored Normal file
View File

@@ -0,0 +1,489 @@
// server/module.ts
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 { z } from "zod";
function parseBoolean(value, fallback) {
if (value === void 0) {
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;
}
function createAuthModule(options) {
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 = [
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) {
console.error("[authjs:error]", error.name, error.message, error.cause ?? "");
},
warn(code) {
console.warn("[authjs:warn]", code);
},
debug(message, metadata) {
console.log("[authjs:debug]", message, metadata ?? "");
}
} : void 0,
session: { strategy: "database" },
secret: options.authSecret ?? process.env.AUTH_SECRET,
cookies: {
sessionToken: {
name: options.sessionCookieName,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: options.sessionCookieSecure
}
}
},
providers,
pages: {
signIn: signInPath
},
callbacks: {
redirect: async ({ url, baseUrl }) => {
const clientOrigin = new URL(options.clientUrl).origin;
const successUrl = new URL(authenticatedRedirectPath, clientOrigin).toString();
const shouldForceChat = (pathname, searchParams) => {
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 = 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: "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 <= /* @__PURE__ */ new Date()) {
const payload = { 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.authUser = authUser;
await options.onSessionValidated?.(authUser);
next();
};
const extractSessionToken = (cookieHeader) => {
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))];
}).filter((entry) => 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];
}).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
};
}
// server/routes.ts
import { createHash, randomBytes } from "crypto";
import { z as z2 } from "zod";
var defaultNormalizeEmail = (email) => email.trim();
var defaultPasswordResetIdentifierPrefix = "password-reset:";
var defaultMessages = {
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) {
return createHash("sha256").update(token).digest("hex");
}
function buildPasswordResetIdentifier(prefix, userId) {
return `${prefix}${userId}`;
}
function registerAuthApiRoutes(options) {
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) => Promise.resolve(password));
const passwordComparator = options.passwordComparator ?? ((password, hash) => Promise.resolve(password === hash));
const passwordResetIdentifierPrefix = options.passwordReset?.identifierPrefix ?? defaultPasswordResetIdentifierPrefix;
const messages = { ...defaultMessages, ...options.messages ?? {} };
const findUserByEmail = async (email) => {
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) => {
const verificationToken = await options.prisma.verificationToken.findUnique({
where: { token: hashPasswordResetToken(rawToken) },
select: { identifier: true, expires: true }
});
if (!verificationToken || verificationToken.expires <= /* @__PURE__ */ 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 = z2.object({
name: z2.string().min(2).max(60),
email: z2.string().email(),
password: z2.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 = z2.object({
email: z2.string().email(),
password: z2.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 * 1e3);
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 = z2.object({
email: z2.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 * 1e3));
const resetUrl = options.passwordReset.buildResetUrl(rawToken);
const isPasswordCreation = !user.passwordHash;
await options.prisma.verificationToken.deleteMany({
where: {
OR: [{ identifier }, { expires: { lt: /* @__PURE__ */ 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 = z2.object({ token: z2.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 = z2.object({
token: z2.string().min(1),
password: z2.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 ?? /* @__PURE__ */ 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.authUser });
});
}
export {
createAuthModule,
registerAuthApiRoutes
};
//# sourceMappingURL=index.js.map

1
dist/server/index.js.map vendored Normal file

File diff suppressed because one or more lines are too long