Files
Epicure/apps/web/components/admin/signups-toggle.tsx
T
Arnaud e0e1ac49d9 feat: signup toggle, invite links, admin-created users
- New invites table: token-gated signup, optional email lock,
  role/tier override, single-use, expiry.
- SIGNUPS_DISABLED site setting toggle at /admin/settings.
- databaseHooks.user.create gate in auth/server.ts blocks new account
  creation (email + Google OAuth) when disabled unless a valid invite
  cookie is present; applies invite role/tier and marks it consumed.
- /admin/invites: create/list/revoke shareable invite links.
- /admin/users: "Create user" dialog — admin sets email/role/tier,
  account is pre-verified, user gets a set-password email (admin
  never sees a password).
- Signup page reads ?invite=, validates via public
  /api/v1/invites/[token], locks the form when signups are closed
  and no valid invite is present.
- proxy.ts: allowlist /api/v1/invites/ for anonymous invite checks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 21:36:40 +02:00

56 lines
1.8 KiB
TypeScript

"use client";
import { useState } from "react";
import { toast } from "sonner";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
export function SignupsToggle({ initialDisabled }: { initialDisabled: boolean }) {
const [disabled, setDisabled] = useState(initialDisabled);
const [saving, setSaving] = useState(false);
async function handleChange(checked: boolean) {
setSaving(true);
const previous = disabled;
setDisabled(checked);
try {
const res = await fetch("/api/v1/admin/settings", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ SIGNUPS_DISABLED: checked ? "true" : null }),
});
if (!res.ok) throw new Error("Save failed");
toast.success(checked ? "Signups disabled" : "Signups enabled");
} catch {
setDisabled(previous);
toast.error("Failed to update");
} finally {
setSaving(false);
}
}
return (
<section className="rounded-xl border p-6 space-y-1">
<div className="flex items-center justify-between">
<div>
<h2 className="font-semibold text-lg">Signups</h2>
<p className="text-sm text-muted-foreground mt-1">
When disabled, only people with a valid invite link can create an account.
</p>
</div>
<div className="flex items-center gap-2 shrink-0">
<Label htmlFor="signups-disabled" className="text-sm">
{disabled ? "Disabled" : "Open"}
</Label>
<Switch
id="signups-disabled"
checked={disabled}
disabled={saving}
onCheckedChange={(checked) => { void handleChange(checked); }}
/>
</div>
</div>
</section>
);
}