project-methods-panel.tsx129 lines · main
1'use client';
2
3import Link from 'next/link';
4import { useRouter } from 'next/navigation';
5import { useState } from 'react';
6
7type Chip = {
8 id: string;
9 label: string;
10 kind: 'core' | 'oauth';
11 enabled: boolean;
12 configured: boolean;
13 hrefSuffix: string;
14};
15
16const CORE_FLAG: Record<string, string> = {
17 emailPassword: 'emailPassword',
18 'passwordless-email': 'passwordlessEmail',
19 'magic-link': 'magicLink',
20 'passwordless-sms': 'passwordlessSms',
21 passkeys: 'passkeys',
22 mfa: 'mfa',
23};
24
25/**
26 * Click a core method to toggle it for this project.
27 * Highlighted = on for this project only.
28 */
29export function ProjectMethodsPanel({
30 projectId,
31 coreChips,
32 methods,
33}: {
34 projectId: string;
35 coreChips: Chip[];
36 methods: Record<string, boolean>;
37}) {
38 const router = useRouter();
39 const [pending, setPending] = useState<string | null>(null);
40 const [err, setErr] = useState<string | null>(null);
41 const [local, setLocal] = useState(methods);
42
43 async function toggle(chip: Chip): Promise<void> {
44 const flagKey = CORE_FLAG[chip.id];
45 if (!flagKey) return;
46 const next = !Boolean(local[flagKey] ?? chip.enabled);
47 setPending(chip.id);
48 setErr(null);
49 try {
50 const res = await fetch(
51 `/api/v1/auth-core/projects/${projectId}/methods`,
52 {
53 method: 'PUT',
54 credentials: 'include',
55 headers: { 'content-type': 'application/json' },
56 body: JSON.stringify({ [flagKey]: next }),
57 },
58 );
59 const body = (await res.json().catch(() => ({}))) as {
60 message?: string;
61 methods?: Record<string, boolean>;
62 };
63 if (!res.ok) throw new Error(body.message ?? `http ${res.status}`);
64 if (body.methods) setLocal(body.methods);
65 else setLocal((m) => ({ ...m, [flagKey]: next }));
66 router.refresh();
67 } catch (e) {
68 setErr(e instanceof Error ? e.message : 'could not update method');
69 } finally {
70 setPending(null);
71 }
72 }
73
74 if (coreChips.length === 0) {
75 return (
76 <p className="mt-3 font-mono text-xs text-[var(--color-text-muted)]">
77 methods will show after Auth config loads
78 </p>
79 );
80 }
81
82 return (
83 <div className="mt-3 space-y-2">
84 <ul className="flex flex-wrap gap-2">
85 {coreChips.map((c) => {
86 const flagKey = CORE_FLAG[c.id] ?? c.id;
87 const on = Boolean(local[flagKey] ?? c.enabled);
88 return (
89 <li key={c.id}>
90 <button
91 type="button"
92 disabled={pending === c.id}
93 onClick={() => void toggle(c)}
94 title={
95 on
96 ? 'on for this project — click to turn off'
97 : 'off for this project — click to turn on'
98 }
99 className="rounded border px-2.5 py-1.5 font-mono text-[11px] disabled:opacity-50"
100 style={{
101 borderColor: 'var(--auth-accent-border)',
102 background: on ? 'var(--auth-accent-soft)' : 'transparent',
103 color: 'var(--color-text)',
104 }}
105 >
106 {c.label}
107 {on ? '' : ' · off'}
108 </button>
109 </li>
110 );
111 })}
112 </ul>
113 <p className="font-mono text-[10px] text-[var(--color-text-muted)]">
114 yellow = on for this project only. For phone codes, open{' '}
115 <Link
116 href={`/dashboard/auth/${projectId}/providers?method=passwordlessSms#auth-sms-setup`}
117 className="underline"
118 style={{ color: 'var(--auth-accent, #FFFD74)' }}
119 >
120 Providers → SMS login (Twilio)
121 </Link>
122 .
123 </p>
124 {err ? (
125 <p className="font-mono text-xs text-red-400">{err}</p>
126 ) : null}
127 </div>
128 );
129}