Better Auth용 React 로그인 UI: 2FA 로그인, 회원가입, 소셜 버튼, 세션 훅
코드파일 6개컨텍스트약 468토큰검사 통과
설치
$
genpm add @yohangel/auth-react포함 내용
- src/lib/auth-react/에 소스 코드, 파일 6개. (10.4kB)
- src/lib/auth-react/AGENTS.md에 AI 규칙, 그리고 IDE 규칙 파일.
README
이 패키지에는 README가 없습니다.
약 468토큰→ src/lib/auth-react/AGENTS.md→ .cursor/rules/genpm-yohangel-auth-react.mdc
이것이 AI가 src/lib/auth-react에서 작업할 때 읽는 내용 그대로입니다. 그 외에는 컨텍스트에 아무것도 추가되지 않습니다.
@yohangel/auth-react — rules for AI agents
Purpose
React client for a server running @yohangel/auth (Next.js, Express, NestJS…): useSession, unstyled accessible forms (sign-in with TOTP second step, sign-up, social buttons) and UI guards. Client-only; it never decides access by itself.
Module map
index.ts— public API.client.ts—authClient(Better Auth React client + 2FA plugin),useSession,signIn,signUp,signOut,AUTH_BASE_URL.forms.tsx—SignInForm,SignUpForm,SocialButtons,defaultLabels/AuthLabels.guard.tsx—SignedIn(render with/without session),SignOutButton.
Integration (do this after installing)
- Same origin as the API (Next.js, or SPA served by the same server): nothing to configure.
- API on another origin: set
AUTH_BASE_URLinclient.tsto the API URL, add this app's origin toAUTH_TRUSTED_ORIGINSon the server, and enable CORS withcredentials: truethere. - Pages:
<SignInForm redirectTo="/dashboard" />,<SignUpForm redirectTo="/welcome" />,<SocialButtons providers={['github']} />(only providers configured on the server). - In Next.js these are Client Components: put
'use client'in the file that renders them. - Translate by passing
labels={{ signIn: '…', password: '…' }}; style viaclassNameand the[data-auth-error]attribute.
Conventions
- Use
useSession()for UI state only (show name, hide buttons). Every protected API/page must check the session on the server. - Keep the password
minLengthin sync with the server (12).
Don't
- Don't store tokens in
localStorage/sessionStorage: the session lives in an HttpOnly cookie managed by Better Auth. - Don't pass absolute URLs from query params to
redirectTo(the components only follow internal paths). - Don't log form values or errors that may contain the password.
# @yohangel/auth-react — rules for AI agents
## Purpose
React client for a server running `@yohangel/auth` (Next.js, Express, NestJS…): `useSession`, unstyled accessible forms (sign-in with TOTP second step, sign-up, social buttons) and UI guards. Client-only; it never decides access by itself.
## Module map
- `index.ts` — public API.
- `client.ts` — `authClient` (Better Auth React client + 2FA plugin), `useSession`, `signIn`, `signUp`, `signOut`, `AUTH_BASE_URL`.
- `forms.tsx` — `SignInForm`, `SignUpForm`, `SocialButtons`, `defaultLabels` / `AuthLabels`.
- `guard.tsx` — `SignedIn` (render with/without session), `SignOutButton`.
## Integration (do this after installing)
1. Same origin as the API (Next.js, or SPA served by the same server): nothing to configure.
2. API on another origin: set `AUTH_BASE_URL` in `client.ts` to the API URL, add this app's origin to `AUTH_TRUSTED_ORIGINS` on the server, and enable CORS with `credentials: true` there.
3. Pages: `<SignInForm redirectTo="/dashboard" />`, `<SignUpForm redirectTo="/welcome" />`, `<SocialButtons providers={['github']} />` (only providers configured on the server).
4. In Next.js these are Client Components: put `'use client'` in the file that renders them.
5. Translate by passing `labels={{ signIn: '…', password: '…' }}`; style via `className` and the `[data-auth-error]` attribute.
## Conventions
- Use `useSession()` for UI state only (show name, hide buttons). Every protected API/page must check the session on the server.
- Keep the password `minLength` in sync with the server (12).
## Don't
- Don't store tokens in `localStorage`/`sessionStorage`: the session lives in an HttpOnly cookie managed by Better Auth.
- Don't pass absolute URLs from query params to `redirectTo` (the components only follow internal paths).
- Don't log form values or errors that may contain the password.
.genpmignore 적용 후 주입될 정확한 트리입니다. 고정 대상:
// Formularios accesibles y sin estilos propios (usa `className`): inicio de sesión (con segundo factor si la
// cuenta lo tiene), registro y botones de proveedores. Los textos se pasan por `labels` para traducirlos.
import { type FormEvent, useState } from 'react';
import { authClient } from './client.js';
export type AuthLabels = {
email: string;
password: string;
name: string;
signIn: string;
signUp: string;
code: string;
verify: string;
continueWith: string;
working: string;
passwordHint: string;
};
export const defaultLabels: AuthLabels = {
email: 'Email',
password: 'Password',
name: 'Name',
signIn: 'Sign in',
signUp: 'Create account',
code: 'Authentication code',
verify: 'Verify',
continueWith: 'Continue with',
working: 'Please wait…',
passwordHint: 'At least 12 characters.',
};
type Common = {
/** Adónde ir al terminar. Solo rutas internas (empiezan por "/"). */
redirectTo?: string;
labels?: Partial<AuthLabels>;
className?: string;
onSuccess?: () => void;
};
const internal = (to: string | undefined) => (to?.startsWith('/') && !to.startsWith('//') ? to : '/');
function useForm(onSuccess: (() => void) | undefined, redirectTo: string | undefined) {
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const run = async (fn: () => Promise<{ error?: { message?: string } | null } | undefined | null>) => {
setBusy(true);
setError(null);
try {
const res = await fn();
if (res?.error) setError(res.error.message ?? 'Error');
else if (onSuccess) onSuccess();
else if (typeof window !== 'undefined') window.location.assign(internal(redirectTo));
} finally {
setBusy(false);
}
};
return { error, busy, run };
}
function ErrorText({ error }: { error: string | null }) {
return error ? (
<p role="alert" data-auth-error="">
{error}
</p>
) : null;
}
export function SignInForm({ redirectTo, labels, className, onSuccess }: Common) {
const L = { ...defaultLabels, ...labels };
const { error, busy, run } = useForm(onSuccess, redirectTo);
const [needsCode, setNeedsCode] = useState(false);
const submit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const f = new FormData(e.currentTarget);
if (needsCode)
return void run(() => authClient.twoFactor.verifyTotp({ code: String(f.get('code') ?? ''), trustDevice: true }));
void run(async () => {
const res = await authClient.signIn.email({
email: String(f.get('email') ?? ''),
password: String(f.get('password') ?? ''),
});
// Cuenta con 2FA: Better Auth no crea la sesión hasta verificar el código.
if ((res.data as { twoFactorRedirect?: boolean } | null)?.twoFactorRedirect) {
setNeedsCode(true);
return { error: null, pending: true } as never;
}
return res;
});
};
return (
<form className={className} onSubmit={submit} noValidate={false}>
{needsCode ? (
<label>
{L.code}
<input name="code" inputMode="numeric" autoComplete="one-time-code" pattern="[0-9]{6}" required />
</label>
) : (
<>
<label>
{L.email}
<input name="email" type="email" autoComplete="email" required />
</label>
<label>
{L.password}
<input name="password" type="password" autoComplete="current-password" required />
</label>
</>
)}
<ErrorText error={error} />
<button type="submit" disabled={busy}>
{busy ? L.working : needsCode ? L.verify : L.signIn}
</button>
</form>
);
}
export function SignUpForm({ redirectTo, labels, className, onSuccess }: Common) {
const L = { ...defaultLabels, ...labels };
const { error, busy, run } = useForm(onSuccess, redirectTo);
const submit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const f = new FormData(e.currentTarget);
void run(() =>
authClient.signUp.email({
name: String(f.get('name') ?? ''),
email: String(f.get('email') ?? ''),
password: String(f.get('password') ?? ''),
}),
);
};
return (
<form className={className} onSubmit={submit}>
<label>
{L.name}
<input name="name" autoComplete="name" required />
</label>
<label>
{L.email}
<input name="email" type="email" autoComplete="email" required />
</label>
<label>
{L.password}
<input
name="password"
type="password"
autoComplete="new-password"
minLength={12}
maxLength={128}
required
aria-describedby="auth-password-hint"
/>
<small id="auth-password-hint">{L.passwordHint}</small>
</label>
<ErrorText error={error} />
<button type="submit" disabled={busy}>
{busy ? L.working : L.signUp}
</button>
</form>
);
}
/** Un botón por proveedor. Solo funcionan los configurados en el servidor (GITHUB_*, GOOGLE_*). */
export function SocialButtons({
providers,
redirectTo,
labels,
className,
}: Common & { providers: Array<'github' | 'google'> }) {
const L = { ...defaultLabels, ...labels };
const names = { github: 'GitHub', google: 'Google' } as const;
return (
<div className={className}>
{providers.map((p) => (
<button
key={p}
type="button"
onClick={() => void authClient.signIn.social({ provider: p, callbackURL: internal(redirectTo) })}
>
{L.continueWith} {names[p]}
</button>
))}
</div>
);
}
이 패키지는 MCP 서버를 선언하지 않습니다.
| 버전 | 커밋 | 게시일 | 검사 |
|---|---|---|---|
| 1.0.0 | 7145b9d | 3시간 전 | ✔ 검사 통과 |
- genpm
- 없음
- 제안됨
- GenPM은 npm 명령을 제안하고, 동의한 경우에만 실행합니다.
- 검사
- 검사 통과 · 문제 0건
- 커밋
- auth-react@1.0.0 → 7145b9d58055b5145085782c958b7577f1d65276 · 가져온 뒤 검증됨
- 스크립트
- 없음. GenPM은 패키지 코드를 절대 실행하지 않습니다.
- 라이선스
- MIT
- 신고
- 문제가 있나요?