From 18bf4e60d1ee04ee65a485933fba0c0f3bddc2eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fre=CC=81de=CC=81ric=20Jean?= Date: Sun, 19 Jul 2026 19:40:38 +0200 Subject: [PATCH] Ajoute le flux d'invitation avec activation de compte --- dist/react/index.d.ts | 47 ++++++++- dist/react/index.js | 202 ++++++++++++++++++++++++++----------- dist/react/index.js.map | 2 +- dist/server/index.d.ts | 20 +++- dist/server/index.js | 179 ++++++++++++++++++++++++++++---- dist/server/index.js.map | 2 +- react/InviteAcceptForm.tsx | 109 ++++++++++++++++++++ react/client.ts | 30 ++++++ react/index.ts | 2 + react/types.ts | 5 + server/index.ts | 1 + server/invites.ts | 109 ++++++++++++++++++++ server/routes.ts | 136 +++++++++++++++++++++---- 13 files changed, 747 insertions(+), 97 deletions(-) create mode 100644 react/InviteAcceptForm.tsx create mode 100644 server/invites.ts diff --git a/dist/react/index.d.ts b/dist/react/index.d.ts index 54738cf..479bd18 100644 --- a/dist/react/index.d.ts +++ b/dist/react/index.d.ts @@ -30,6 +30,16 @@ type PasswordResetTokenState = { email: string; mode: PasswordResetMode; }; +type AccountInviteTokenState = { + status: "loading"; +} | { + status: "invalid"; + error: string; +} | { + status: "valid"; + email: string; + name: string | null; +}; type CreateAuthClientOptions = { apiUrl: (path: string) => string; @@ -59,10 +69,45 @@ declare function createAuthClient(options: CreateAuthClientOptions): { token: string; password: string; }): Promise; + validateAccountInviteToken(token: string): Promise<{ + email: string; + name: string | null; + }>; + acceptAccountInvite(input: { + token: string; + name: string; + password: string; + }): Promise; logout(): Promise; startOAuthSignIn(provider: string, callbackUrl?: string): Promise; }; +type InviteAcceptFormTexts = { + loadingLabel: string; + invalidLinkLabel: string; + emailLabel: string; + nameLabel: string; + passwordLabel: string; + passwordConfirmLabel: string; + submitLabel: string; + googleLabel: string; +}; +type InviteAcceptFormProps = { + texts: InviteAcceptFormTexts; + tokenState: AccountInviteTokenState; + loading?: boolean; + initialName?: string | null; + showGoogleSignIn?: boolean; + googleLoading?: boolean; + onSubmit: (values: { + name: string; + password: string; + passwordConfirm: string; + }) => void | Promise; + onGoogleSignIn?: () => void | Promise; +}; +declare function InviteAcceptForm({ texts, tokenState, loading, initialName, showGoogleSignIn, googleLoading, onSubmit, onGoogleSignIn }: InviteAcceptFormProps): react_jsx_runtime.JSX.Element; + type LoginFormTexts = { nameLabel: string; emailLabel: string; @@ -132,4 +177,4 @@ type PasswordResetConfirmFormProps = { 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 }; +export { type AccountInviteTokenState, AuthGuard, type AuthProviderAvailability, type AuthProviderKey, type AuthSubmitValues, InviteAcceptForm, LoginForm, type LoginMode, PasswordResetConfirmForm, type PasswordResetMode, PasswordResetRequestForm, type PasswordResetTokenState, createAuthClient }; diff --git a/dist/react/index.js b/dist/react/index.js index 68f5f97..ac74fd4 100644 --- a/dist/react/index.js +++ b/dist/react/index.js @@ -209,6 +209,28 @@ function createAuthClient(options) { throw await readJsonError(response, "Invalid reset link"); } }, + async validateAccountInviteToken(token) { + const response = await request(`/api/auth/invite/validate?token=${encodeURIComponent(token)}`, { + headers: {} + }); + const payload = await response.json().catch(() => null); + if (!response.ok || !payload?.email) { + throw new Error(payload?.error ?? "Invalid invite link"); + } + return { + email: payload.email, + name: payload.name ?? null + }; + }, + async acceptAccountInvite(input) { + const response = await request("/api/auth/invite/accept", { + method: "POST", + body: JSON.stringify(input) + }); + if (!response.ok) { + throw await readJsonError(response, "Invalid invite link"); + } + }, async logout() { const response = await request("/api/auth/logout", { method: "POST" @@ -248,9 +270,72 @@ function createAuthClient(options) { }; } -// react/LoginForm.tsx +// react/InviteAcceptForm.tsx +import { useEffect as useEffect2, useState as useState2 } from "react"; import { FcGoogle } from "react-icons/fc"; import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime"; +function InviteAcceptForm({ + texts, + tokenState, + loading = false, + initialName = null, + showGoogleSignIn = false, + googleLoading = false, + onSubmit, + onGoogleSignIn +}) { + const [name, setName] = useState2(initialName ?? ""); + useEffect2(() => { + setName(initialName ?? ""); + }, [initialName, tokenState.status]); + function handleSubmit(event) { + event.preventDefault(); + const form = new FormData(event.currentTarget); + void onSubmit({ + name: String(form.get("name") ?? ""), + password: String(form.get("password") ?? ""), + passwordConfirm: String(form.get("passwordConfirm") ?? "") + }); + } + if (tokenState.status === "loading") { + return /* @__PURE__ */ jsxs2(Stack, { align: "center", py: 6, gap: 3, children: [ + /* @__PURE__ */ jsx3(Spinner, {}), + /* @__PURE__ */ jsx3(Text, { color: "gray.600", children: texts.loadingLabel }) + ] }); + } + if (tokenState.status === "invalid") { + return /* @__PURE__ */ jsxs2(Alert, { status: "error", borderRadius: "md", children: [ + /* @__PURE__ */ jsx3(AlertIcon, {}), + /* @__PURE__ */ jsx3(AlertDescription, { children: tokenState.error || texts.invalidLinkLabel }) + ] }); + } + return /* @__PURE__ */ jsxs2(Stack, { gap: 4, children: [ + /* @__PURE__ */ jsxs2(FormControl, { children: [ + /* @__PURE__ */ jsx3(FormLabel, { children: texts.emailLabel }), + /* @__PURE__ */ jsx3(Input, { value: tokenState.email, readOnly: true, disabled: true }) + ] }), + /* @__PURE__ */ jsx3("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs2(Stack, { gap: 4, children: [ + /* @__PURE__ */ jsxs2(FormControl, { isRequired: true, children: [ + /* @__PURE__ */ jsx3(FormLabel, { children: texts.nameLabel }), + /* @__PURE__ */ jsx3(Input, { name: "name", value: name, onChange: (event) => setName(event.target.value) }) + ] }), + /* @__PURE__ */ jsxs2(FormControl, { isRequired: true, children: [ + /* @__PURE__ */ jsx3(FormLabel, { children: texts.passwordLabel }), + /* @__PURE__ */ jsx3(Input, { name: "password", type: "password", minLength: 8 }) + ] }), + /* @__PURE__ */ jsxs2(FormControl, { isRequired: true, children: [ + /* @__PURE__ */ jsx3(FormLabel, { children: texts.passwordConfirmLabel }), + /* @__PURE__ */ jsx3(Input, { name: "passwordConfirm", type: "password", minLength: 8 }) + ] }), + /* @__PURE__ */ jsx3(Button, { type: "submit", loading, children: texts.submitLabel }) + ] }) }), + showGoogleSignIn && onGoogleSignIn ? /* @__PURE__ */ jsx3(Button, { variant: "outline", loading: googleLoading, leftIcon: /* @__PURE__ */ jsx3(FcGoogle, {}), onClick: () => void onGoogleSignIn(), children: texts.googleLabel }) : null + ] }); +} + +// react/LoginForm.tsx +import { FcGoogle as FcGoogle2 } from "react-icons/fc"; +import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime"; function LoginForm({ mode, texts, @@ -278,37 +363,37 @@ function LoginForm({ passwordConfirm: String(form.get("passwordConfirm") ?? "") }); } - return /* @__PURE__ */ jsxs2(Stack, { spacing: 5, children: [ - errorMessage ? /* @__PURE__ */ jsxs2(Alert, { status: "error", borderRadius: "md", children: [ - /* @__PURE__ */ jsx3(AlertIcon, {}), - /* @__PURE__ */ jsx3(AlertDescription, { children: errorMessage }) + return /* @__PURE__ */ jsxs3(Stack, { spacing: 5, children: [ + errorMessage ? /* @__PURE__ */ jsxs3(Alert, { status: "error", borderRadius: "md", children: [ + /* @__PURE__ */ jsx4(AlertIcon, {}), + /* @__PURE__ */ jsx4(AlertDescription, { children: errorMessage }) ] }) : null, - successMessage ? /* @__PURE__ */ jsxs2(Alert, { status: "success", borderRadius: "md", children: [ - /* @__PURE__ */ jsx3(AlertIcon, {}), - /* @__PURE__ */ jsx3(AlertDescription, { children: successMessage }) + successMessage ? /* @__PURE__ */ jsxs3(Alert, { status: "success", borderRadius: "md", children: [ + /* @__PURE__ */ jsx4(AlertIcon, {}), + /* @__PURE__ */ jsx4(AlertDescription, { children: successMessage }) ] }) : null, - /* @__PURE__ */ jsx3("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs2(Stack, { spacing: 4, children: [ - registerMode ? /* @__PURE__ */ jsxs2(FormControl, { isRequired: true, children: [ - /* @__PURE__ */ jsx3(FormLabel, { children: texts.nameLabel }), - /* @__PURE__ */ jsx3(Input, { name: "name", placeholder: namePlaceholder }) + /* @__PURE__ */ jsx4("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs3(Stack, { spacing: 4, children: [ + registerMode ? /* @__PURE__ */ jsxs3(FormControl, { isRequired: true, children: [ + /* @__PURE__ */ jsx4(FormLabel, { children: texts.nameLabel }), + /* @__PURE__ */ jsx4(Input, { name: "name", placeholder: namePlaceholder }) ] }) : null, - /* @__PURE__ */ jsxs2(FormControl, { isRequired: true, children: [ - /* @__PURE__ */ jsx3(FormLabel, { children: texts.emailLabel }), - /* @__PURE__ */ jsx3(Input, { name: "email", type: "email", placeholder: emailPlaceholder }) + /* @__PURE__ */ jsxs3(FormControl, { isRequired: true, children: [ + /* @__PURE__ */ jsx4(FormLabel, { children: texts.emailLabel }), + /* @__PURE__ */ jsx4(Input, { name: "email", type: "email", placeholder: emailPlaceholder }) ] }), - /* @__PURE__ */ jsxs2(FormControl, { isRequired: true, children: [ - /* @__PURE__ */ jsx3(FormLabel, { children: texts.passwordLabel }), - /* @__PURE__ */ jsx3(Input, { name: "password", type: "password", minLength: 8 }) + /* @__PURE__ */ jsxs3(FormControl, { isRequired: true, children: [ + /* @__PURE__ */ jsx4(FormLabel, { children: texts.passwordLabel }), + /* @__PURE__ */ jsx4(Input, { name: "password", type: "password", minLength: 8 }) ] }), - !registerMode && forgotPasswordLink ? /* @__PURE__ */ jsx3(Stack, { align: "flex-end", children: forgotPasswordLink }) : null, - registerMode ? /* @__PURE__ */ jsxs2(FormControl, { isRequired: true, children: [ - /* @__PURE__ */ jsx3(FormLabel, { children: texts.passwordConfirmLabel }), - /* @__PURE__ */ jsx3(Input, { name: "passwordConfirm", type: "password", minLength: 8 }) + !registerMode && forgotPasswordLink ? /* @__PURE__ */ jsx4(Stack, { align: "flex-end", children: forgotPasswordLink }) : null, + registerMode ? /* @__PURE__ */ jsxs3(FormControl, { isRequired: true, children: [ + /* @__PURE__ */ jsx4(FormLabel, { children: texts.passwordConfirmLabel }), + /* @__PURE__ */ jsx4(Input, { name: "passwordConfirm", type: "password", minLength: 8 }) ] }) : null, - /* @__PURE__ */ jsx3(Button, { type: "submit", isLoading: loading, children: registerMode ? texts.submitRegisterLabel : texts.submitSignInLabel }) + /* @__PURE__ */ jsx4(Button, { type: "submit", isLoading: loading, children: registerMode ? texts.submitRegisterLabel : texts.submitSignInLabel }) ] }) }), - registerMode || !onOAuthSignIn || !providers?.google && !providers?.slack ? null : /* @__PURE__ */ jsxs2(HStack, { children: [ - providers.google ? /* @__PURE__ */ jsx3( + registerMode || !onOAuthSignIn || !providers?.google && !providers?.slack ? null : /* @__PURE__ */ jsxs3(HStack, { children: [ + providers.google ? /* @__PURE__ */ jsx4( Button, { flex: 1, @@ -321,7 +406,7 @@ function LoginForm({ fontSize: { base: "md", md: "lg" }, fontWeight: "semibold", iconSpacing: 4, - leftIcon: /* @__PURE__ */ jsx3(Center, { boxSize: "40px", bg: "white", borderRadius: "full", boxShadow: "sm", children: /* @__PURE__ */ jsx3(Icon, { as: FcGoogle, boxSize: 6 }) }), + leftIcon: /* @__PURE__ */ jsx4(Center, { boxSize: "40px", bg: "white", borderRadius: "full", boxShadow: "sm", children: /* @__PURE__ */ jsx4(Icon, { as: FcGoogle2, boxSize: 6 }) }), _hover: { bg: "gray.300" }, _active: { bg: "gray.300" }, isLoading: oauthLoadingProvider === "google", @@ -329,7 +414,7 @@ function LoginForm({ children: texts.googleLabel } ) : null, - providers.slack ? /* @__PURE__ */ jsx3( + providers.slack ? /* @__PURE__ */ jsx4( Button, { flex: 1, @@ -340,13 +425,13 @@ function LoginForm({ } ) : null ] }), - /* @__PURE__ */ jsx3(Button, { variant: "ghost", onClick: onModeToggle, children: registerMode ? texts.toggleToSignInLabel : texts.toggleToRegisterLabel }), + /* @__PURE__ */ jsx4(Button, { variant: "ghost", onClick: onModeToggle, children: registerMode ? texts.toggleToSignInLabel : texts.toggleToRegisterLabel }), footer ?? null ] }); } // react/PasswordResetForms.tsx -import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime"; +import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime"; function PasswordResetRequestForm({ texts, helperText, @@ -362,18 +447,18 @@ function PasswordResetRequestForm({ email: String(form.get("email") ?? "") }); } - return /* @__PURE__ */ jsxs3(Stack, { spacing: 5, children: [ - requestSent ? /* @__PURE__ */ jsxs3(Alert, { status: "success", borderRadius: "md", children: [ - /* @__PURE__ */ jsx4(AlertIcon, {}), - /* @__PURE__ */ jsx4(AlertDescription, { children: texts.requestSentMessage }) + return /* @__PURE__ */ jsxs4(Stack, { spacing: 5, children: [ + requestSent ? /* @__PURE__ */ jsxs4(Alert, { status: "success", borderRadius: "md", children: [ + /* @__PURE__ */ jsx5(AlertIcon, {}), + /* @__PURE__ */ jsx5(AlertDescription, { children: texts.requestSentMessage }) ] }) : null, - /* @__PURE__ */ jsx4("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs3(Stack, { spacing: 4, children: [ - /* @__PURE__ */ jsxs3(FormControl, { isRequired: true, children: [ - /* @__PURE__ */ jsx4(FormLabel, { children: texts.emailLabel }), - /* @__PURE__ */ jsx4(Input, { name: "email", type: "email", placeholder: emailPlaceholder }) + /* @__PURE__ */ jsx5("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs4(Stack, { spacing: 4, children: [ + /* @__PURE__ */ jsxs4(FormControl, { isRequired: true, children: [ + /* @__PURE__ */ jsx5(FormLabel, { children: texts.emailLabel }), + /* @__PURE__ */ jsx5(Input, { name: "email", type: "email", placeholder: emailPlaceholder }) ] }), - /* @__PURE__ */ jsx4(Text, { fontSize: "sm", color: "gray.600", children: helperText }), - /* @__PURE__ */ jsx4(Button, { type: "submit", isLoading: loading, children: texts.submitLabel }) + /* @__PURE__ */ jsx5(Text, { fontSize: "sm", color: "gray.600", children: helperText }), + /* @__PURE__ */ jsx5(Button, { type: "submit", isLoading: loading, children: texts.submitLabel }) ] }) }) ] }); } @@ -393,40 +478,41 @@ function PasswordResetConfirmForm({ }); } if (tokenState.status === "loading") { - return /* @__PURE__ */ jsxs3(Stack, { align: "center", py: 6, spacing: 3, children: [ - /* @__PURE__ */ jsx4(Spinner, {}), - /* @__PURE__ */ jsx4(Text, { color: "gray.600", children: texts.loadingLabel }) + return /* @__PURE__ */ jsxs4(Stack, { align: "center", py: 6, spacing: 3, children: [ + /* @__PURE__ */ jsx5(Spinner, {}), + /* @__PURE__ */ jsx5(Text, { color: "gray.600", children: texts.loadingLabel }) ] }); } if (tokenState.status === "invalid") { - return /* @__PURE__ */ jsxs3(Alert, { status: "error", borderRadius: "md", children: [ - /* @__PURE__ */ jsx4(AlertIcon, {}), - /* @__PURE__ */ jsx4(AlertDescription, { children: tokenState.error || texts.invalidLinkLabel }) + return /* @__PURE__ */ jsxs4(Alert, { status: "error", borderRadius: "md", children: [ + /* @__PURE__ */ jsx5(AlertIcon, {}), + /* @__PURE__ */ jsx5(AlertDescription, { children: tokenState.error || texts.invalidLinkLabel }) ] }); } if (completedMode !== null) { - return /* @__PURE__ */ jsxs3(Alert, { status: "success", borderRadius: "md", children: [ - /* @__PURE__ */ jsx4(AlertIcon, {}), - /* @__PURE__ */ jsx4(AlertDescription, { children: completedMode === "create" ? texts.createSuccessLabel : texts.resetSuccessLabel }) + return /* @__PURE__ */ jsxs4(Alert, { status: "success", borderRadius: "md", children: [ + /* @__PURE__ */ jsx5(AlertIcon, {}), + /* @__PURE__ */ jsx5(AlertDescription, { children: completedMode === "create" ? texts.createSuccessLabel : texts.resetSuccessLabel }) ] }); } - return /* @__PURE__ */ jsxs3(Stack, { spacing: 4, children: [ - /* @__PURE__ */ jsx4(Text, { fontSize: "sm", color: "gray.600", children: tokenState.email }), - /* @__PURE__ */ jsx4("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs3(Stack, { spacing: 4, children: [ - /* @__PURE__ */ jsxs3(FormControl, { isRequired: true, children: [ - /* @__PURE__ */ jsx4(FormLabel, { children: texts.passwordLabel }), - /* @__PURE__ */ jsx4(Input, { name: "password", type: "password", minLength: 8 }) + return /* @__PURE__ */ jsxs4(Stack, { spacing: 4, children: [ + /* @__PURE__ */ jsx5(Text, { fontSize: "sm", color: "gray.600", children: tokenState.email }), + /* @__PURE__ */ jsx5("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxs4(Stack, { spacing: 4, children: [ + /* @__PURE__ */ jsxs4(FormControl, { isRequired: true, children: [ + /* @__PURE__ */ jsx5(FormLabel, { children: texts.passwordLabel }), + /* @__PURE__ */ jsx5(Input, { name: "password", type: "password", minLength: 8 }) ] }), - /* @__PURE__ */ jsxs3(FormControl, { isRequired: true, children: [ - /* @__PURE__ */ jsx4(FormLabel, { children: texts.passwordConfirmLabel }), - /* @__PURE__ */ jsx4(Input, { name: "passwordConfirm", type: "password", minLength: 8 }) + /* @__PURE__ */ jsxs4(FormControl, { isRequired: true, children: [ + /* @__PURE__ */ jsx5(FormLabel, { children: texts.passwordConfirmLabel }), + /* @__PURE__ */ jsx5(Input, { name: "passwordConfirm", type: "password", minLength: 8 }) ] }), - /* @__PURE__ */ jsx4(Button, { type: "submit", isLoading: loading, children: tokenState.mode === "create" ? texts.createSubmitLabel : texts.resetSubmitLabel }) + /* @__PURE__ */ jsx5(Button, { type: "submit", isLoading: loading, children: tokenState.mode === "create" ? texts.createSubmitLabel : texts.resetSubmitLabel }) ] }) }) ] }); } export { AuthGuard, + InviteAcceptForm, LoginForm, PasswordResetConfirmForm, PasswordResetRequestForm, diff --git a/dist/react/index.js.map b/dist/react/index.js.map index 1be915a..1372664 100644 --- a/dist/react/index.js.map +++ b/dist/react/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../react/AuthGuard.tsx","../../react/chakra-compat.tsx","../../react/client.ts","../../react/LoginForm.tsx","../../react/PasswordResetForms.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Center, Spinner } from \"./chakra-compat\";\nimport { Navigate } from \"react-router\";\n\ntype AuthGuardProps = {\n children: React.ReactNode;\n fetchCurrentUser: () => Promise;\n redirectTo?: string;\n loadingFallback?: React.ReactNode;\n authenticatedWrapper?: (children: React.ReactNode) => React.ReactNode;\n};\n\nexport function AuthGuard({\n children,\n fetchCurrentUser,\n redirectTo = \"/login\",\n loadingFallback,\n authenticatedWrapper\n}: AuthGuardProps) {\n const [state, setState] = useState<{ loading: boolean; authenticated: boolean }>({\n loading: true,\n authenticated: false\n });\n\n useEffect(() => {\n let cancelled = false;\n\n fetchCurrentUser()\n .then(() => {\n if (!cancelled) {\n setState({ loading: false, authenticated: true });\n }\n })\n .catch(() => {\n if (!cancelled) {\n setState({ loading: false, authenticated: false });\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [fetchCurrentUser]);\n\n if (state.loading) {\n return (\n <>\n {loadingFallback ?? (\n
\n \n
\n )}\n \n );\n }\n\n if (!state.authenticated) {\n return ;\n }\n\n return <>{authenticatedWrapper ? authenticatedWrapper(children) : children};\n}\n","import {\n Alert as ChakraAlert,\n Box as ChakraBox,\n Button as ChakraButton,\n Center as ChakraCenter,\n HStack as ChakraHStack,\n Icon as ChakraIcon,\n Input as ChakraInput,\n Spinner as ChakraSpinner,\n Stack as ChakraStack,\n Text as ChakraText\n} from \"@chakra-ui/react\";\nimport type { ReactNode } from \"react\";\n\ntype AnyProps = Record;\n\nfunction normalizeSpacingProps(props: AnyProps) {\n const next = { ...props };\n if (next.spacing !== undefined && next.gap === undefined) {\n next.gap = next.spacing;\n }\n delete next.spacing;\n return next;\n}\n\nfunction normalizeInteractiveProps(props: AnyProps) {\n const next = normalizeSpacingProps(props);\n if (next.isDisabled !== undefined && next.disabled === undefined) {\n next.disabled = next.isDisabled;\n }\n if (next.isLoading !== undefined && next.loading === undefined) {\n next.loading = next.isLoading;\n }\n delete next.isDisabled;\n delete next.isLoading;\n return next;\n}\n\nexport const Center = ChakraCenter as any;\nexport const Icon = ChakraIcon as any;\nexport const Input = ChakraInput as any;\nexport const Spinner = ChakraSpinner as any;\n\nexport function Stack(props: AnyProps) {\n return ;\n}\n\nexport function HStack(props: AnyProps) {\n return ;\n}\n\nexport function Text(props: AnyProps) {\n const next = { ...props };\n if (next.noOfLines !== undefined && next.lineClamp === undefined) {\n next.lineClamp = next.noOfLines;\n }\n delete next.noOfLines;\n return ;\n}\n\nexport function Button(props: AnyProps) {\n const { leftIcon, rightIcon, children, ...rest } = normalizeInteractiveProps(props);\n return (\n \n \n {leftIcon ?? null}\n {children}\n {rightIcon ?? null}\n \n \n );\n}\n\nexport function Alert({ children, status = \"info\", ...props }: AnyProps) {\n return (\n \n {children}\n \n );\n}\n\nexport function AlertIcon() {\n return ;\n}\n\nexport function AlertDescription({ children, ...props }: { children: ReactNode } & AnyProps) {\n return {children};\n}\n\nexport function FormControl({ children, ...props }: AnyProps) {\n return {children};\n}\n\nexport function FormLabel(props: AnyProps) {\n return ;\n}\n","import type { AuthProviderAvailability, PasswordResetMode } from \"./types\";\n\ntype CreateAuthClientOptions = {\n apiUrl: (path: string) => string;\n authUrl?: (path: string) => string;\n fetchImpl?: typeof fetch;\n credentials?: RequestCredentials;\n defaultOAuthCallbackUrl?: string | (() => string);\n};\n\ntype LoginInput = {\n email: string;\n password: string;\n};\n\ntype RegisterInput = LoginInput & {\n name: string;\n};\n\ntype PasswordResetValidationPayload = {\n email?: string;\n mode?: PasswordResetMode;\n error?: string;\n};\n\ntype JsonErrorPayload = {\n error?: string;\n};\n\nasync function readJsonError(response: Response, fallback: string): Promise {\n const payload = (await response.json().catch(() => null)) as JsonErrorPayload | null;\n return new Error(payload?.error ?? fallback);\n}\n\nexport function createAuthClient(options: CreateAuthClientOptions) {\n const fetchImpl = options.fetchImpl ?? fetch;\n const authUrl = options.authUrl ?? options.apiUrl;\n const credentials = options.credentials ?? \"include\";\n\n function resolveDefaultOAuthCallbackUrl(): string {\n const configured = options.defaultOAuthCallbackUrl;\n if (typeof configured === \"function\") {\n return configured();\n }\n if (typeof configured === \"string\" && configured.trim().length > 0) {\n return configured;\n }\n return `${window.location.origin}/chat`;\n }\n\n async function request(path: string, init?: RequestInit): Promise {\n return fetchImpl(options.apiUrl(path), {\n ...init,\n credentials,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(init?.headers ?? {})\n }\n });\n }\n\n return {\n async getProviders(): Promise {\n const response = await request(\"/api/auth/providers\");\n if (!response.ok) {\n throw await readJsonError(response, \"providers_unavailable\");\n }\n return (await response.json()) as AuthProviderAvailability;\n },\n\n async getCurrentUser(): Promise {\n const response = await request(\"/api/me\");\n if (!response.ok) {\n throw await readJsonError(response, \"Unauthorized\");\n }\n const payload = (await response.json()) as { user: TUser };\n return payload.user;\n },\n\n async register(input: RegisterInput): Promise {\n const response = await request(\"/api/auth/register\", {\n method: \"POST\",\n body: JSON.stringify(input)\n });\n if (!response.ok) {\n throw await readJsonError(response, \"Registration failed\");\n }\n },\n\n async login(input: LoginInput): Promise {\n const response = await request(\"/api/auth/login\", {\n method: \"POST\",\n body: JSON.stringify(input)\n });\n if (!response.ok) {\n throw await readJsonError(response, \"Sign in failed\");\n }\n },\n\n async requestPasswordReset(email: string): Promise {\n const response = await request(\"/api/auth/password-reset/request\", {\n method: \"POST\",\n body: JSON.stringify({ email })\n });\n if (!response.ok) {\n throw await readJsonError(response, \"Password reset request failed\");\n }\n },\n\n async validatePasswordResetToken(token: string): Promise<{ email: string; mode: PasswordResetMode }> {\n const response = await request(`/api/auth/password-reset/validate?token=${encodeURIComponent(token)}`, {\n headers: {}\n });\n const payload = (await response.json().catch(() => null)) as PasswordResetValidationPayload | null;\n if (!response.ok || !payload?.email || (payload.mode !== \"reset\" && payload.mode !== \"create\")) {\n throw new Error(payload?.error ?? \"Invalid reset link\");\n }\n return {\n email: payload.email,\n mode: payload.mode\n };\n },\n\n async confirmPasswordReset(input: { token: string; password: string }): Promise {\n const response = await request(\"/api/auth/password-reset/confirm\", {\n method: \"POST\",\n body: JSON.stringify(input)\n });\n if (!response.ok) {\n throw await readJsonError(response, \"Invalid reset link\");\n }\n },\n\n async logout(): Promise {\n const response = await request(\"/api/auth/logout\", {\n method: \"POST\"\n });\n if (!response.ok) {\n throw await readJsonError(response, \"Logout failed\");\n }\n },\n\n async startOAuthSignIn(provider: string, callbackUrl = resolveDefaultOAuthCallbackUrl()): Promise {\n const response = await fetchImpl(authUrl(\"/auth/csrf\"), {\n credentials\n });\n if (!response.ok) {\n throw await readJsonError(response, \"Sign in failed\");\n }\n\n const payload = (await response.json()) as { csrfToken?: string };\n if (!payload.csrfToken) {\n throw new Error(\"Sign in failed\");\n }\n\n const form = document.createElement(\"form\");\n form.method = \"POST\";\n form.action = authUrl(`/auth/signin/${provider}`);\n form.style.display = \"none\";\n\n const csrfInput = document.createElement(\"input\");\n csrfInput.type = \"hidden\";\n csrfInput.name = \"csrfToken\";\n csrfInput.value = payload.csrfToken;\n form.appendChild(csrfInput);\n\n const callbackInput = document.createElement(\"input\");\n callbackInput.type = \"hidden\";\n callbackInput.name = \"callbackUrl\";\n callbackInput.value = callbackUrl;\n form.appendChild(callbackInput);\n\n document.body.appendChild(form);\n form.submit();\n }\n };\n}\n","import type { FormEvent, ReactNode } from \"react\";\nimport { Alert, AlertDescription, AlertIcon, Button, Center, FormControl, FormLabel, HStack, Icon, Input, Stack } from \"./chakra-compat\";\nimport { FcGoogle } from \"react-icons/fc\";\nimport type { AuthProviderAvailability, AuthProviderKey, AuthSubmitValues, LoginMode } from \"./types\";\n\ntype LoginFormTexts = {\n nameLabel: string;\n emailLabel: string;\n passwordLabel: string;\n passwordConfirmLabel: string;\n submitRegisterLabel: string;\n submitSignInLabel: string;\n toggleToRegisterLabel: string;\n toggleToSignInLabel: string;\n forgotPasswordLabel: string;\n googleLabel: string;\n slackLabel: string;\n};\n\ntype LoginFormProps = {\n mode: LoginMode;\n texts: LoginFormTexts;\n onSubmit: (values: AuthSubmitValues) => void | Promise;\n onModeToggle: () => void;\n loading?: boolean;\n oauthLoadingProvider?: AuthProviderKey | null;\n providers?: AuthProviderAvailability;\n onOAuthSignIn?: (provider: AuthProviderKey) => void | Promise;\n errorMessage?: string | null;\n successMessage?: string | null;\n footer?: ReactNode;\n forgotPasswordLink?: ReactNode;\n emailPlaceholder?: string;\n namePlaceholder?: string;\n};\n\nexport function LoginForm({\n mode,\n texts,\n onSubmit,\n onModeToggle,\n loading = false,\n oauthLoadingProvider = null,\n providers,\n onOAuthSignIn,\n errorMessage,\n successMessage,\n footer,\n forgotPasswordLink,\n emailPlaceholder = \"you@example.com\",\n namePlaceholder = \"Jane Doe\"\n}: LoginFormProps) {\n const registerMode = mode === \"register\";\n\n function handleSubmit(event: FormEvent) {\n event.preventDefault();\n const form = new FormData(event.currentTarget);\n void onSubmit({\n name: String(form.get(\"name\") ?? \"\"),\n email: String(form.get(\"email\") ?? \"\"),\n password: String(form.get(\"password\") ?? \"\"),\n passwordConfirm: String(form.get(\"passwordConfirm\") ?? \"\")\n });\n }\n\n return (\n \n {errorMessage ? (\n \n \n {errorMessage}\n \n ) : null}\n\n {successMessage ? (\n \n \n {successMessage}\n \n ) : null}\n\n
\n \n {registerMode ? (\n \n {texts.nameLabel}\n \n \n ) : null}\n\n \n {texts.emailLabel}\n \n \n\n \n {texts.passwordLabel}\n \n \n\n {!registerMode && forgotPasswordLink ? {forgotPasswordLink} : null}\n\n {registerMode ? (\n \n {texts.passwordConfirmLabel}\n \n \n ) : null}\n\n \n \n
\n\n {registerMode || !onOAuthSignIn || (!providers?.google && !providers?.slack) ? null : (\n \n {providers.google ? (\n \n \n \n }\n _hover={{ bg: \"gray.300\" }}\n _active={{ bg: \"gray.300\" }}\n isLoading={oauthLoadingProvider === \"google\"}\n onClick={() => void onOAuthSignIn(\"google\")}\n >\n {texts.googleLabel}\n \n ) : null}\n {providers.slack ? (\n void onOAuthSignIn(\"slack\")}\n >\n {texts.slackLabel}\n \n ) : null}\n \n )}\n\n \n\n {footer ?? null}\n
\n );\n}\n","import type { FormEvent, ReactNode } from \"react\";\nimport {\n Alert,\n AlertDescription,\n AlertIcon,\n Button,\n FormControl,\n FormLabel,\n Input,\n Spinner,\n Stack,\n Text\n} from \"./chakra-compat\";\nimport type { PasswordResetMode, PasswordResetTokenState } from \"./types\";\n\ntype PasswordResetRequestTexts = {\n emailLabel: string;\n submitLabel: string;\n requestSentMessage: string;\n};\n\ntype PasswordResetRequestFormProps = {\n texts: PasswordResetRequestTexts;\n helperText: ReactNode;\n loading?: boolean;\n requestSent?: boolean;\n onSubmit: (values: { email: string }) => void | Promise;\n emailPlaceholder?: string;\n};\n\ntype PasswordResetConfirmTexts = {\n loadingLabel: string;\n passwordLabel: string;\n passwordConfirmLabel: string;\n invalidLinkLabel: string;\n resetSubmitLabel: string;\n createSubmitLabel: string;\n resetSuccessLabel: string;\n createSuccessLabel: string;\n};\n\ntype PasswordResetConfirmFormProps = {\n texts: PasswordResetConfirmTexts;\n tokenState: PasswordResetTokenState;\n loading?: boolean;\n completedMode?: PasswordResetMode | null;\n onSubmit: (values: { password: string; passwordConfirm: string }) => void | Promise;\n};\n\nexport function PasswordResetRequestForm({\n texts,\n helperText,\n loading = false,\n requestSent = false,\n onSubmit,\n emailPlaceholder = \"you@example.com\"\n}: PasswordResetRequestFormProps) {\n function handleSubmit(event: FormEvent) {\n event.preventDefault();\n const form = new FormData(event.currentTarget);\n void onSubmit({\n email: String(form.get(\"email\") ?? \"\")\n });\n }\n\n return (\n \n {requestSent ? (\n \n \n {texts.requestSentMessage}\n \n ) : null}\n\n
\n \n \n {texts.emailLabel}\n \n \n\n \n {helperText}\n \n\n \n \n
\n
\n );\n}\n\nexport function PasswordResetConfirmForm({\n texts,\n tokenState,\n loading = false,\n completedMode = null,\n onSubmit\n}: PasswordResetConfirmFormProps) {\n function handleSubmit(event: FormEvent) {\n event.preventDefault();\n const form = new FormData(event.currentTarget);\n void onSubmit({\n password: String(form.get(\"password\") ?? \"\"),\n passwordConfirm: String(form.get(\"passwordConfirm\") ?? \"\")\n });\n }\n\n if (tokenState.status === \"loading\") {\n return (\n \n \n {texts.loadingLabel}\n \n );\n }\n\n if (tokenState.status === \"invalid\") {\n return (\n \n \n {tokenState.error || texts.invalidLinkLabel}\n \n );\n }\n\n if (completedMode !== null) {\n return (\n \n \n {completedMode === \"create\" ? texts.createSuccessLabel : texts.resetSuccessLabel}\n \n );\n }\n\n return (\n \n \n {tokenState.email}\n \n\n
\n \n \n {texts.passwordLabel}\n \n \n\n \n {texts.passwordConfirmLabel}\n \n \n\n \n \n
\n
\n );\n}\n"],"mappings":"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","names":["jsx","jsx","jsxs","jsx","jsxs"]} \ No newline at end of file +{"version":3,"sources":["../../react/AuthGuard.tsx","../../react/chakra-compat.tsx","../../react/client.ts","../../react/InviteAcceptForm.tsx","../../react/LoginForm.tsx","../../react/PasswordResetForms.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Center, Spinner } from \"./chakra-compat\";\nimport { Navigate } from \"react-router\";\n\ntype AuthGuardProps = {\n children: React.ReactNode;\n fetchCurrentUser: () => Promise;\n redirectTo?: string;\n loadingFallback?: React.ReactNode;\n authenticatedWrapper?: (children: React.ReactNode) => React.ReactNode;\n};\n\nexport function AuthGuard({\n children,\n fetchCurrentUser,\n redirectTo = \"/login\",\n loadingFallback,\n authenticatedWrapper\n}: AuthGuardProps) {\n const [state, setState] = useState<{ loading: boolean; authenticated: boolean }>({\n loading: true,\n authenticated: false\n });\n\n useEffect(() => {\n let cancelled = false;\n\n fetchCurrentUser()\n .then(() => {\n if (!cancelled) {\n setState({ loading: false, authenticated: true });\n }\n })\n .catch(() => {\n if (!cancelled) {\n setState({ loading: false, authenticated: false });\n }\n });\n\n return () => {\n cancelled = true;\n };\n }, [fetchCurrentUser]);\n\n if (state.loading) {\n return (\n <>\n {loadingFallback ?? (\n
\n \n
\n )}\n \n );\n }\n\n if (!state.authenticated) {\n return ;\n }\n\n return <>{authenticatedWrapper ? authenticatedWrapper(children) : children};\n}\n","import {\n Alert as ChakraAlert,\n Box as ChakraBox,\n Button as ChakraButton,\n Center as ChakraCenter,\n HStack as ChakraHStack,\n Icon as ChakraIcon,\n Input as ChakraInput,\n Spinner as ChakraSpinner,\n Stack as ChakraStack,\n Text as ChakraText\n} from \"@chakra-ui/react\";\nimport type { ReactNode } from \"react\";\n\ntype AnyProps = Record;\n\nfunction normalizeSpacingProps(props: AnyProps) {\n const next = { ...props };\n if (next.spacing !== undefined && next.gap === undefined) {\n next.gap = next.spacing;\n }\n delete next.spacing;\n return next;\n}\n\nfunction normalizeInteractiveProps(props: AnyProps) {\n const next = normalizeSpacingProps(props);\n if (next.isDisabled !== undefined && next.disabled === undefined) {\n next.disabled = next.isDisabled;\n }\n if (next.isLoading !== undefined && next.loading === undefined) {\n next.loading = next.isLoading;\n }\n delete next.isDisabled;\n delete next.isLoading;\n return next;\n}\n\nexport const Center = ChakraCenter as any;\nexport const Icon = ChakraIcon as any;\nexport const Input = ChakraInput as any;\nexport const Spinner = ChakraSpinner as any;\n\nexport function Stack(props: AnyProps) {\n return ;\n}\n\nexport function HStack(props: AnyProps) {\n return ;\n}\n\nexport function Text(props: AnyProps) {\n const next = { ...props };\n if (next.noOfLines !== undefined && next.lineClamp === undefined) {\n next.lineClamp = next.noOfLines;\n }\n delete next.noOfLines;\n return ;\n}\n\nexport function Button(props: AnyProps) {\n const { leftIcon, rightIcon, children, ...rest } = normalizeInteractiveProps(props);\n return (\n \n \n {leftIcon ?? null}\n {children}\n {rightIcon ?? null}\n \n \n );\n}\n\nexport function Alert({ children, status = \"info\", ...props }: AnyProps) {\n return (\n \n {children}\n \n );\n}\n\nexport function AlertIcon() {\n return ;\n}\n\nexport function AlertDescription({ children, ...props }: { children: ReactNode } & AnyProps) {\n return {children};\n}\n\nexport function FormControl({ children, ...props }: AnyProps) {\n return {children};\n}\n\nexport function FormLabel(props: AnyProps) {\n return ;\n}\n","import type { AuthProviderAvailability, PasswordResetMode } from \"./types\";\n\ntype CreateAuthClientOptions = {\n apiUrl: (path: string) => string;\n authUrl?: (path: string) => string;\n fetchImpl?: typeof fetch;\n credentials?: RequestCredentials;\n defaultOAuthCallbackUrl?: string | (() => string);\n};\n\ntype LoginInput = {\n email: string;\n password: string;\n};\n\ntype RegisterInput = LoginInput & {\n name: string;\n};\n\ntype PasswordResetValidationPayload = {\n email?: string;\n mode?: PasswordResetMode;\n error?: string;\n};\n\ntype AccountInviteValidationPayload = {\n email?: string;\n name?: string | null;\n error?: string;\n};\n\ntype JsonErrorPayload = {\n error?: string;\n};\n\nasync function readJsonError(response: Response, fallback: string): Promise {\n const payload = (await response.json().catch(() => null)) as JsonErrorPayload | null;\n return new Error(payload?.error ?? fallback);\n}\n\nexport function createAuthClient(options: CreateAuthClientOptions) {\n const fetchImpl = options.fetchImpl ?? fetch;\n const authUrl = options.authUrl ?? options.apiUrl;\n const credentials = options.credentials ?? \"include\";\n\n function resolveDefaultOAuthCallbackUrl(): string {\n const configured = options.defaultOAuthCallbackUrl;\n if (typeof configured === \"function\") {\n return configured();\n }\n if (typeof configured === \"string\" && configured.trim().length > 0) {\n return configured;\n }\n return `${window.location.origin}/chat`;\n }\n\n async function request(path: string, init?: RequestInit): Promise {\n return fetchImpl(options.apiUrl(path), {\n ...init,\n credentials,\n headers: {\n \"Content-Type\": \"application/json\",\n ...(init?.headers ?? {})\n }\n });\n }\n\n return {\n async getProviders(): Promise {\n const response = await request(\"/api/auth/providers\");\n if (!response.ok) {\n throw await readJsonError(response, \"providers_unavailable\");\n }\n return (await response.json()) as AuthProviderAvailability;\n },\n\n async getCurrentUser(): Promise {\n const response = await request(\"/api/me\");\n if (!response.ok) {\n throw await readJsonError(response, \"Unauthorized\");\n }\n const payload = (await response.json()) as { user: TUser };\n return payload.user;\n },\n\n async register(input: RegisterInput): Promise {\n const response = await request(\"/api/auth/register\", {\n method: \"POST\",\n body: JSON.stringify(input)\n });\n if (!response.ok) {\n throw await readJsonError(response, \"Registration failed\");\n }\n },\n\n async login(input: LoginInput): Promise {\n const response = await request(\"/api/auth/login\", {\n method: \"POST\",\n body: JSON.stringify(input)\n });\n if (!response.ok) {\n throw await readJsonError(response, \"Sign in failed\");\n }\n },\n\n async requestPasswordReset(email: string): Promise {\n const response = await request(\"/api/auth/password-reset/request\", {\n method: \"POST\",\n body: JSON.stringify({ email })\n });\n if (!response.ok) {\n throw await readJsonError(response, \"Password reset request failed\");\n }\n },\n\n async validatePasswordResetToken(token: string): Promise<{ email: string; mode: PasswordResetMode }> {\n const response = await request(`/api/auth/password-reset/validate?token=${encodeURIComponent(token)}`, {\n headers: {}\n });\n const payload = (await response.json().catch(() => null)) as PasswordResetValidationPayload | null;\n if (!response.ok || !payload?.email || (payload.mode !== \"reset\" && payload.mode !== \"create\")) {\n throw new Error(payload?.error ?? \"Invalid reset link\");\n }\n return {\n email: payload.email,\n mode: payload.mode\n };\n },\n\n async confirmPasswordReset(input: { token: string; password: string }): Promise {\n const response = await request(\"/api/auth/password-reset/confirm\", {\n method: \"POST\",\n body: JSON.stringify(input)\n });\n if (!response.ok) {\n throw await readJsonError(response, \"Invalid reset link\");\n }\n },\n\n async validateAccountInviteToken(token: string): Promise<{ email: string; name: string | null }> {\n const response = await request(`/api/auth/invite/validate?token=${encodeURIComponent(token)}`, {\n headers: {}\n });\n const payload = (await response.json().catch(() => null)) as AccountInviteValidationPayload | null;\n if (!response.ok || !payload?.email) {\n throw new Error(payload?.error ?? \"Invalid invite link\");\n }\n return {\n email: payload.email,\n name: payload.name ?? null\n };\n },\n\n async acceptAccountInvite(input: { token: string; name: string; password: string }): Promise {\n const response = await request(\"/api/auth/invite/accept\", {\n method: \"POST\",\n body: JSON.stringify(input)\n });\n if (!response.ok) {\n throw await readJsonError(response, \"Invalid invite link\");\n }\n },\n\n async logout(): Promise {\n const response = await request(\"/api/auth/logout\", {\n method: \"POST\"\n });\n if (!response.ok) {\n throw await readJsonError(response, \"Logout failed\");\n }\n },\n\n async startOAuthSignIn(provider: string, callbackUrl = resolveDefaultOAuthCallbackUrl()): Promise {\n const response = await fetchImpl(authUrl(\"/auth/csrf\"), {\n credentials\n });\n if (!response.ok) {\n throw await readJsonError(response, \"Sign in failed\");\n }\n\n const payload = (await response.json()) as { csrfToken?: string };\n if (!payload.csrfToken) {\n throw new Error(\"Sign in failed\");\n }\n\n const form = document.createElement(\"form\");\n form.method = \"POST\";\n form.action = authUrl(`/auth/signin/${provider}`);\n form.style.display = \"none\";\n\n const csrfInput = document.createElement(\"input\");\n csrfInput.type = \"hidden\";\n csrfInput.name = \"csrfToken\";\n csrfInput.value = payload.csrfToken;\n form.appendChild(csrfInput);\n\n const callbackInput = document.createElement(\"input\");\n callbackInput.type = \"hidden\";\n callbackInput.name = \"callbackUrl\";\n callbackInput.value = callbackUrl;\n form.appendChild(callbackInput);\n\n document.body.appendChild(form);\n form.submit();\n }\n };\n}\n","import { useEffect, useState, type ChangeEvent, type FormEvent } from \"react\";\nimport { Alert, AlertDescription, AlertIcon, Button, FormControl, FormLabel, Input, Spinner, Stack, Text } from \"./chakra-compat\";\nimport { FcGoogle } from \"react-icons/fc\";\nimport type { AccountInviteTokenState } from \"./types\";\n\ntype InviteAcceptFormTexts = {\n loadingLabel: string;\n invalidLinkLabel: string;\n emailLabel: string;\n nameLabel: string;\n passwordLabel: string;\n passwordConfirmLabel: string;\n submitLabel: string;\n googleLabel: string;\n};\n\ntype InviteAcceptFormProps = {\n texts: InviteAcceptFormTexts;\n tokenState: AccountInviteTokenState;\n loading?: boolean;\n initialName?: string | null;\n showGoogleSignIn?: boolean;\n googleLoading?: boolean;\n onSubmit: (values: { name: string; password: string; passwordConfirm: string }) => void | Promise;\n onGoogleSignIn?: () => void | Promise;\n};\n\nexport function InviteAcceptForm({\n texts,\n tokenState,\n loading = false,\n initialName = null,\n showGoogleSignIn = false,\n googleLoading = false,\n onSubmit,\n onGoogleSignIn\n}: InviteAcceptFormProps) {\n const [name, setName] = useState(initialName ?? \"\");\n\n useEffect(() => {\n setName(initialName ?? \"\");\n }, [initialName, tokenState.status]);\n\n function handleSubmit(event: FormEvent) {\n event.preventDefault();\n const form = new FormData(event.currentTarget);\n void onSubmit({\n name: String(form.get(\"name\") ?? \"\"),\n password: String(form.get(\"password\") ?? \"\"),\n passwordConfirm: String(form.get(\"passwordConfirm\") ?? \"\")\n });\n }\n\n if (tokenState.status === \"loading\") {\n return (\n \n \n {texts.loadingLabel}\n \n );\n }\n\n if (tokenState.status === \"invalid\") {\n return (\n \n \n {tokenState.error || texts.invalidLinkLabel}\n \n );\n }\n\n return (\n \n \n {texts.emailLabel}\n \n \n\n
\n \n \n {texts.nameLabel}\n ) => setName(event.target.value)} />\n \n\n \n {texts.passwordLabel}\n \n \n\n \n {texts.passwordConfirmLabel}\n \n \n\n \n \n
\n\n {showGoogleSignIn && onGoogleSignIn ? (\n \n ) : null}\n
\n );\n}\n","import type { FormEvent, ReactNode } from \"react\";\nimport { Alert, AlertDescription, AlertIcon, Button, Center, FormControl, FormLabel, HStack, Icon, Input, Stack } from \"./chakra-compat\";\nimport { FcGoogle } from \"react-icons/fc\";\nimport type { AuthProviderAvailability, AuthProviderKey, AuthSubmitValues, LoginMode } from \"./types\";\n\ntype LoginFormTexts = {\n nameLabel: string;\n emailLabel: string;\n passwordLabel: string;\n passwordConfirmLabel: string;\n submitRegisterLabel: string;\n submitSignInLabel: string;\n toggleToRegisterLabel: string;\n toggleToSignInLabel: string;\n forgotPasswordLabel: string;\n googleLabel: string;\n slackLabel: string;\n};\n\ntype LoginFormProps = {\n mode: LoginMode;\n texts: LoginFormTexts;\n onSubmit: (values: AuthSubmitValues) => void | Promise;\n onModeToggle: () => void;\n loading?: boolean;\n oauthLoadingProvider?: AuthProviderKey | null;\n providers?: AuthProviderAvailability;\n onOAuthSignIn?: (provider: AuthProviderKey) => void | Promise;\n errorMessage?: string | null;\n successMessage?: string | null;\n footer?: ReactNode;\n forgotPasswordLink?: ReactNode;\n emailPlaceholder?: string;\n namePlaceholder?: string;\n};\n\nexport function LoginForm({\n mode,\n texts,\n onSubmit,\n onModeToggle,\n loading = false,\n oauthLoadingProvider = null,\n providers,\n onOAuthSignIn,\n errorMessage,\n successMessage,\n footer,\n forgotPasswordLink,\n emailPlaceholder = \"you@example.com\",\n namePlaceholder = \"Jane Doe\"\n}: LoginFormProps) {\n const registerMode = mode === \"register\";\n\n function handleSubmit(event: FormEvent) {\n event.preventDefault();\n const form = new FormData(event.currentTarget);\n void onSubmit({\n name: String(form.get(\"name\") ?? \"\"),\n email: String(form.get(\"email\") ?? \"\"),\n password: String(form.get(\"password\") ?? \"\"),\n passwordConfirm: String(form.get(\"passwordConfirm\") ?? \"\")\n });\n }\n\n return (\n \n {errorMessage ? (\n \n \n {errorMessage}\n \n ) : null}\n\n {successMessage ? (\n \n \n {successMessage}\n \n ) : null}\n\n
\n \n {registerMode ? (\n \n {texts.nameLabel}\n \n \n ) : null}\n\n \n {texts.emailLabel}\n \n \n\n \n {texts.passwordLabel}\n \n \n\n {!registerMode && forgotPasswordLink ? {forgotPasswordLink} : null}\n\n {registerMode ? (\n \n {texts.passwordConfirmLabel}\n \n \n ) : null}\n\n \n \n
\n\n {registerMode || !onOAuthSignIn || (!providers?.google && !providers?.slack) ? null : (\n \n {providers.google ? (\n \n \n \n }\n _hover={{ bg: \"gray.300\" }}\n _active={{ bg: \"gray.300\" }}\n isLoading={oauthLoadingProvider === \"google\"}\n onClick={() => void onOAuthSignIn(\"google\")}\n >\n {texts.googleLabel}\n \n ) : null}\n {providers.slack ? (\n void onOAuthSignIn(\"slack\")}\n >\n {texts.slackLabel}\n \n ) : null}\n \n )}\n\n \n\n {footer ?? null}\n
\n );\n}\n","import type { FormEvent, ReactNode } from \"react\";\nimport {\n Alert,\n AlertDescription,\n AlertIcon,\n Button,\n FormControl,\n FormLabel,\n Input,\n Spinner,\n Stack,\n Text\n} from \"./chakra-compat\";\nimport type { PasswordResetMode, PasswordResetTokenState } from \"./types\";\n\ntype PasswordResetRequestTexts = {\n emailLabel: string;\n submitLabel: string;\n requestSentMessage: string;\n};\n\ntype PasswordResetRequestFormProps = {\n texts: PasswordResetRequestTexts;\n helperText: ReactNode;\n loading?: boolean;\n requestSent?: boolean;\n onSubmit: (values: { email: string }) => void | Promise;\n emailPlaceholder?: string;\n};\n\ntype PasswordResetConfirmTexts = {\n loadingLabel: string;\n passwordLabel: string;\n passwordConfirmLabel: string;\n invalidLinkLabel: string;\n resetSubmitLabel: string;\n createSubmitLabel: string;\n resetSuccessLabel: string;\n createSuccessLabel: string;\n};\n\ntype PasswordResetConfirmFormProps = {\n texts: PasswordResetConfirmTexts;\n tokenState: PasswordResetTokenState;\n loading?: boolean;\n completedMode?: PasswordResetMode | null;\n onSubmit: (values: { password: string; passwordConfirm: string }) => void | Promise;\n};\n\nexport function PasswordResetRequestForm({\n texts,\n helperText,\n loading = false,\n requestSent = false,\n onSubmit,\n emailPlaceholder = \"you@example.com\"\n}: PasswordResetRequestFormProps) {\n function handleSubmit(event: FormEvent) {\n event.preventDefault();\n const form = new FormData(event.currentTarget);\n void onSubmit({\n email: String(form.get(\"email\") ?? \"\")\n });\n }\n\n return (\n \n {requestSent ? (\n \n \n {texts.requestSentMessage}\n \n ) : null}\n\n
\n \n \n {texts.emailLabel}\n \n \n\n \n {helperText}\n \n\n \n \n
\n
\n );\n}\n\nexport function PasswordResetConfirmForm({\n texts,\n tokenState,\n loading = false,\n completedMode = null,\n onSubmit\n}: PasswordResetConfirmFormProps) {\n function handleSubmit(event: FormEvent) {\n event.preventDefault();\n const form = new FormData(event.currentTarget);\n void onSubmit({\n password: String(form.get(\"password\") ?? \"\"),\n passwordConfirm: String(form.get(\"passwordConfirm\") ?? \"\")\n });\n }\n\n if (tokenState.status === \"loading\") {\n return (\n \n \n {texts.loadingLabel}\n \n );\n }\n\n if (tokenState.status === \"invalid\") {\n return (\n \n \n {tokenState.error || texts.invalidLinkLabel}\n \n );\n }\n\n if (completedMode !== null) {\n return (\n \n \n {completedMode === \"create\" ? texts.createSuccessLabel : texts.resetSuccessLabel}\n \n );\n }\n\n return (\n \n \n {tokenState.email}\n \n\n
\n \n \n {texts.passwordLabel}\n \n \n\n \n {texts.passwordConfirmLabel}\n \n \n\n \n \n
\n
\n );\n}\n"],"mappings":"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","names":["jsx","useEffect","useState","jsx","jsxs","useState","useEffect","FcGoogle","jsx","jsxs","jsx","jsxs"]} \ No newline at end of file diff --git a/dist/server/index.d.ts b/dist/server/index.d.ts index a3fbd3f..d6c0a07 100644 --- a/dist/server/index.d.ts +++ b/dist/server/index.d.ts @@ -76,6 +76,18 @@ declare function createAuthModule(options: CreateAuthModuleOptions; + type RegisterAuthApiRoutesOptions = { app: Express; prisma: any; @@ -110,6 +122,10 @@ type RegisterAuthApiRoutesOptions = { expiresAt: Date; }) => Promise; }; + accountInvite?: { + enabled: boolean; + identifierPrefix?: string; + }; onUserRegistered?: (user: { id: string; email: string | null; @@ -130,7 +146,9 @@ type AuthRouteMessages = { passwordResetUnavailable: string; invalidResetLink: string; expiredResetLink: string; + invalidInviteLink: string; + inviteAlreadyAccepted: string; }; declare function registerAuthApiRoutes(options: RegisterAuthApiRoutesOptions): void; -export { createAuthModule, registerAuthApiRoutes }; +export { createAccountInviteToken, createAuthModule, registerAuthApiRoutes }; diff --git a/dist/server/index.js b/dist/server/index.js index 7a192c7..e4cdf99 100644 --- a/dist/server/index.js +++ b/dist/server/index.js @@ -224,8 +224,73 @@ function createAuthModule(options) { }; } -// server/routes.ts +// server/invites.ts import { createHash, randomBytes } from "crypto"; +var DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX = "account-invite:"; +var DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1e3; +function hashAccountInviteToken(token) { + return createHash("sha256").update(token).digest("hex"); +} +function buildAccountInviteIdentifier(prefix, userId) { + return `${prefix}${userId}`; +} +async function createAccountInviteToken(options) { + const identifierPrefix = options.identifierPrefix ?? DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX; + const identifier = buildAccountInviteIdentifier(identifierPrefix, options.userId); + const token = randomBytes(32).toString("hex"); + const expiresAt = new Date(Date.now() + (options.tokenTtlMs ?? DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS)); + await options.prisma.verificationToken.deleteMany({ + where: { + OR: [{ identifier }, { expires: { lt: /* @__PURE__ */ new Date() } }] + } + }); + await options.prisma.verificationToken.create({ + data: { + identifier, + token: hashAccountInviteToken(token), + expires: expiresAt + } + }); + return { token, expiresAt }; +} +async function getAccountInviteContext(options) { + const identifierPrefix = options.identifierPrefix ?? DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX; + const verificationToken = await options.prisma.verificationToken.findUnique({ + where: { token: hashAccountInviteToken(options.token) }, + select: { identifier: true, expires: true } + }); + if (!verificationToken || verificationToken.expires <= /* @__PURE__ */ new Date() || !verificationToken.identifier.startsWith(identifierPrefix)) { + return null; + } + const userId = verificationToken.identifier.slice(identifierPrefix.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, + accounts: { + select: { id: true }, + take: 1 + } + } + }); + if (!user?.email) { + return null; + } + return { + verificationToken, + user + }; +} + +// server/routes.ts +import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto"; import { z as z2 } from "zod"; var defaultNormalizeEmail = (email) => email.trim(); var defaultPasswordResetIdentifierPrefix = "password-reset:"; @@ -237,10 +302,12 @@ var defaultMessages = { invalidPassword: "Invalid password", passwordResetUnavailable: "Email service is not configured.", invalidResetLink: "Invalid reset link", - expiredResetLink: "Invalid or expired reset link" + expiredResetLink: "Invalid or expired reset link", + invalidInviteLink: "Invalid or expired invite link", + inviteAlreadyAccepted: "This invite has already been accepted" }; function hashPasswordResetToken(token) { - return createHash("sha256").update(token).digest("hex"); + return createHash2("sha256").update(token).digest("hex"); } function buildPasswordResetIdentifier(prefix, userId) { return `${prefix}${userId}`; @@ -253,7 +320,22 @@ function registerAuthApiRoutes(options) { 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 accountInviteIdentifierPrefix = options.accountInvite?.identifierPrefix ?? DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX; const messages = { ...defaultMessages, ...options.messages ?? {} }; + const buildSession = (userId) => ({ + sessionToken: randomBytes2(32).toString("hex"), + userId, + expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1e3) + }); + const applySessionCookie = (res, session) => { + res.cookie(options.sessionCookieName, session.sessionToken, { + httpOnly: true, + sameSite: "lax", + secure: options.sessionCookieSecure, + path: "/", + expires: session.expires + }); + }; const findUserByEmail = async (email) => { const normalized = normalizeEmail(email); const lowered = normalized.toLowerCase(); @@ -348,22 +430,11 @@ function registerAuthApiRoutes(options) { 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); + const session = buildSession(user.id); 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 + data: session }); + applySessionCookie(res, session); return res.status(200).json({ ok: true }); }); options.app.post(`${authApiBasePath}/password-reset/request`, async (req, res) => { @@ -381,7 +452,7 @@ function registerAuthApiRoutes(options) { if (!user?.email) { return res.status(200).json({ ok: true }); } - const rawToken = randomBytes(32).toString("hex"); + const rawToken = randomBytes2(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); @@ -465,6 +536,77 @@ function registerAuthApiRoutes(options) { await options.onPasswordResetConfirmed?.(context.user); return res.status(200).json({ ok: true }); }); + options.app.get(`${authApiBasePath}/invite/validate`, async (req, res) => { + if (!options.accountInvite?.enabled) { + return res.status(404).json({ error: messages.invalidInviteLink }); + } + 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.invalidInviteLink }); + } + const context = await getAccountInviteContext({ + prisma: options.prisma, + token: parsed.data.token, + identifierPrefix: accountInviteIdentifierPrefix + }); + if (!context) { + return res.status(400).json({ error: messages.invalidInviteLink }); + } + if (context.user.passwordHash || context.user.accounts.length > 0) { + return res.status(409).json({ error: messages.inviteAlreadyAccepted }); + } + return res.status(200).json({ + ok: true, + email: context.user.email, + name: context.user.name + }); + }); + options.app.post(`${authApiBasePath}/invite/accept`, async (req, res) => { + if (!options.accountInvite?.enabled) { + return res.status(404).json({ error: messages.invalidInviteLink }); + } + const parsed = z2.object({ + token: z2.string().min(1), + name: z2.string().min(2).max(60), + password: z2.string().min(8) + }).safeParse(req.body); + if (!parsed.success) { + return res.status(400).json({ error: messages.invalidPayload }); + } + const context = await getAccountInviteContext({ + prisma: options.prisma, + token: parsed.data.token, + identifierPrefix: accountInviteIdentifierPrefix + }); + if (!context) { + return res.status(400).json({ error: messages.invalidInviteLink }); + } + if (context.user.passwordHash || context.user.accounts.length > 0) { + return res.status(409).json({ error: messages.inviteAlreadyAccepted }); + } + const passwordHash = await passwordHasher(parsed.data.password); + const session = buildSession(context.user.id); + await options.prisma.$transaction([ + options.prisma.verificationToken.deleteMany({ + where: { identifier: context.verificationToken.identifier } + }), + options.prisma.session.create({ + data: session + }), + options.prisma.user.update({ + where: { id: context.user.id }, + data: { + name: parsed.data.name, + passwordHash, + emailVerified: context.user.emailVerified ?? /* @__PURE__ */ new Date() + } + }) + ]); + applySessionCookie(res, session); + return res.status(200).json({ ok: true }); + }); options.app.post(`${authApiBasePath}/logout`, async (req, res) => { const token = options.extractSessionToken(req.headers.cookie); if (token) { @@ -488,6 +630,7 @@ function registerAuthApiRoutes(options) { }); } export { + createAccountInviteToken, createAuthModule, registerAuthApiRoutes }; diff --git a/dist/server/index.js.map b/dist/server/index.js.map index 16c6b94..347ddf0 100644 --- a/dist/server/index.js.map +++ b/dist/server/index.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../server/module.ts","../../server/routes.ts"],"sourcesContent":["import { ExpressAuth } from \"@auth/express\";\nimport Credentials from \"@auth/express/providers/credentials\";\nimport Google from \"@auth/express/providers/google\";\nimport Slack from \"@auth/express/providers/slack\";\nimport { PrismaAdapter } from \"@auth/prisma-adapter\";\nimport type { Request, RequestHandler } from \"express\";\nimport { z } from \"zod\";\n\ntype CredentialUser = {\n id: string;\n name: string | null;\n email: string | null;\n image?: string | null;\n passwordHash: string | null;\n};\n\ntype CreateAuthModuleOptions = {\n prisma: any;\n clientUrl: string;\n sessionCookieName: string;\n sessionCookieSecure: boolean;\n authUrl?: string;\n authDebug?: boolean;\n authSecret?: string;\n trustHost?: boolean;\n extraSessionCookieNames?: string[];\n signInPath?: string;\n authenticatedRedirectPath?: string;\n googleClientId?: string;\n googleClientSecret?: string;\n slackClientId?: string;\n slackClientSecret?: string;\n findCredentialsUserByEmail: (email: string) => Promise;\n comparePassword: (password: string, passwordHash: string) => Promise;\n sessionUserSelect: Record;\n mapSessionUser: (user: any) => TAuthUser;\n onSessionValidated?: (user: TAuthUser) => Promise | void;\n};\n\nfunction parseBoolean(value: string | undefined, fallback: boolean): boolean {\n if (value === undefined) {\n return fallback;\n }\n const normalized = value.trim().toLowerCase();\n if ([\"1\", \"true\", \"yes\", \"on\"].includes(normalized)) {\n return true;\n }\n if ([\"0\", \"false\", \"no\", \"off\"].includes(normalized)) {\n return false;\n }\n return fallback;\n}\n\nexport function createAuthModule(options: CreateAuthModuleOptions) {\n const signInPath = options.signInPath ?? \"/login\";\n const authenticatedRedirectPath = options.authenticatedRedirectPath ?? \"/chat\";\n const googleAuthEnabled = Boolean(options.googleClientId && options.googleClientSecret);\n const slackAuthEnabled = Boolean(options.slackClientId && options.slackClientSecret);\n\n const providers: any[] = [\n Credentials({\n name: \"Email et mot de passe\",\n credentials: {\n email: { label: \"Email\", type: \"email\" },\n password: { label: \"Password\", type: \"password\" }\n },\n authorize: async (rawCredentials) => {\n const parsed = z\n .object({\n email: z.string().email(),\n password: z.string().min(8)\n })\n .safeParse(rawCredentials);\n\n if (!parsed.success) {\n return null;\n }\n\n const user = await options.findCredentialsUserByEmail(parsed.data.email);\n if (!user?.passwordHash) {\n return null;\n }\n\n const valid = await options.comparePassword(parsed.data.password, user.passwordHash);\n if (!valid) {\n return null;\n }\n\n return {\n id: user.id,\n name: user.name,\n email: user.email,\n image: user.image ?? null\n };\n }\n })\n ];\n\n if (googleAuthEnabled) {\n providers.push(\n Google({\n clientId: options.googleClientId!,\n clientSecret: options.googleClientSecret!,\n authorization: {\n params: {\n prompt: \"select_account\"\n }\n },\n allowDangerousEmailAccountLinking: true\n })\n );\n }\n\n if (slackAuthEnabled) {\n providers.push(\n Slack({\n clientId: options.slackClientId!,\n clientSecret: options.slackClientSecret!,\n allowDangerousEmailAccountLinking: true\n })\n );\n }\n\n const authConfig = {\n adapter: PrismaAdapter(options.prisma),\n trustHost: options.trustHost ?? parseBoolean(process.env.AUTH_TRUST_HOST, true),\n debug: options.authDebug ?? parseBoolean(process.env.AUTH_DEBUG, false),\n logger: options.authDebug\n ? {\n error(error: Error) {\n console.error(\"[authjs:error]\", error.name, error.message, error.cause ?? \"\");\n },\n warn(code: string) {\n console.warn(\"[authjs:warn]\", code);\n },\n debug(message: string, metadata?: unknown) {\n console.log(\"[authjs:debug]\", message, metadata ?? \"\");\n }\n }\n : undefined,\n session: { strategy: \"database\" as const },\n secret: options.authSecret ?? process.env.AUTH_SECRET,\n cookies: {\n sessionToken: {\n name: options.sessionCookieName,\n options: {\n httpOnly: true,\n sameSite: \"lax\" as const,\n path: \"/\",\n secure: options.sessionCookieSecure\n }\n }\n },\n providers,\n pages: {\n signIn: signInPath\n },\n callbacks: {\n redirect: async ({ url, baseUrl }: { url: string; baseUrl: string }) => {\n const clientOrigin = new URL(options.clientUrl).origin;\n const successUrl = new URL(authenticatedRedirectPath, clientOrigin).toString();\n\n const shouldForceChat = (pathname: string, searchParams: URLSearchParams): boolean => {\n if (pathname !== \"/\" && pathname !== signInPath) {\n return false;\n }\n return !searchParams.has(\"error\");\n };\n\n if (url.startsWith(\"/\")) {\n const relative = new URL(url, clientOrigin);\n if (shouldForceChat(relative.pathname, relative.searchParams)) {\n return successUrl;\n }\n return `${clientOrigin}${relative.pathname}${relative.search}${relative.hash}`;\n }\n\n try {\n const target = new URL(url);\n const base = new URL(baseUrl);\n\n if (target.origin === clientOrigin) {\n if (shouldForceChat(target.pathname, target.searchParams)) {\n return successUrl;\n }\n return target.toString();\n }\n\n if (target.origin === base.origin) {\n if (shouldForceChat(target.pathname, target.searchParams)) {\n return successUrl;\n }\n return `${clientOrigin}${target.pathname}${target.search}${target.hash}`;\n }\n } catch {\n return successUrl;\n }\n\n return successUrl;\n }\n }\n };\n\n const authHandler = ExpressAuth(authConfig);\n\n const requireSession: RequestHandler = async (req, res, next) => {\n const token = extractSessionToken(req.headers.cookie);\n const authDebug = options.authDebug ?? parseBoolean(process.env.AUTH_DEBUG, false);\n\n if (!token) {\n const payload: { error: string; reason?: string } = { error: \"Unauthorized\" };\n if (authDebug) {\n payload.reason = \"missing_session_cookie\";\n }\n return res.status(401).json(payload);\n }\n\n const session = await options.prisma.session.findUnique({\n where: { sessionToken: token },\n include: {\n user: {\n select: options.sessionUserSelect\n }\n }\n });\n\n if (!session || session.expires <= new Date()) {\n const payload: { error: string; reason?: string } = { error: \"Unauthorized\" };\n if (authDebug) {\n payload.reason = !session ? \"session_not_found\" : \"session_expired\";\n }\n return res.status(401).json(payload);\n }\n\n const authUser = options.mapSessionUser(session.user);\n (req as Request & { authUser?: TAuthUser }).authUser = authUser;\n await options.onSessionValidated?.(authUser);\n next();\n };\n\n const extractSessionToken = (cookieHeader: string | undefined): string | null => {\n if (!cookieHeader) {\n return null;\n }\n\n const cookies = cookieHeader\n .split(\";\")\n .map((part) => part.trim())\n .map((part) => {\n const index = part.indexOf(\"=\");\n if (index < 0) {\n return null;\n }\n return [part.slice(0, index), decodeURIComponent(part.slice(index + 1))] as const;\n })\n .filter((entry): entry is readonly [string, string] => entry !== null);\n\n const possibleNames = [\n options.sessionCookieName,\n ...(options.extraSessionCookieNames ?? []),\n \"__Secure-authjs.session-token\",\n \"authjs.session-token\",\n \"__Secure-next-auth.session-token\",\n \"next-auth.session-token\"\n ];\n\n for (const name of possibleNames) {\n const exact = cookies.find(([cookieName]) => cookieName === name);\n if (exact) {\n return exact[1];\n }\n\n const chunks = cookies\n .filter(([cookieName]) => cookieName.startsWith(`${name}.`))\n .map(([cookieName, value]) => {\n const suffix = cookieName.slice(name.length + 1);\n return [Number.parseInt(suffix, 10), value] as const;\n })\n .filter(([index]) => Number.isInteger(index))\n .sort((left, right) => left[0] - right[0]);\n\n if (chunks.length > 0) {\n return chunks.map(([, value]) => value).join(\"\");\n }\n }\n\n return null;\n };\n\n return {\n authConfig,\n authHandler,\n requireSession,\n extractSessionToken,\n googleAuthEnabled,\n slackAuthEnabled\n };\n}\n","import { createHash, randomBytes } from \"node:crypto\";\nimport type { Express, RequestHandler } from \"express\";\nimport { z } from \"zod\";\n\ntype RegisterAuthApiRoutesOptions = {\n app: Express;\n prisma: any;\n authHandler: RequestHandler;\n requireSession: RequestHandler;\n extractSessionToken: (cookieHeader: string | undefined) => string | null;\n providersAvailability: Record;\n sessionCookieName: string;\n sessionCookieSecure: boolean;\n extraCookieNamesToClear?: string[];\n messages?: Partial;\n authBasePath?: string;\n authApiBasePath?: string;\n mePath?: string;\n normalizeEmail?: (email: string) => string;\n passwordHasher?: (password: string) => Promise;\n passwordComparator?: (password: string, passwordHash: string) => Promise;\n passwordReset?: {\n enabled: boolean;\n tokenTtlMs?: number;\n identifierPrefix?: string;\n buildResetUrl: (token: string) => string;\n sendMessage: (input: {\n user: { id: string; email: string; name: string | null; passwordHash: string | null };\n resetUrl: string;\n isPasswordCreation: boolean;\n expiresAt: Date;\n }) => Promise;\n };\n onUserRegistered?: (user: { id: string; email: string | null; name: string | null }) => Promise | void;\n onPasswordResetConfirmed?: (user: { id: string; email: string | null; name: string | null }) => Promise | void;\n};\n\ntype AuthRouteMessages = {\n invalidPayload: string;\n emailAlreadyUsed: string;\n accountNotFound: string;\n externalAccountOnly: string;\n invalidPassword: string;\n passwordResetUnavailable: string;\n invalidResetLink: string;\n expiredResetLink: string;\n};\n\nconst defaultNormalizeEmail = (email: string) => email.trim();\nconst defaultPasswordResetIdentifierPrefix = \"password-reset:\";\nconst defaultMessages: AuthRouteMessages = {\n invalidPayload: \"Invalid payload\",\n emailAlreadyUsed: \"Email already used\",\n accountNotFound: \"Account not found\",\n externalAccountOnly: \"This account uses an external sign-in provider.\",\n invalidPassword: \"Invalid password\",\n passwordResetUnavailable: \"Email service is not configured.\",\n invalidResetLink: \"Invalid reset link\",\n expiredResetLink: \"Invalid or expired reset link\"\n};\n\nfunction hashPasswordResetToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n}\n\nfunction buildPasswordResetIdentifier(prefix: string, userId: string): string {\n return `${prefix}${userId}`;\n}\n\nexport function registerAuthApiRoutes(options: RegisterAuthApiRoutesOptions): void {\n const authBasePath = options.authBasePath ?? \"/auth\";\n const authApiBasePath = options.authApiBasePath ?? \"/api/auth\";\n const mePath = options.mePath ?? \"/api/me\";\n const normalizeEmail = options.normalizeEmail ?? defaultNormalizeEmail;\n const passwordHasher = options.passwordHasher ?? ((password: string) => Promise.resolve(password));\n const passwordComparator = options.passwordComparator ?? ((password: string, hash: string) => Promise.resolve(password === hash));\n const passwordResetIdentifierPrefix = options.passwordReset?.identifierPrefix ?? defaultPasswordResetIdentifierPrefix;\n const messages = { ...defaultMessages, ...(options.messages ?? {}) };\n\n const findUserByEmail = async (email: string) => {\n const normalized = normalizeEmail(email);\n const lowered = normalized.toLowerCase();\n\n return options.prisma.user.findFirst({\n where: {\n OR: lowered === normalized ? [{ email: normalized }] : [{ email: normalized }, { email: lowered }]\n },\n select: {\n id: true,\n email: true,\n name: true,\n image: true,\n passwordHash: true,\n emailVerified: true\n }\n });\n };\n\n const getPasswordResetContext = async (\n rawToken: string\n ): Promise<\n | {\n verificationToken: { identifier: string; expires: Date };\n user: { id: string; email: string | null; name: string | null; passwordHash: string | null; emailVerified: Date | null };\n }\n | null\n > => {\n const verificationToken = await options.prisma.verificationToken.findUnique({\n where: { token: hashPasswordResetToken(rawToken) },\n select: { identifier: true, expires: true }\n });\n\n if (\n !verificationToken ||\n verificationToken.expires <= new Date() ||\n !verificationToken.identifier.startsWith(passwordResetIdentifierPrefix)\n ) {\n return null;\n }\n\n const userId = verificationToken.identifier.slice(passwordResetIdentifierPrefix.length);\n if (!userId) {\n return null;\n }\n\n const user = await options.prisma.user.findUnique({\n where: { id: userId },\n select: {\n id: true,\n email: true,\n name: true,\n passwordHash: true,\n emailVerified: true\n }\n });\n\n if (!user?.email) {\n return null;\n }\n\n return { verificationToken, user };\n };\n\n options.app.use(authBasePath, options.authHandler);\n\n options.app.get(`${authApiBasePath}/providers`, (_req, res) => {\n res.json(options.providersAvailability);\n });\n\n options.app.post(`${authApiBasePath}/register`, async (req, res) => {\n const parsed = z\n .object({\n name: z.string().min(2).max(60),\n email: z.string().email(),\n password: z.string().min(8)\n })\n .safeParse(req.body);\n\n if (!parsed.success) {\n return res.status(400).json({ error: messages.invalidPayload });\n }\n\n const email = normalizeEmail(parsed.data.email);\n const exists = await findUserByEmail(email);\n if (exists) {\n return res.status(409).json({ error: messages.emailAlreadyUsed });\n }\n\n const passwordHash = await passwordHasher(parsed.data.password);\n const created = await options.prisma.user.create({\n data: {\n name: parsed.data.name,\n email,\n passwordHash\n },\n select: { id: true, email: true, name: true }\n });\n\n await options.onUserRegistered?.(created);\n return res.status(201).json(created);\n });\n\n options.app.post(`${authApiBasePath}/login`, async (req, res) => {\n const parsed = z\n .object({\n email: z.string().email(),\n password: z.string().min(8)\n })\n .safeParse(req.body);\n\n if (!parsed.success) {\n return res.status(400).json({ error: messages.invalidPayload });\n }\n\n const email = normalizeEmail(parsed.data.email);\n const user = await findUserByEmail(email);\n if (!user) {\n return res.status(404).json({ error: messages.accountNotFound });\n }\n\n if (!user.passwordHash) {\n return res.status(400).json({ error: messages.externalAccountOnly });\n }\n\n const valid = await passwordComparator(parsed.data.password, user.passwordHash);\n if (!valid) {\n return res.status(401).json({ error: messages.invalidPassword });\n }\n\n const sessionToken = randomBytes(32).toString(\"hex\");\n const expires = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);\n\n await options.prisma.session.create({\n data: {\n sessionToken,\n userId: user.id,\n expires\n }\n });\n\n res.cookie(options.sessionCookieName, sessionToken, {\n httpOnly: true,\n sameSite: \"lax\",\n secure: options.sessionCookieSecure,\n path: \"/\",\n expires\n });\n\n return res.status(200).json({ ok: true });\n });\n\n options.app.post(`${authApiBasePath}/password-reset/request`, async (req, res) => {\n const parsed = z\n .object({\n email: z.string().email()\n })\n .safeParse(req.body);\n\n if (!parsed.success) {\n return res.status(400).json({ error: messages.invalidPayload });\n }\n\n if (!options.passwordReset?.enabled) {\n return res.status(503).json({ error: messages.passwordResetUnavailable });\n }\n\n const email = normalizeEmail(parsed.data.email);\n const user = await findUserByEmail(email);\n if (!user?.email) {\n return res.status(200).json({ ok: true });\n }\n\n const rawToken = randomBytes(32).toString(\"hex\");\n const identifier = buildPasswordResetIdentifier(passwordResetIdentifierPrefix, user.id);\n const expiresAt = new Date(Date.now() + (options.passwordReset.tokenTtlMs ?? 2 * 60 * 60 * 1000));\n const resetUrl = options.passwordReset.buildResetUrl(rawToken);\n const isPasswordCreation = !user.passwordHash;\n\n await options.prisma.verificationToken.deleteMany({\n where: {\n OR: [{ identifier }, { expires: { lt: new Date() } }]\n }\n });\n\n await options.prisma.verificationToken.create({\n data: {\n identifier,\n token: hashPasswordResetToken(rawToken),\n expires: expiresAt\n }\n });\n\n await options.passwordReset.sendMessage({\n user: {\n id: user.id,\n email: user.email,\n name: user.name,\n passwordHash: user.passwordHash\n },\n resetUrl,\n isPasswordCreation,\n expiresAt\n });\n\n return res.status(200).json({ ok: true });\n });\n\n options.app.get(`${authApiBasePath}/password-reset/validate`, async (req, res) => {\n if (!options.passwordReset?.enabled) {\n return res.status(400).json({ error: messages.expiredResetLink });\n }\n\n const parsed = z.object({ token: z.string().min(1) }).safeParse({\n token: Array.isArray(req.query.token) ? req.query.token[0] : req.query.token\n });\n\n if (!parsed.success) {\n return res.status(400).json({ error: messages.invalidResetLink });\n }\n\n const context = await getPasswordResetContext(parsed.data.token);\n if (!context) {\n return res.status(400).json({ error: messages.expiredResetLink });\n }\n\n return res.status(200).json({\n ok: true,\n email: context.user.email,\n mode: context.user.passwordHash ? \"reset\" : \"create\"\n });\n });\n\n options.app.post(`${authApiBasePath}/password-reset/confirm`, async (req, res) => {\n if (!options.passwordReset?.enabled) {\n return res.status(400).json({ error: messages.expiredResetLink });\n }\n\n const parsed = z\n .object({\n token: z.string().min(1),\n password: z.string().min(8)\n })\n .safeParse(req.body);\n\n if (!parsed.success) {\n return res.status(400).json({ error: messages.invalidPayload });\n }\n\n const context = await getPasswordResetContext(parsed.data.token);\n if (!context) {\n return res.status(400).json({ error: messages.expiredResetLink });\n }\n\n const passwordHash = await passwordHasher(parsed.data.password);\n await options.prisma.$transaction([\n options.prisma.verificationToken.deleteMany({\n where: { identifier: context.verificationToken.identifier }\n }),\n options.prisma.session.deleteMany({\n where: { userId: context.user.id }\n }),\n options.prisma.user.update({\n where: { id: context.user.id },\n data: {\n passwordHash,\n emailVerified: context.user.emailVerified ?? new Date()\n }\n })\n ]);\n\n await options.onPasswordResetConfirmed?.(context.user);\n return res.status(200).json({ ok: true });\n });\n\n options.app.post(`${authApiBasePath}/logout`, async (req, res) => {\n const token = options.extractSessionToken(req.headers.cookie);\n\n if (token) {\n await options.prisma.session.deleteMany({ where: { sessionToken: token } });\n }\n\n const cookieNamesToClear = [\n options.sessionCookieName,\n ...(options.extraCookieNamesToClear ?? []),\n \"authjs.session-token\",\n \"__Secure-authjs.session-token\",\n \"next-auth.session-token\",\n \"__Secure-next-auth.session-token\"\n ];\n\n for (const cookieName of cookieNamesToClear) {\n res.clearCookie(cookieName, { path: \"/\" });\n }\n\n return res.status(200).json({ ok: true });\n });\n\n options.app.get(mePath, options.requireSession, async (req, res) => {\n res.json({ user: (req as { authUser?: unknown }).authUser });\n });\n}\n"],"mappings":";AAAA,SAAS,mBAAmB;AAC5B,OAAO,iBAAiB;AACxB,OAAO,YAAY;AACnB,OAAO,WAAW;AAClB,SAAS,qBAAqB;AAE9B,SAAS,SAAS;AAiClB,SAAS,aAAa,OAA2B,UAA4B;AAC3E,MAAI,UAAU,QAAW;AACvB,WAAO;AAAA,EACT;AACA,QAAM,aAAa,MAAM,KAAK,EAAE,YAAY;AAC5C,MAAI,CAAC,KAAK,QAAQ,OAAO,IAAI,EAAE,SAAS,UAAU,GAAG;AACnD,WAAO;AAAA,EACT;AACA,MAAI,CAAC,KAAK,SAAS,MAAM,KAAK,EAAE,SAAS,UAAU,GAAG;AACpD,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEO,SAAS,iBAA4B,SAA6C;AACvF,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,4BAA4B,QAAQ,6BAA6B;AACvE,QAAM,oBAAoB,QAAQ,QAAQ,kBAAkB,QAAQ,kBAAkB;AACtF,QAAM,mBAAmB,QAAQ,QAAQ,iBAAiB,QAAQ,iBAAiB;AAEnF,QAAM,YAAmB;AAAA,IACvB,YAAY;AAAA,MACV,MAAM;AAAA,MACN,aAAa;AAAA,QACX,OAAO,EAAE,OAAO,SAAS,MAAM,QAAQ;AAAA,QACvC,UAAU,EAAE,OAAO,YAAY,MAAM,WAAW;AAAA,MAClD;AAAA,MACA,WAAW,OAAO,mBAAmB;AACnC,cAAM,SAAS,EACZ,OAAO;AAAA,UACN,OAAO,EAAE,OAAO,EAAE,MAAM;AAAA,UACxB,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,QAC5B,CAAC,EACA,UAAU,cAAc;AAE3B,YAAI,CAAC,OAAO,SAAS;AACnB,iBAAO;AAAA,QACT;AAEA,cAAM,OAAO,MAAM,QAAQ,2BAA2B,OAAO,KAAK,KAAK;AACvE,YAAI,CAAC,MAAM,cAAc;AACvB,iBAAO;AAAA,QACT;AAEA,cAAM,QAAQ,MAAM,QAAQ,gBAAgB,OAAO,KAAK,UAAU,KAAK,YAAY;AACnF,YAAI,CAAC,OAAO;AACV,iBAAO;AAAA,QACT;AAEA,eAAO;AAAA,UACL,IAAI,KAAK;AAAA,UACT,MAAM,KAAK;AAAA,UACX,OAAO,KAAK;AAAA,UACZ,OAAO,KAAK,SAAS;AAAA,QACvB;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AAEA,MAAI,mBAAmB;AACrB,cAAU;AAAA,MACR,OAAO;AAAA,QACL,UAAU,QAAQ;AAAA,QAClB,cAAc,QAAQ;AAAA,QACtB,eAAe;AAAA,UACb,QAAQ;AAAA,YACN,QAAQ;AAAA,UACV;AAAA,QACF;AAAA,QACA,mCAAmC;AAAA,MACrC,CAAC;AAAA,IACH;AAAA,EACF;AAEA,MAAI,kBAAkB;AACpB,cAAU;AAAA,MACR,MAAM;AAAA,QACJ,UAAU,QAAQ;AAAA,QAClB,cAAc,QAAQ;AAAA,QACtB,mCAAmC;AAAA,MACrC,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,aAAa;AAAA,IACjB,SAAS,cAAc,QAAQ,MAAM;AAAA,IACrC,WAAW,QAAQ,aAAa,aAAa,QAAQ,IAAI,iBAAiB,IAAI;AAAA,IAC9E,OAAO,QAAQ,aAAa,aAAa,QAAQ,IAAI,YAAY,KAAK;AAAA,IACtE,QAAQ,QAAQ,YACZ;AAAA,MACE,MAAM,OAAc;AAClB,gBAAQ,MAAM,kBAAkB,MAAM,MAAM,MAAM,SAAS,MAAM,SAAS,EAAE;AAAA,MAC9E;AAAA,MACA,KAAK,MAAc;AACjB,gBAAQ,KAAK,iBAAiB,IAAI;AAAA,MACpC;AAAA,MACA,MAAM,SAAiB,UAAoB;AACzC,gBAAQ,IAAI,kBAAkB,SAAS,YAAY,EAAE;AAAA,MACvD;AAAA,IACF,IACA;AAAA,IACJ,SAAS,EAAE,UAAU,WAAoB;AAAA,IACzC,QAAQ,QAAQ,cAAc,QAAQ,IAAI;AAAA,IAC1C,SAAS;AAAA,MACP,cAAc;AAAA,QACZ,MAAM,QAAQ;AAAA,QACd,SAAS;AAAA,UACP,UAAU;AAAA,UACV,UAAU;AAAA,UACV,MAAM;AAAA,UACN,QAAQ,QAAQ;AAAA,QAClB;AAAA,MACF;AAAA,IACF;AAAA,IACA;AAAA,IACA,OAAO;AAAA,MACL,QAAQ;AAAA,IACV;AAAA,IACA,WAAW;AAAA,MACT,UAAU,OAAO,EAAE,KAAK,QAAQ,MAAwC;AACtE,cAAM,eAAe,IAAI,IAAI,QAAQ,SAAS,EAAE;AAChD,cAAM,aAAa,IAAI,IAAI,2BAA2B,YAAY,EAAE,SAAS;AAE7E,cAAM,kBAAkB,CAAC,UAAkB,iBAA2C;AACpF,cAAI,aAAa,OAAO,aAAa,YAAY;AAC/C,mBAAO;AAAA,UACT;AACA,iBAAO,CAAC,aAAa,IAAI,OAAO;AAAA,QAClC;AAEA,YAAI,IAAI,WAAW,GAAG,GAAG;AACvB,gBAAM,WAAW,IAAI,IAAI,KAAK,YAAY;AAC1C,cAAI,gBAAgB,SAAS,UAAU,SAAS,YAAY,GAAG;AAC7D,mBAAO;AAAA,UACT;AACA,iBAAO,GAAG,YAAY,GAAG,SAAS,QAAQ,GAAG,SAAS,MAAM,GAAG,SAAS,IAAI;AAAA,QAC9E;AAEA,YAAI;AACF,gBAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,gBAAM,OAAO,IAAI,IAAI,OAAO;AAE5B,cAAI,OAAO,WAAW,cAAc;AAClC,gBAAI,gBAAgB,OAAO,UAAU,OAAO,YAAY,GAAG;AACzD,qBAAO;AAAA,YACT;AACA,mBAAO,OAAO,SAAS;AAAA,UACzB;AAEA,cAAI,OAAO,WAAW,KAAK,QAAQ;AACjC,gBAAI,gBAAgB,OAAO,UAAU,OAAO,YAAY,GAAG;AACzD,qBAAO;AAAA,YACT;AACA,mBAAO,GAAG,YAAY,GAAG,OAAO,QAAQ,GAAG,OAAO,MAAM,GAAG,OAAO,IAAI;AAAA,UACxE;AAAA,QACF,QAAQ;AACN,iBAAO;AAAA,QACT;AAEA,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAEA,QAAM,cAAc,YAAY,UAAU;AAE1C,QAAM,iBAAiC,OAAO,KAAK,KAAK,SAAS;AAC/D,UAAM,QAAQ,oBAAoB,IAAI,QAAQ,MAAM;AACpD,UAAM,YAAY,QAAQ,aAAa,aAAa,QAAQ,IAAI,YAAY,KAAK;AAEjF,QAAI,CAAC,OAAO;AACV,YAAM,UAA8C,EAAE,OAAO,eAAe;AAC5E,UAAI,WAAW;AACb,gBAAQ,SAAS;AAAA,MACnB;AACA,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,OAAO;AAAA,IACrC;AAEA,UAAM,UAAU,MAAM,QAAQ,OAAO,QAAQ,WAAW;AAAA,MACtD,OAAO,EAAE,cAAc,MAAM;AAAA,MAC7B,SAAS;AAAA,QACP,MAAM;AAAA,UACJ,QAAQ,QAAQ;AAAA,QAClB;AAAA,MACF;AAAA,IACF,CAAC;AAED,QAAI,CAAC,WAAW,QAAQ,WAAW,oBAAI,KAAK,GAAG;AAC7C,YAAM,UAA8C,EAAE,OAAO,eAAe;AAC5E,UAAI,WAAW;AACb,gBAAQ,SAAS,CAAC,UAAU,sBAAsB;AAAA,MACpD;AACA,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,OAAO;AAAA,IACrC;AAEA,UAAM,WAAW,QAAQ,eAAe,QAAQ,IAAI;AACpD,IAAC,IAA2C,WAAW;AACvD,UAAM,QAAQ,qBAAqB,QAAQ;AAC3C,SAAK;AAAA,EACP;AAEA,QAAM,sBAAsB,CAAC,iBAAoD;AAC/E,QAAI,CAAC,cAAc;AACjB,aAAO;AAAA,IACT;AAEA,UAAM,UAAU,aACb,MAAM,GAAG,EACT,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,EACzB,IAAI,CAAC,SAAS;AACb,YAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,UAAI,QAAQ,GAAG;AACb,eAAO;AAAA,MACT;AACA,aAAO,CAAC,KAAK,MAAM,GAAG,KAAK,GAAG,mBAAmB,KAAK,MAAM,QAAQ,CAAC,CAAC,CAAC;AAAA,IACzE,CAAC,EACA,OAAO,CAAC,UAA8C,UAAU,IAAI;AAEvE,UAAM,gBAAgB;AAAA,MACpB,QAAQ;AAAA,MACR,GAAI,QAAQ,2BAA2B,CAAC;AAAA,MACxC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,eAAW,QAAQ,eAAe;AAChC,YAAM,QAAQ,QAAQ,KAAK,CAAC,CAAC,UAAU,MAAM,eAAe,IAAI;AAChE,UAAI,OAAO;AACT,eAAO,MAAM,CAAC;AAAA,MAChB;AAEA,YAAM,SAAS,QACZ,OAAO,CAAC,CAAC,UAAU,MAAM,WAAW,WAAW,GAAG,IAAI,GAAG,CAAC,EAC1D,IAAI,CAAC,CAAC,YAAY,KAAK,MAAM;AAC5B,cAAM,SAAS,WAAW,MAAM,KAAK,SAAS,CAAC;AAC/C,eAAO,CAAC,OAAO,SAAS,QAAQ,EAAE,GAAG,KAAK;AAAA,MAC5C,CAAC,EACA,OAAO,CAAC,CAAC,KAAK,MAAM,OAAO,UAAU,KAAK,CAAC,EAC3C,KAAK,CAAC,MAAM,UAAU,KAAK,CAAC,IAAI,MAAM,CAAC,CAAC;AAE3C,UAAI,OAAO,SAAS,GAAG;AACrB,eAAO,OAAO,IAAI,CAAC,CAAC,EAAE,KAAK,MAAM,KAAK,EAAE,KAAK,EAAE;AAAA,MACjD;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;ACzSA,SAAS,YAAY,mBAAmB;AAExC,SAAS,KAAAA,UAAS;AA8ClB,IAAM,wBAAwB,CAAC,UAAkB,MAAM,KAAK;AAC5D,IAAM,uCAAuC;AAC7C,IAAM,kBAAqC;AAAA,EACzC,gBAAgB;AAAA,EAChB,kBAAkB;AAAA,EAClB,iBAAiB;AAAA,EACjB,qBAAqB;AAAA,EACrB,iBAAiB;AAAA,EACjB,0BAA0B;AAAA,EAC1B,kBAAkB;AAAA,EAClB,kBAAkB;AACpB;AAEA,SAAS,uBAAuB,OAAuB;AACrD,SAAO,WAAW,QAAQ,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACxD;AAEA,SAAS,6BAA6B,QAAgB,QAAwB;AAC5E,SAAO,GAAG,MAAM,GAAG,MAAM;AAC3B;AAEO,SAAS,sBAAsB,SAA6C;AACjF,QAAM,eAAe,QAAQ,gBAAgB;AAC7C,QAAM,kBAAkB,QAAQ,mBAAmB;AACnD,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,iBAAiB,QAAQ,kBAAkB;AACjD,QAAM,iBAAiB,QAAQ,mBAAmB,CAAC,aAAqB,QAAQ,QAAQ,QAAQ;AAChG,QAAM,qBAAqB,QAAQ,uBAAuB,CAAC,UAAkB,SAAiB,QAAQ,QAAQ,aAAa,IAAI;AAC/H,QAAM,gCAAgC,QAAQ,eAAe,oBAAoB;AACjF,QAAM,WAAW,EAAE,GAAG,iBAAiB,GAAI,QAAQ,YAAY,CAAC,EAAG;AAEnE,QAAM,kBAAkB,OAAO,UAAkB;AAC/C,UAAM,aAAa,eAAe,KAAK;AACvC,UAAM,UAAU,WAAW,YAAY;AAEvC,WAAO,QAAQ,OAAO,KAAK,UAAU;AAAA,MACnC,OAAO;AAAA,QACL,IAAI,YAAY,aAAa,CAAC,EAAE,OAAO,WAAW,CAAC,IAAI,CAAC,EAAE,OAAO,WAAW,GAAG,EAAE,OAAO,QAAQ,CAAC;AAAA,MACnG;AAAA,MACA,QAAQ;AAAA,QACN,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,OAAO;AAAA,QACP,cAAc;AAAA,QACd,eAAe;AAAA,MACjB;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,0BAA0B,OAC9B,aAOG;AACH,UAAM,oBAAoB,MAAM,QAAQ,OAAO,kBAAkB,WAAW;AAAA,MAC1E,OAAO,EAAE,OAAO,uBAAuB,QAAQ,EAAE;AAAA,MACjD,QAAQ,EAAE,YAAY,MAAM,SAAS,KAAK;AAAA,IAC5C,CAAC;AAED,QACE,CAAC,qBACD,kBAAkB,WAAW,oBAAI,KAAK,KACtC,CAAC,kBAAkB,WAAW,WAAW,6BAA6B,GACtE;AACA,aAAO;AAAA,IACT;AAEA,UAAM,SAAS,kBAAkB,WAAW,MAAM,8BAA8B,MAAM;AACtF,QAAI,CAAC,QAAQ;AACX,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,MAAM,QAAQ,OAAO,KAAK,WAAW;AAAA,MAChD,OAAO,EAAE,IAAI,OAAO;AAAA,MACpB,QAAQ;AAAA,QACN,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,cAAc;AAAA,QACd,eAAe;AAAA,MACjB;AAAA,IACF,CAAC;AAED,QAAI,CAAC,MAAM,OAAO;AAChB,aAAO;AAAA,IACT;AAEA,WAAO,EAAE,mBAAmB,KAAK;AAAA,EACnC;AAEA,UAAQ,IAAI,IAAI,cAAc,QAAQ,WAAW;AAEjD,UAAQ,IAAI,IAAI,GAAG,eAAe,cAAc,CAAC,MAAM,QAAQ;AAC7D,QAAI,KAAK,QAAQ,qBAAqB;AAAA,EACxC,CAAC;AAED,UAAQ,IAAI,KAAK,GAAG,eAAe,aAAa,OAAO,KAAK,QAAQ;AAClE,UAAM,SAASA,GACZ,OAAO;AAAA,MACN,MAAMA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE;AAAA,MAC9B,OAAOA,GAAE,OAAO,EAAE,MAAM;AAAA,MACxB,UAAUA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC5B,CAAC,EACA,UAAU,IAAI,IAAI;AAErB,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,eAAe,CAAC;AAAA,IAChE;AAEA,UAAM,QAAQ,eAAe,OAAO,KAAK,KAAK;AAC9C,UAAM,SAAS,MAAM,gBAAgB,KAAK;AAC1C,QAAI,QAAQ;AACV,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,iBAAiB,CAAC;AAAA,IAClE;AAEA,UAAM,eAAe,MAAM,eAAe,OAAO,KAAK,QAAQ;AAC9D,UAAM,UAAU,MAAM,QAAQ,OAAO,KAAK,OAAO;AAAA,MAC/C,MAAM;AAAA,QACJ,MAAM,OAAO,KAAK;AAAA,QAClB;AAAA,QACA;AAAA,MACF;AAAA,MACA,QAAQ,EAAE,IAAI,MAAM,OAAO,MAAM,MAAM,KAAK;AAAA,IAC9C,CAAC;AAED,UAAM,QAAQ,mBAAmB,OAAO;AACxC,WAAO,IAAI,OAAO,GAAG,EAAE,KAAK,OAAO;AAAA,EACrC,CAAC;AAED,UAAQ,IAAI,KAAK,GAAG,eAAe,UAAU,OAAO,KAAK,QAAQ;AAC/D,UAAM,SAASA,GACZ,OAAO;AAAA,MACN,OAAOA,GAAE,OAAO,EAAE,MAAM;AAAA,MACxB,UAAUA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC5B,CAAC,EACA,UAAU,IAAI,IAAI;AAErB,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,eAAe,CAAC;AAAA,IAChE;AAEA,UAAM,QAAQ,eAAe,OAAO,KAAK,KAAK;AAC9C,UAAM,OAAO,MAAM,gBAAgB,KAAK;AACxC,QAAI,CAAC,MAAM;AACT,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,gBAAgB,CAAC;AAAA,IACjE;AAEA,QAAI,CAAC,KAAK,cAAc;AACtB,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,oBAAoB,CAAC;AAAA,IACrE;AAEA,UAAM,QAAQ,MAAM,mBAAmB,OAAO,KAAK,UAAU,KAAK,YAAY;AAC9E,QAAI,CAAC,OAAO;AACV,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,gBAAgB,CAAC;AAAA,IACjE;AAEA,UAAM,eAAe,YAAY,EAAE,EAAE,SAAS,KAAK;AACnD,UAAM,UAAU,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,KAAK,KAAK,GAAI;AAE9D,UAAM,QAAQ,OAAO,QAAQ,OAAO;AAAA,MAClC,MAAM;AAAA,QACJ;AAAA,QACA,QAAQ,KAAK;AAAA,QACb;AAAA,MACF;AAAA,IACF,CAAC;AAED,QAAI,OAAO,QAAQ,mBAAmB,cAAc;AAAA,MAClD,UAAU;AAAA,MACV,UAAU;AAAA,MACV,QAAQ,QAAQ;AAAA,MAChB,MAAM;AAAA,MACN;AAAA,IACF,CAAC;AAED,WAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EAC1C,CAAC;AAED,UAAQ,IAAI,KAAK,GAAG,eAAe,2BAA2B,OAAO,KAAK,QAAQ;AAChF,UAAM,SAASA,GACZ,OAAO;AAAA,MACN,OAAOA,GAAE,OAAO,EAAE,MAAM;AAAA,IAC1B,CAAC,EACA,UAAU,IAAI,IAAI;AAErB,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,eAAe,CAAC;AAAA,IAChE;AAEA,QAAI,CAAC,QAAQ,eAAe,SAAS;AACnC,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,yBAAyB,CAAC;AAAA,IAC1E;AAEA,UAAM,QAAQ,eAAe,OAAO,KAAK,KAAK;AAC9C,UAAM,OAAO,MAAM,gBAAgB,KAAK;AACxC,QAAI,CAAC,MAAM,OAAO;AAChB,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,IAC1C;AAEA,UAAM,WAAW,YAAY,EAAE,EAAE,SAAS,KAAK;AAC/C,UAAM,aAAa,6BAA6B,+BAA+B,KAAK,EAAE;AACtF,UAAM,YAAY,IAAI,KAAK,KAAK,IAAI,KAAK,QAAQ,cAAc,cAAc,IAAI,KAAK,KAAK,IAAK;AAChG,UAAM,WAAW,QAAQ,cAAc,cAAc,QAAQ;AAC7D,UAAM,qBAAqB,CAAC,KAAK;AAEjC,UAAM,QAAQ,OAAO,kBAAkB,WAAW;AAAA,MAChD,OAAO;AAAA,QACL,IAAI,CAAC,EAAE,WAAW,GAAG,EAAE,SAAS,EAAE,IAAI,oBAAI,KAAK,EAAE,EAAE,CAAC;AAAA,MACtD;AAAA,IACF,CAAC;AAED,UAAM,QAAQ,OAAO,kBAAkB,OAAO;AAAA,MAC5C,MAAM;AAAA,QACJ;AAAA,QACA,OAAO,uBAAuB,QAAQ;AAAA,QACtC,SAAS;AAAA,MACX;AAAA,IACF,CAAC;AAED,UAAM,QAAQ,cAAc,YAAY;AAAA,MACtC,MAAM;AAAA,QACJ,IAAI,KAAK;AAAA,QACT,OAAO,KAAK;AAAA,QACZ,MAAM,KAAK;AAAA,QACX,cAAc,KAAK;AAAA,MACrB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AAED,WAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EAC1C,CAAC;AAED,UAAQ,IAAI,IAAI,GAAG,eAAe,4BAA4B,OAAO,KAAK,QAAQ;AAChF,QAAI,CAAC,QAAQ,eAAe,SAAS;AACnC,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,iBAAiB,CAAC;AAAA,IAClE;AAEA,UAAM,SAASA,GAAE,OAAO,EAAE,OAAOA,GAAE,OAAO,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,UAAU;AAAA,MAC9D,OAAO,MAAM,QAAQ,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,MAAM,CAAC,IAAI,IAAI,MAAM;AAAA,IACzE,CAAC;AAED,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,iBAAiB,CAAC;AAAA,IAClE;AAEA,UAAM,UAAU,MAAM,wBAAwB,OAAO,KAAK,KAAK;AAC/D,QAAI,CAAC,SAAS;AACZ,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,iBAAiB,CAAC;AAAA,IAClE;AAEA,WAAO,IAAI,OAAO,GAAG,EAAE,KAAK;AAAA,MAC1B,IAAI;AAAA,MACJ,OAAO,QAAQ,KAAK;AAAA,MACpB,MAAM,QAAQ,KAAK,eAAe,UAAU;AAAA,IAC9C,CAAC;AAAA,EACH,CAAC;AAED,UAAQ,IAAI,KAAK,GAAG,eAAe,2BAA2B,OAAO,KAAK,QAAQ;AAChF,QAAI,CAAC,QAAQ,eAAe,SAAS;AACnC,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,iBAAiB,CAAC;AAAA,IAClE;AAEA,UAAM,SAASA,GACZ,OAAO;AAAA,MACN,OAAOA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA,MACvB,UAAUA,GAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IAC5B,CAAC,EACA,UAAU,IAAI,IAAI;AAErB,QAAI,CAAC,OAAO,SAAS;AACnB,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,eAAe,CAAC;AAAA,IAChE;AAEA,UAAM,UAAU,MAAM,wBAAwB,OAAO,KAAK,KAAK;AAC/D,QAAI,CAAC,SAAS;AACZ,aAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,OAAO,SAAS,iBAAiB,CAAC;AAAA,IAClE;AAEA,UAAM,eAAe,MAAM,eAAe,OAAO,KAAK,QAAQ;AAC9D,UAAM,QAAQ,OAAO,aAAa;AAAA,MAChC,QAAQ,OAAO,kBAAkB,WAAW;AAAA,QAC1C,OAAO,EAAE,YAAY,QAAQ,kBAAkB,WAAW;AAAA,MAC5D,CAAC;AAAA,MACD,QAAQ,OAAO,QAAQ,WAAW;AAAA,QAChC,OAAO,EAAE,QAAQ,QAAQ,KAAK,GAAG;AAAA,MACnC,CAAC;AAAA,MACD,QAAQ,OAAO,KAAK,OAAO;AAAA,QACzB,OAAO,EAAE,IAAI,QAAQ,KAAK,GAAG;AAAA,QAC7B,MAAM;AAAA,UACJ;AAAA,UACA,eAAe,QAAQ,KAAK,iBAAiB,oBAAI,KAAK;AAAA,QACxD;AAAA,MACF,CAAC;AAAA,IACH,CAAC;AAED,UAAM,QAAQ,2BAA2B,QAAQ,IAAI;AACrD,WAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EAC1C,CAAC;AAED,UAAQ,IAAI,KAAK,GAAG,eAAe,WAAW,OAAO,KAAK,QAAQ;AAChE,UAAM,QAAQ,QAAQ,oBAAoB,IAAI,QAAQ,MAAM;AAE5D,QAAI,OAAO;AACT,YAAM,QAAQ,OAAO,QAAQ,WAAW,EAAE,OAAO,EAAE,cAAc,MAAM,EAAE,CAAC;AAAA,IAC5E;AAEA,UAAM,qBAAqB;AAAA,MACzB,QAAQ;AAAA,MACR,GAAI,QAAQ,2BAA2B,CAAC;AAAA,MACxC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAEA,eAAW,cAAc,oBAAoB;AAC3C,UAAI,YAAY,YAAY,EAAE,MAAM,IAAI,CAAC;AAAA,IAC3C;AAEA,WAAO,IAAI,OAAO,GAAG,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC;AAAA,EAC1C,CAAC;AAED,UAAQ,IAAI,IAAI,QAAQ,QAAQ,gBAAgB,OAAO,KAAK,QAAQ;AAClE,QAAI,KAAK,EAAE,MAAO,IAA+B,SAAS,CAAC;AAAA,EAC7D,CAAC;AACH;","names":["z"]} \ No newline at end of file +{"version":3,"sources":["../../server/module.ts","../../server/invites.ts","../../server/routes.ts"],"sourcesContent":["import { ExpressAuth } from \"@auth/express\";\nimport Credentials from \"@auth/express/providers/credentials\";\nimport Google from \"@auth/express/providers/google\";\nimport Slack from \"@auth/express/providers/slack\";\nimport { PrismaAdapter } from \"@auth/prisma-adapter\";\nimport type { Request, RequestHandler } from \"express\";\nimport { z } from \"zod\";\n\ntype CredentialUser = {\n id: string;\n name: string | null;\n email: string | null;\n image?: string | null;\n passwordHash: string | null;\n};\n\ntype CreateAuthModuleOptions = {\n prisma: any;\n clientUrl: string;\n sessionCookieName: string;\n sessionCookieSecure: boolean;\n authUrl?: string;\n authDebug?: boolean;\n authSecret?: string;\n trustHost?: boolean;\n extraSessionCookieNames?: string[];\n signInPath?: string;\n authenticatedRedirectPath?: string;\n googleClientId?: string;\n googleClientSecret?: string;\n slackClientId?: string;\n slackClientSecret?: string;\n findCredentialsUserByEmail: (email: string) => Promise;\n comparePassword: (password: string, passwordHash: string) => Promise;\n sessionUserSelect: Record;\n mapSessionUser: (user: any) => TAuthUser;\n onSessionValidated?: (user: TAuthUser) => Promise | void;\n};\n\nfunction parseBoolean(value: string | undefined, fallback: boolean): boolean {\n if (value === undefined) {\n return fallback;\n }\n const normalized = value.trim().toLowerCase();\n if ([\"1\", \"true\", \"yes\", \"on\"].includes(normalized)) {\n return true;\n }\n if ([\"0\", \"false\", \"no\", \"off\"].includes(normalized)) {\n return false;\n }\n return fallback;\n}\n\nexport function createAuthModule(options: CreateAuthModuleOptions) {\n const signInPath = options.signInPath ?? \"/login\";\n const authenticatedRedirectPath = options.authenticatedRedirectPath ?? \"/chat\";\n const googleAuthEnabled = Boolean(options.googleClientId && options.googleClientSecret);\n const slackAuthEnabled = Boolean(options.slackClientId && options.slackClientSecret);\n\n const providers: any[] = [\n Credentials({\n name: \"Email et mot de passe\",\n credentials: {\n email: { label: \"Email\", type: \"email\" },\n password: { label: \"Password\", type: \"password\" }\n },\n authorize: async (rawCredentials) => {\n const parsed = z\n .object({\n email: z.string().email(),\n password: z.string().min(8)\n })\n .safeParse(rawCredentials);\n\n if (!parsed.success) {\n return null;\n }\n\n const user = await options.findCredentialsUserByEmail(parsed.data.email);\n if (!user?.passwordHash) {\n return null;\n }\n\n const valid = await options.comparePassword(parsed.data.password, user.passwordHash);\n if (!valid) {\n return null;\n }\n\n return {\n id: user.id,\n name: user.name,\n email: user.email,\n image: user.image ?? null\n };\n }\n })\n ];\n\n if (googleAuthEnabled) {\n providers.push(\n Google({\n clientId: options.googleClientId!,\n clientSecret: options.googleClientSecret!,\n authorization: {\n params: {\n prompt: \"select_account\"\n }\n },\n allowDangerousEmailAccountLinking: true\n })\n );\n }\n\n if (slackAuthEnabled) {\n providers.push(\n Slack({\n clientId: options.slackClientId!,\n clientSecret: options.slackClientSecret!,\n allowDangerousEmailAccountLinking: true\n })\n );\n }\n\n const authConfig = {\n adapter: PrismaAdapter(options.prisma),\n trustHost: options.trustHost ?? parseBoolean(process.env.AUTH_TRUST_HOST, true),\n debug: options.authDebug ?? parseBoolean(process.env.AUTH_DEBUG, false),\n logger: options.authDebug\n ? {\n error(error: Error) {\n console.error(\"[authjs:error]\", error.name, error.message, error.cause ?? \"\");\n },\n warn(code: string) {\n console.warn(\"[authjs:warn]\", code);\n },\n debug(message: string, metadata?: unknown) {\n console.log(\"[authjs:debug]\", message, metadata ?? \"\");\n }\n }\n : undefined,\n session: { strategy: \"database\" as const },\n secret: options.authSecret ?? process.env.AUTH_SECRET,\n cookies: {\n sessionToken: {\n name: options.sessionCookieName,\n options: {\n httpOnly: true,\n sameSite: \"lax\" as const,\n path: \"/\",\n secure: options.sessionCookieSecure\n }\n }\n },\n providers,\n pages: {\n signIn: signInPath\n },\n callbacks: {\n redirect: async ({ url, baseUrl }: { url: string; baseUrl: string }) => {\n const clientOrigin = new URL(options.clientUrl).origin;\n const successUrl = new URL(authenticatedRedirectPath, clientOrigin).toString();\n\n const shouldForceChat = (pathname: string, searchParams: URLSearchParams): boolean => {\n if (pathname !== \"/\" && pathname !== signInPath) {\n return false;\n }\n return !searchParams.has(\"error\");\n };\n\n if (url.startsWith(\"/\")) {\n const relative = new URL(url, clientOrigin);\n if (shouldForceChat(relative.pathname, relative.searchParams)) {\n return successUrl;\n }\n return `${clientOrigin}${relative.pathname}${relative.search}${relative.hash}`;\n }\n\n try {\n const target = new URL(url);\n const base = new URL(baseUrl);\n\n if (target.origin === clientOrigin) {\n if (shouldForceChat(target.pathname, target.searchParams)) {\n return successUrl;\n }\n return target.toString();\n }\n\n if (target.origin === base.origin) {\n if (shouldForceChat(target.pathname, target.searchParams)) {\n return successUrl;\n }\n return `${clientOrigin}${target.pathname}${target.search}${target.hash}`;\n }\n } catch {\n return successUrl;\n }\n\n return successUrl;\n }\n }\n };\n\n const authHandler = ExpressAuth(authConfig);\n\n const requireSession: RequestHandler = async (req, res, next) => {\n const token = extractSessionToken(req.headers.cookie);\n const authDebug = options.authDebug ?? parseBoolean(process.env.AUTH_DEBUG, false);\n\n if (!token) {\n const payload: { error: string; reason?: string } = { error: \"Unauthorized\" };\n if (authDebug) {\n payload.reason = \"missing_session_cookie\";\n }\n return res.status(401).json(payload);\n }\n\n const session = await options.prisma.session.findUnique({\n where: { sessionToken: token },\n include: {\n user: {\n select: options.sessionUserSelect\n }\n }\n });\n\n if (!session || session.expires <= new Date()) {\n const payload: { error: string; reason?: string } = { error: \"Unauthorized\" };\n if (authDebug) {\n payload.reason = !session ? \"session_not_found\" : \"session_expired\";\n }\n return res.status(401).json(payload);\n }\n\n const authUser = options.mapSessionUser(session.user);\n (req as Request & { authUser?: TAuthUser }).authUser = authUser;\n await options.onSessionValidated?.(authUser);\n next();\n };\n\n const extractSessionToken = (cookieHeader: string | undefined): string | null => {\n if (!cookieHeader) {\n return null;\n }\n\n const cookies = cookieHeader\n .split(\";\")\n .map((part) => part.trim())\n .map((part) => {\n const index = part.indexOf(\"=\");\n if (index < 0) {\n return null;\n }\n return [part.slice(0, index), decodeURIComponent(part.slice(index + 1))] as const;\n })\n .filter((entry): entry is readonly [string, string] => entry !== null);\n\n const possibleNames = [\n options.sessionCookieName,\n ...(options.extraSessionCookieNames ?? []),\n \"__Secure-authjs.session-token\",\n \"authjs.session-token\",\n \"__Secure-next-auth.session-token\",\n \"next-auth.session-token\"\n ];\n\n for (const name of possibleNames) {\n const exact = cookies.find(([cookieName]) => cookieName === name);\n if (exact) {\n return exact[1];\n }\n\n const chunks = cookies\n .filter(([cookieName]) => cookieName.startsWith(`${name}.`))\n .map(([cookieName, value]) => {\n const suffix = cookieName.slice(name.length + 1);\n return [Number.parseInt(suffix, 10), value] as const;\n })\n .filter(([index]) => Number.isInteger(index))\n .sort((left, right) => left[0] - right[0]);\n\n if (chunks.length > 0) {\n return chunks.map(([, value]) => value).join(\"\");\n }\n }\n\n return null;\n };\n\n return {\n authConfig,\n authHandler,\n requireSession,\n extractSessionToken,\n googleAuthEnabled,\n slackAuthEnabled\n };\n}\n","import { createHash, randomBytes } from \"node:crypto\";\n\ntype AccountInviteContext = {\n verificationToken: { identifier: string; expires: Date };\n user: {\n id: string;\n email: string | null;\n name: string | null;\n passwordHash: string | null;\n emailVerified: Date | null;\n accounts: Array<{ id: string }>;\n };\n};\n\ntype AccountInviteOptions = {\n prisma: any;\n tokenTtlMs?: number;\n identifierPrefix?: string;\n};\n\nconst DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX = \"account-invite:\";\nconst DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000;\n\nfunction hashAccountInviteToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n}\n\nfunction buildAccountInviteIdentifier(prefix: string, userId: string): string {\n return `${prefix}${userId}`;\n}\n\nexport async function createAccountInviteToken(\n options: AccountInviteOptions & { userId: string }\n): Promise<{ token: string; expiresAt: Date }> {\n const identifierPrefix = options.identifierPrefix ?? DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX;\n const identifier = buildAccountInviteIdentifier(identifierPrefix, options.userId);\n const token = randomBytes(32).toString(\"hex\");\n const expiresAt = new Date(Date.now() + (options.tokenTtlMs ?? DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS));\n\n await options.prisma.verificationToken.deleteMany({\n where: {\n OR: [{ identifier }, { expires: { lt: new Date() } }]\n }\n });\n\n await options.prisma.verificationToken.create({\n data: {\n identifier,\n token: hashAccountInviteToken(token),\n expires: expiresAt\n }\n });\n\n return { token, expiresAt };\n}\n\nexport async function getAccountInviteContext(\n options: AccountInviteOptions & { token: string }\n): Promise {\n const identifierPrefix = options.identifierPrefix ?? DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX;\n const verificationToken = await options.prisma.verificationToken.findUnique({\n where: { token: hashAccountInviteToken(options.token) },\n select: { identifier: true, expires: true }\n });\n\n if (\n !verificationToken ||\n verificationToken.expires <= new Date() ||\n !verificationToken.identifier.startsWith(identifierPrefix)\n ) {\n return null;\n }\n\n const userId = verificationToken.identifier.slice(identifierPrefix.length);\n if (!userId) {\n return null;\n }\n\n const user = await options.prisma.user.findUnique({\n where: { id: userId },\n select: {\n id: true,\n email: true,\n name: true,\n passwordHash: true,\n emailVerified: true,\n accounts: {\n select: { id: true },\n take: 1\n }\n }\n });\n\n if (!user?.email) {\n return null;\n }\n\n return {\n verificationToken,\n user\n };\n}\n\nexport {\n DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX,\n DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS,\n buildAccountInviteIdentifier,\n hashAccountInviteToken\n};\n","import { createHash, randomBytes } from \"node:crypto\";\nimport type { Express, RequestHandler } from \"express\";\nimport { z } from \"zod\";\nimport { DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX, getAccountInviteContext } from \"./invites.js\";\n\ntype RegisterAuthApiRoutesOptions = {\n app: Express;\n prisma: any;\n authHandler: RequestHandler;\n requireSession: RequestHandler;\n extractSessionToken: (cookieHeader: string | undefined) => string | null;\n providersAvailability: Record;\n sessionCookieName: string;\n sessionCookieSecure: boolean;\n extraCookieNamesToClear?: string[];\n messages?: Partial;\n authBasePath?: string;\n authApiBasePath?: string;\n mePath?: string;\n normalizeEmail?: (email: string) => string;\n passwordHasher?: (password: string) => Promise;\n passwordComparator?: (password: string, passwordHash: string) => Promise;\n passwordReset?: {\n enabled: boolean;\n tokenTtlMs?: number;\n identifierPrefix?: string;\n buildResetUrl: (token: string) => string;\n sendMessage: (input: {\n user: { id: string; email: string; name: string | null; passwordHash: string | null };\n resetUrl: string;\n isPasswordCreation: boolean;\n expiresAt: Date;\n }) => Promise;\n };\n accountInvite?: {\n enabled: boolean;\n identifierPrefix?: string;\n };\n onUserRegistered?: (user: { id: string; email: string | null; name: string | null }) => Promise | void;\n onPasswordResetConfirmed?: (user: { id: string; email: string | null; name: string | null }) => Promise | void;\n};\n\ntype AuthRouteMessages = {\n invalidPayload: string;\n emailAlreadyUsed: string;\n accountNotFound: string;\n externalAccountOnly: string;\n invalidPassword: string;\n passwordResetUnavailable: string;\n invalidResetLink: string;\n expiredResetLink: string;\n invalidInviteLink: string;\n inviteAlreadyAccepted: string;\n};\n\nconst defaultNormalizeEmail = (email: string) => email.trim();\nconst defaultPasswordResetIdentifierPrefix = \"password-reset:\";\nconst defaultMessages: AuthRouteMessages = {\n invalidPayload: \"Invalid payload\",\n emailAlreadyUsed: \"Email already used\",\n accountNotFound: \"Account not found\",\n externalAccountOnly: \"This account uses an external sign-in provider.\",\n invalidPassword: \"Invalid password\",\n passwordResetUnavailable: \"Email service is not configured.\",\n invalidResetLink: \"Invalid reset link\",\n expiredResetLink: \"Invalid or expired reset link\",\n invalidInviteLink: \"Invalid or expired invite link\",\n inviteAlreadyAccepted: \"This invite has already been accepted\"\n};\n\nfunction hashPasswordResetToken(token: string): string {\n return createHash(\"sha256\").update(token).digest(\"hex\");\n}\n\nfunction buildPasswordResetIdentifier(prefix: string, userId: string): string {\n return `${prefix}${userId}`;\n}\n\nexport function registerAuthApiRoutes(options: RegisterAuthApiRoutesOptions): void {\n const authBasePath = options.authBasePath ?? \"/auth\";\n const authApiBasePath = options.authApiBasePath ?? \"/api/auth\";\n const mePath = options.mePath ?? \"/api/me\";\n const normalizeEmail = options.normalizeEmail ?? defaultNormalizeEmail;\n const passwordHasher = options.passwordHasher ?? ((password: string) => Promise.resolve(password));\n const passwordComparator = options.passwordComparator ?? ((password: string, hash: string) => Promise.resolve(password === hash));\n const passwordResetIdentifierPrefix = options.passwordReset?.identifierPrefix ?? defaultPasswordResetIdentifierPrefix;\n const accountInviteIdentifierPrefix = options.accountInvite?.identifierPrefix ?? DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX;\n const messages = { ...defaultMessages, ...(options.messages ?? {}) };\n\n const buildSession = (userId: string) => ({\n sessionToken: randomBytes(32).toString(\"hex\"),\n userId,\n expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)\n });\n\n const applySessionCookie = (res: any, session: { sessionToken: string; expires: Date }) => {\n res.cookie(options.sessionCookieName, session.sessionToken, {\n httpOnly: true,\n sameSite: \"lax\",\n secure: options.sessionCookieSecure,\n path: \"/\",\n expires: session.expires\n });\n };\n\n const findUserByEmail = async (email: string) => {\n const normalized = normalizeEmail(email);\n const lowered = normalized.toLowerCase();\n\n return options.prisma.user.findFirst({\n where: {\n OR: lowered === normalized ? [{ email: normalized }] : [{ email: normalized }, { email: lowered }]\n },\n select: {\n id: true,\n email: true,\n name: true,\n image: true,\n passwordHash: true,\n emailVerified: true\n }\n });\n };\n\n const getPasswordResetContext = async (\n rawToken: string\n ): Promise<\n | {\n verificationToken: { identifier: string; expires: Date };\n user: { id: string; email: string | null; name: string | null; passwordHash: string | null; emailVerified: Date | null };\n }\n | null\n > => {\n const verificationToken = await options.prisma.verificationToken.findUnique({\n where: { token: hashPasswordResetToken(rawToken) },\n select: { identifier: true, expires: true }\n });\n\n if (\n !verificationToken ||\n verificationToken.expires <= new Date() ||\n !verificationToken.identifier.startsWith(passwordResetIdentifierPrefix)\n ) {\n return null;\n }\n\n const userId = verificationToken.identifier.slice(passwordResetIdentifierPrefix.length);\n if (!userId) {\n return null;\n }\n\n const user = await options.prisma.user.findUnique({\n where: { id: userId },\n select: {\n id: true,\n email: true,\n name: true,\n passwordHash: true,\n emailVerified: true\n }\n });\n\n if (!user?.email) {\n return null;\n }\n\n return { verificationToken, user };\n };\n\n options.app.use(authBasePath, options.authHandler);\n\n options.app.get(`${authApiBasePath}/providers`, (_req, res) => {\n res.json(options.providersAvailability);\n });\n\n options.app.post(`${authApiBasePath}/register`, async (req, res) => {\n const parsed = z\n .object({\n name: z.string().min(2).max(60),\n email: z.string().email(),\n password: z.string().min(8)\n })\n .safeParse(req.body);\n\n if (!parsed.success) {\n return res.status(400).json({ error: messages.invalidPayload });\n }\n\n const email = normalizeEmail(parsed.data.email);\n const exists = await findUserByEmail(email);\n if (exists) {\n return res.status(409).json({ error: messages.emailAlreadyUsed });\n }\n\n const passwordHash = await passwordHasher(parsed.data.password);\n const created = await options.prisma.user.create({\n data: {\n name: parsed.data.name,\n email,\n passwordHash\n },\n select: { id: true, email: true, name: true }\n });\n\n await options.onUserRegistered?.(created);\n return res.status(201).json(created);\n });\n\n options.app.post(`${authApiBasePath}/login`, async (req, res) => {\n const parsed = z\n .object({\n email: z.string().email(),\n password: z.string().min(8)\n })\n .safeParse(req.body);\n\n if (!parsed.success) {\n return res.status(400).json({ error: messages.invalidPayload });\n }\n\n const email = normalizeEmail(parsed.data.email);\n const user = await findUserByEmail(email);\n if (!user) {\n return res.status(404).json({ error: messages.accountNotFound });\n }\n\n if (!user.passwordHash) {\n return res.status(400).json({ error: messages.externalAccountOnly });\n }\n\n const valid = await passwordComparator(parsed.data.password, user.passwordHash);\n if (!valid) {\n return res.status(401).json({ error: messages.invalidPassword });\n }\n\n const session = buildSession(user.id);\n await options.prisma.session.create({\n data: session\n });\n applySessionCookie(res, session);\n\n return res.status(200).json({ ok: true });\n });\n\n options.app.post(`${authApiBasePath}/password-reset/request`, async (req, res) => {\n const parsed = z\n .object({\n email: z.string().email()\n })\n .safeParse(req.body);\n\n if (!parsed.success) {\n return res.status(400).json({ error: messages.invalidPayload });\n }\n\n if (!options.passwordReset?.enabled) {\n return res.status(503).json({ error: messages.passwordResetUnavailable });\n }\n\n const email = normalizeEmail(parsed.data.email);\n const user = await findUserByEmail(email);\n if (!user?.email) {\n return res.status(200).json({ ok: true });\n }\n\n const rawToken = randomBytes(32).toString(\"hex\");\n const identifier = buildPasswordResetIdentifier(passwordResetIdentifierPrefix, user.id);\n const expiresAt = new Date(Date.now() + (options.passwordReset.tokenTtlMs ?? 2 * 60 * 60 * 1000));\n const resetUrl = options.passwordReset.buildResetUrl(rawToken);\n const isPasswordCreation = !user.passwordHash;\n\n await options.prisma.verificationToken.deleteMany({\n where: {\n OR: [{ identifier }, { expires: { lt: new Date() } }]\n }\n });\n\n await options.prisma.verificationToken.create({\n data: {\n identifier,\n token: hashPasswordResetToken(rawToken),\n expires: expiresAt\n }\n });\n\n await options.passwordReset.sendMessage({\n user: {\n id: user.id,\n email: user.email,\n name: user.name,\n passwordHash: user.passwordHash\n },\n resetUrl,\n isPasswordCreation,\n expiresAt\n });\n\n return res.status(200).json({ ok: true });\n });\n\n options.app.get(`${authApiBasePath}/password-reset/validate`, async (req, res) => {\n if (!options.passwordReset?.enabled) {\n return res.status(400).json({ error: messages.expiredResetLink });\n }\n\n const parsed = z.object({ token: z.string().min(1) }).safeParse({\n token: Array.isArray(req.query.token) ? req.query.token[0] : req.query.token\n });\n\n if (!parsed.success) {\n return res.status(400).json({ error: messages.invalidResetLink });\n }\n\n const context = await getPasswordResetContext(parsed.data.token);\n if (!context) {\n return res.status(400).json({ error: messages.expiredResetLink });\n }\n\n return res.status(200).json({\n ok: true,\n email: context.user.email,\n mode: context.user.passwordHash ? \"reset\" : \"create\"\n });\n });\n\n options.app.post(`${authApiBasePath}/password-reset/confirm`, async (req, res) => {\n if (!options.passwordReset?.enabled) {\n return res.status(400).json({ error: messages.expiredResetLink });\n }\n\n const parsed = z\n .object({\n token: z.string().min(1),\n password: z.string().min(8)\n })\n .safeParse(req.body);\n\n if (!parsed.success) {\n return res.status(400).json({ error: messages.invalidPayload });\n }\n\n const context = await getPasswordResetContext(parsed.data.token);\n if (!context) {\n return res.status(400).json({ error: messages.expiredResetLink });\n }\n\n const passwordHash = await passwordHasher(parsed.data.password);\n await options.prisma.$transaction([\n options.prisma.verificationToken.deleteMany({\n where: { identifier: context.verificationToken.identifier }\n }),\n options.prisma.session.deleteMany({\n where: { userId: context.user.id }\n }),\n options.prisma.user.update({\n where: { id: context.user.id },\n data: {\n passwordHash,\n emailVerified: context.user.emailVerified ?? new Date()\n }\n })\n ]);\n\n await options.onPasswordResetConfirmed?.(context.user);\n return res.status(200).json({ ok: true });\n });\n\n options.app.get(`${authApiBasePath}/invite/validate`, async (req, res) => {\n if (!options.accountInvite?.enabled) {\n return res.status(404).json({ error: messages.invalidInviteLink });\n }\n\n const parsed = z.object({ token: z.string().min(1) }).safeParse({\n token: Array.isArray(req.query.token) ? req.query.token[0] : req.query.token\n });\n\n if (!parsed.success) {\n return res.status(400).json({ error: messages.invalidInviteLink });\n }\n\n const context = await getAccountInviteContext({\n prisma: options.prisma,\n token: parsed.data.token,\n identifierPrefix: accountInviteIdentifierPrefix\n });\n\n if (!context) {\n return res.status(400).json({ error: messages.invalidInviteLink });\n }\n\n if (context.user.passwordHash || context.user.accounts.length > 0) {\n return res.status(409).json({ error: messages.inviteAlreadyAccepted });\n }\n\n return res.status(200).json({\n ok: true,\n email: context.user.email,\n name: context.user.name\n });\n });\n\n options.app.post(`${authApiBasePath}/invite/accept`, async (req, res) => {\n if (!options.accountInvite?.enabled) {\n return res.status(404).json({ error: messages.invalidInviteLink });\n }\n\n const parsed = z\n .object({\n token: z.string().min(1),\n name: z.string().min(2).max(60),\n password: z.string().min(8)\n })\n .safeParse(req.body);\n\n if (!parsed.success) {\n return res.status(400).json({ error: messages.invalidPayload });\n }\n\n const context = await getAccountInviteContext({\n prisma: options.prisma,\n token: parsed.data.token,\n identifierPrefix: accountInviteIdentifierPrefix\n });\n\n if (!context) {\n return res.status(400).json({ error: messages.invalidInviteLink });\n }\n\n if (context.user.passwordHash || context.user.accounts.length > 0) {\n return res.status(409).json({ error: messages.inviteAlreadyAccepted });\n }\n\n const passwordHash = await passwordHasher(parsed.data.password);\n const session = buildSession(context.user.id);\n\n await options.prisma.$transaction([\n options.prisma.verificationToken.deleteMany({\n where: { identifier: context.verificationToken.identifier }\n }),\n options.prisma.session.create({\n data: session\n }),\n options.prisma.user.update({\n where: { id: context.user.id },\n data: {\n name: parsed.data.name,\n passwordHash,\n emailVerified: context.user.emailVerified ?? new Date()\n }\n })\n ]);\n\n applySessionCookie(res, session);\n return res.status(200).json({ ok: true });\n });\n\n options.app.post(`${authApiBasePath}/logout`, async (req, res) => {\n const token = options.extractSessionToken(req.headers.cookie);\n\n if (token) {\n await options.prisma.session.deleteMany({ where: { sessionToken: token } });\n }\n\n const cookieNamesToClear = [\n options.sessionCookieName,\n ...(options.extraCookieNamesToClear ?? []),\n \"authjs.session-token\",\n \"__Secure-authjs.session-token\",\n \"next-auth.session-token\",\n \"__Secure-next-auth.session-token\"\n ];\n\n for (const cookieName of cookieNamesToClear) {\n res.clearCookie(cookieName, { path: \"/\" });\n }\n\n return res.status(200).json({ ok: true });\n });\n\n options.app.get(mePath, options.requireSession, async (req, res) => {\n res.json({ user: (req as { authUser?: unknown }).authUser });\n });\n}\n"],"mappings":"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","names":["createHash","randomBytes","z","createHash","randomBytes","z"]} \ No newline at end of file diff --git a/react/InviteAcceptForm.tsx b/react/InviteAcceptForm.tsx new file mode 100644 index 0000000..bd70714 --- /dev/null +++ b/react/InviteAcceptForm.tsx @@ -0,0 +1,109 @@ +import { useEffect, useState, type ChangeEvent, type FormEvent } from "react"; +import { Alert, AlertDescription, AlertIcon, Button, FormControl, FormLabel, Input, Spinner, Stack, Text } from "./chakra-compat"; +import { FcGoogle } from "react-icons/fc"; +import type { AccountInviteTokenState } from "./types"; + +type InviteAcceptFormTexts = { + loadingLabel: string; + invalidLinkLabel: string; + emailLabel: string; + nameLabel: string; + passwordLabel: string; + passwordConfirmLabel: string; + submitLabel: string; + googleLabel: string; +}; + +type InviteAcceptFormProps = { + texts: InviteAcceptFormTexts; + tokenState: AccountInviteTokenState; + loading?: boolean; + initialName?: string | null; + showGoogleSignIn?: boolean; + googleLoading?: boolean; + onSubmit: (values: { name: string; password: string; passwordConfirm: string }) => void | Promise; + onGoogleSignIn?: () => void | Promise; +}; + +export function InviteAcceptForm({ + texts, + tokenState, + loading = false, + initialName = null, + showGoogleSignIn = false, + googleLoading = false, + onSubmit, + onGoogleSignIn +}: InviteAcceptFormProps) { + const [name, setName] = useState(initialName ?? ""); + + useEffect(() => { + setName(initialName ?? ""); + }, [initialName, tokenState.status]); + + function handleSubmit(event: FormEvent) { + event.preventDefault(); + const form = new FormData(event.currentTarget); + void onSubmit({ + name: String(form.get("name") ?? ""), + password: String(form.get("password") ?? ""), + passwordConfirm: String(form.get("passwordConfirm") ?? "") + }); + } + + if (tokenState.status === "loading") { + return ( + + + {texts.loadingLabel} + + ); + } + + if (tokenState.status === "invalid") { + return ( + + + {tokenState.error || texts.invalidLinkLabel} + + ); + } + + return ( + + + {texts.emailLabel} + + + +
+ + + {texts.nameLabel} + ) => setName(event.target.value)} /> + + + + {texts.passwordLabel} + + + + + {texts.passwordConfirmLabel} + + + + + +
+ + {showGoogleSignIn && onGoogleSignIn ? ( + + ) : null} +
+ ); +} diff --git a/react/client.ts b/react/client.ts index 6bc0828..d1c2511 100644 --- a/react/client.ts +++ b/react/client.ts @@ -23,6 +23,12 @@ type PasswordResetValidationPayload = { error?: string; }; +type AccountInviteValidationPayload = { + email?: string; + name?: string | null; + error?: string; +}; + type JsonErrorPayload = { error?: string; }; @@ -131,6 +137,30 @@ export function createAuthClient(options: CreateAuthClientOptions) { } }, + async validateAccountInviteToken(token: string): Promise<{ email: string; name: string | null }> { + const response = await request(`/api/auth/invite/validate?token=${encodeURIComponent(token)}`, { + headers: {} + }); + const payload = (await response.json().catch(() => null)) as AccountInviteValidationPayload | null; + if (!response.ok || !payload?.email) { + throw new Error(payload?.error ?? "Invalid invite link"); + } + return { + email: payload.email, + name: payload.name ?? null + }; + }, + + async acceptAccountInvite(input: { token: string; name: string; password: string }): Promise { + const response = await request("/api/auth/invite/accept", { + method: "POST", + body: JSON.stringify(input) + }); + if (!response.ok) { + throw await readJsonError(response, "Invalid invite link"); + } + }, + async logout(): Promise { const response = await request("/api/auth/logout", { method: "POST" diff --git a/react/index.ts b/react/index.ts index 499405d..08dbdf8 100644 --- a/react/index.ts +++ b/react/index.ts @@ -1,8 +1,10 @@ export { AuthGuard } from "./AuthGuard"; export { createAuthClient } from "./client"; +export { InviteAcceptForm } from "./InviteAcceptForm"; export { LoginForm } from "./LoginForm"; export { PasswordResetConfirmForm, PasswordResetRequestForm } from "./PasswordResetForms"; export type { + AccountInviteTokenState, AuthProviderAvailability, AuthProviderKey, AuthSubmitValues, diff --git a/react/types.ts b/react/types.ts index 790a1d8..96e3386 100644 --- a/react/types.ts +++ b/react/types.ts @@ -17,3 +17,8 @@ export type PasswordResetTokenState = | { status: "loading" } | { status: "invalid"; error: string } | { status: "valid"; email: string; mode: PasswordResetMode }; + +export type AccountInviteTokenState = + | { status: "loading" } + | { status: "invalid"; error: string } + | { status: "valid"; email: string; name: string | null }; diff --git a/server/index.ts b/server/index.ts index c319e7b..99a6d6c 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,2 +1,3 @@ export { createAuthModule } from "./module.js"; +export { createAccountInviteToken } from "./invites.js"; export { registerAuthApiRoutes } from "./routes.js"; diff --git a/server/invites.ts b/server/invites.ts new file mode 100644 index 0000000..820c0f8 --- /dev/null +++ b/server/invites.ts @@ -0,0 +1,109 @@ +import { createHash, randomBytes } from "node:crypto"; + +type AccountInviteContext = { + verificationToken: { identifier: string; expires: Date }; + user: { + id: string; + email: string | null; + name: string | null; + passwordHash: string | null; + emailVerified: Date | null; + accounts: Array<{ id: string }>; + }; +}; + +type AccountInviteOptions = { + prisma: any; + tokenTtlMs?: number; + identifierPrefix?: string; +}; + +const DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX = "account-invite:"; +const DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000; + +function hashAccountInviteToken(token: string): string { + return createHash("sha256").update(token).digest("hex"); +} + +function buildAccountInviteIdentifier(prefix: string, userId: string): string { + return `${prefix}${userId}`; +} + +export async function createAccountInviteToken( + options: AccountInviteOptions & { userId: string } +): Promise<{ token: string; expiresAt: Date }> { + const identifierPrefix = options.identifierPrefix ?? DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX; + const identifier = buildAccountInviteIdentifier(identifierPrefix, options.userId); + const token = randomBytes(32).toString("hex"); + const expiresAt = new Date(Date.now() + (options.tokenTtlMs ?? DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS)); + + await options.prisma.verificationToken.deleteMany({ + where: { + OR: [{ identifier }, { expires: { lt: new Date() } }] + } + }); + + await options.prisma.verificationToken.create({ + data: { + identifier, + token: hashAccountInviteToken(token), + expires: expiresAt + } + }); + + return { token, expiresAt }; +} + +export async function getAccountInviteContext( + options: AccountInviteOptions & { token: string } +): Promise { + const identifierPrefix = options.identifierPrefix ?? DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX; + const verificationToken = await options.prisma.verificationToken.findUnique({ + where: { token: hashAccountInviteToken(options.token) }, + select: { identifier: true, expires: true } + }); + + if ( + !verificationToken || + verificationToken.expires <= new Date() || + !verificationToken.identifier.startsWith(identifierPrefix) + ) { + return null; + } + + const userId = verificationToken.identifier.slice(identifierPrefix.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, + accounts: { + select: { id: true }, + take: 1 + } + } + }); + + if (!user?.email) { + return null; + } + + return { + verificationToken, + user + }; +} + +export { + DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX, + DEFAULT_ACCOUNT_INVITE_TOKEN_TTL_MS, + buildAccountInviteIdentifier, + hashAccountInviteToken +}; diff --git a/server/routes.ts b/server/routes.ts index ec5261c..693aaf4 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1,6 +1,7 @@ import { createHash, randomBytes } from "node:crypto"; import type { Express, RequestHandler } from "express"; import { z } from "zod"; +import { DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX, getAccountInviteContext } from "./invites.js"; type RegisterAuthApiRoutesOptions = { app: Express; @@ -31,6 +32,10 @@ type RegisterAuthApiRoutesOptions = { expiresAt: Date; }) => Promise; }; + accountInvite?: { + enabled: boolean; + identifierPrefix?: string; + }; onUserRegistered?: (user: { id: string; email: string | null; name: string | null }) => Promise | void; onPasswordResetConfirmed?: (user: { id: string; email: string | null; name: string | null }) => Promise | void; }; @@ -44,6 +49,8 @@ type AuthRouteMessages = { passwordResetUnavailable: string; invalidResetLink: string; expiredResetLink: string; + invalidInviteLink: string; + inviteAlreadyAccepted: string; }; const defaultNormalizeEmail = (email: string) => email.trim(); @@ -56,7 +63,9 @@ const defaultMessages: AuthRouteMessages = { invalidPassword: "Invalid password", passwordResetUnavailable: "Email service is not configured.", invalidResetLink: "Invalid reset link", - expiredResetLink: "Invalid or expired reset link" + expiredResetLink: "Invalid or expired reset link", + invalidInviteLink: "Invalid or expired invite link", + inviteAlreadyAccepted: "This invite has already been accepted" }; function hashPasswordResetToken(token: string): string { @@ -75,8 +84,25 @@ export function registerAuthApiRoutes(options: RegisterAuthApiRoutesOptions): vo 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 accountInviteIdentifierPrefix = options.accountInvite?.identifierPrefix ?? DEFAULT_ACCOUNT_INVITE_IDENTIFIER_PREFIX; const messages = { ...defaultMessages, ...(options.messages ?? {}) }; + const buildSession = (userId: string) => ({ + sessionToken: randomBytes(32).toString("hex"), + userId, + expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000) + }); + + const applySessionCookie = (res: any, session: { sessionToken: string; expires: Date }) => { + res.cookie(options.sessionCookieName, session.sessionToken, { + httpOnly: true, + sameSite: "lax", + secure: options.sessionCookieSecure, + path: "/", + expires: session.expires + }); + }; + const findUserByEmail = async (email: string) => { const normalized = normalizeEmail(email); const lowered = normalized.toLowerCase(); @@ -207,24 +233,11 @@ export function registerAuthApiRoutes(options: RegisterAuthApiRoutesOptions): vo 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); - + const session = buildSession(user.id); 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 + data: session }); + applySessionCookie(res, session); return res.status(200).json({ ok: true }); }); @@ -352,6 +365,95 @@ export function registerAuthApiRoutes(options: RegisterAuthApiRoutesOptions): vo return res.status(200).json({ ok: true }); }); + options.app.get(`${authApiBasePath}/invite/validate`, async (req, res) => { + if (!options.accountInvite?.enabled) { + return res.status(404).json({ error: messages.invalidInviteLink }); + } + + 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.invalidInviteLink }); + } + + const context = await getAccountInviteContext({ + prisma: options.prisma, + token: parsed.data.token, + identifierPrefix: accountInviteIdentifierPrefix + }); + + if (!context) { + return res.status(400).json({ error: messages.invalidInviteLink }); + } + + if (context.user.passwordHash || context.user.accounts.length > 0) { + return res.status(409).json({ error: messages.inviteAlreadyAccepted }); + } + + return res.status(200).json({ + ok: true, + email: context.user.email, + name: context.user.name + }); + }); + + options.app.post(`${authApiBasePath}/invite/accept`, async (req, res) => { + if (!options.accountInvite?.enabled) { + return res.status(404).json({ error: messages.invalidInviteLink }); + } + + const parsed = z + .object({ + token: z.string().min(1), + name: z.string().min(2).max(60), + password: z.string().min(8) + }) + .safeParse(req.body); + + if (!parsed.success) { + return res.status(400).json({ error: messages.invalidPayload }); + } + + const context = await getAccountInviteContext({ + prisma: options.prisma, + token: parsed.data.token, + identifierPrefix: accountInviteIdentifierPrefix + }); + + if (!context) { + return res.status(400).json({ error: messages.invalidInviteLink }); + } + + if (context.user.passwordHash || context.user.accounts.length > 0) { + return res.status(409).json({ error: messages.inviteAlreadyAccepted }); + } + + const passwordHash = await passwordHasher(parsed.data.password); + const session = buildSession(context.user.id); + + await options.prisma.$transaction([ + options.prisma.verificationToken.deleteMany({ + where: { identifier: context.verificationToken.identifier } + }), + options.prisma.session.create({ + data: session + }), + options.prisma.user.update({ + where: { id: context.user.id }, + data: { + name: parsed.data.name, + passwordHash, + emailVerified: context.user.emailVerified ?? new Date() + } + }) + ]); + + applySessionCookie(res, session); + return res.status(200).json({ ok: true }); + }); + options.app.post(`${authApiBasePath}/logout`, async (req, res) => { const token = options.extractSessionToken(req.headers.cookie);