Disambiguates the Phoenix/auth client lib from lib-arcadia-agents-client. Dir lib-arcadia-client → lib-arcadia-core-client; alias updated in tsconfig paths, vite config, app.css @source, imports, CI and docs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
127 lines
3.9 KiB
TypeScript
127 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState, type CSSProperties, type FC, type ReactNode } from "react";
|
|
import { AuthCard, SignInForm, type SignInValues } from "@crema/auth-ui";
|
|
import {
|
|
ArcadiaError,
|
|
useArcadiaClient,
|
|
type ArcadiaAuthTokens,
|
|
type ArcadiaUser,
|
|
} from "@crema/arcadia-core-client";
|
|
|
|
export interface LoginResult {
|
|
tokens: ArcadiaAuthTokens;
|
|
user?: ArcadiaUser;
|
|
/** True if the API responded with a 2FA challenge instead of tokens. */
|
|
twoFactorRequired?: boolean;
|
|
/** Opaque challenge token to pass to the 2FA step (when twoFactorRequired). */
|
|
twoFactorChallenge?: string;
|
|
/** Whether the user asked to be remembered (from the form's checkbox). */
|
|
remember?: boolean;
|
|
}
|
|
|
|
export interface LoginFormProps {
|
|
/** Where to POST. Defaults to "/api/v1/auth/login". */
|
|
loginPath?: string;
|
|
/** Called on successful auth (or 2FA challenge — check twoFactorRequired). */
|
|
onSuccess: (result: LoginResult) => void | Promise<void>;
|
|
/** "Forgot password?" link click. If omitted, the link is hidden. */
|
|
onForgotPassword?: () => void;
|
|
/** "Don't have an account?" link click. Rendered in the card footer. */
|
|
onSignup?: () => void;
|
|
/** Optional brand row above the form (logo, app name). */
|
|
brand?: ReactNode;
|
|
/** Heading text. Default: "Sign in". */
|
|
heading?: string;
|
|
/** Subhead under the heading. */
|
|
subhead?: ReactNode;
|
|
/** Pre-fill email (e.g. from query string after invitation accept). */
|
|
defaultEmail?: string;
|
|
/** Override styles on the outer card. */
|
|
style?: CSSProperties;
|
|
}
|
|
|
|
export const LoginForm: FC<LoginFormProps> = ({
|
|
loginPath = "/api/v1/auth/login",
|
|
onSuccess,
|
|
onForgotPassword,
|
|
onSignup,
|
|
brand,
|
|
heading = "Sign in",
|
|
subhead,
|
|
defaultEmail,
|
|
style,
|
|
}) => {
|
|
const arcadia = useArcadiaClient();
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
async function handleSubmit({ email, password, remember }: SignInValues) {
|
|
setError(null);
|
|
try {
|
|
const res = await arcadia.POST<{
|
|
data: ArcadiaAuthTokens & {
|
|
user?: ArcadiaUser;
|
|
two_factor_required?: boolean;
|
|
two_factor_challenge?: string;
|
|
};
|
|
}>(loginPath, { body: { email, password } });
|
|
const { user, two_factor_required, two_factor_challenge, ...tokens } = res.data;
|
|
await onSuccess({
|
|
tokens,
|
|
user,
|
|
twoFactorRequired: two_factor_required,
|
|
twoFactorChallenge: two_factor_challenge,
|
|
remember,
|
|
});
|
|
} catch (err) {
|
|
// SignInForm catches and clears its own pending state when this throws.
|
|
// We surface the message via local error state below the form.
|
|
if (err instanceof ArcadiaError) setError(err.message);
|
|
else setError("Something went wrong. Please try again.");
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
return (
|
|
<AuthCard
|
|
logo={brand}
|
|
title={heading}
|
|
description={subhead}
|
|
footer={
|
|
onSignup ? (
|
|
<span className="text-sm text-muted-foreground">
|
|
Don't have an account?{" "}
|
|
<button
|
|
type="button"
|
|
data-action="auth-login-signup"
|
|
onClick={onSignup}
|
|
className="text-primary underline underline-offset-2"
|
|
>
|
|
Sign up
|
|
</button>
|
|
</span>
|
|
) : undefined
|
|
}
|
|
className="auth-arcadia-login"
|
|
// style is forwarded via wrapper; AuthCard takes className not style
|
|
>
|
|
<div data-action="auth-login" style={style}>
|
|
<SignInForm
|
|
onSubmit={handleSubmit}
|
|
onForgot={onForgotPassword}
|
|
defaultEmail={defaultEmail}
|
|
/>
|
|
{error ? (
|
|
<div
|
|
role="alert"
|
|
className="mt-3 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive"
|
|
data-action="auth-login-error"
|
|
>
|
|
{error}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</AuthCard>
|
|
);
|
|
};
|