feat: household api token auth foundation
This commit is contained in:
@@ -24,6 +24,7 @@ import type {
|
||||
} from "@/modules/_core/themes";
|
||||
import { getCurrentSession } from "@/lib/session";
|
||||
import { revokeShareLink } from "@/modules/_core/share";
|
||||
import { createHouseholdApiToken, revokeHouseholdApiToken } from "@/modules/_core/api-token";
|
||||
|
||||
export interface ThemePatch {
|
||||
palette?: Palette;
|
||||
@@ -88,3 +89,22 @@ export async function revokeShareLinkAction(formData: FormData): Promise<void> {
|
||||
await revokeShareLink(id);
|
||||
revalidatePath("/settings");
|
||||
}
|
||||
|
||||
function requireOwner(role: "owner" | "member") {
|
||||
if (role !== "owner") throw new Error("Only household owners can manage API tokens");
|
||||
}
|
||||
|
||||
export async function createApiTokenAction(): Promise<{ token: string }> {
|
||||
const { household, role, user } = await getCurrentSession();
|
||||
requireOwner(role);
|
||||
const result = await createHouseholdApiToken(household.id, user.id);
|
||||
revalidatePath("/settings");
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function revokeApiTokenAction(): Promise<void> {
|
||||
const { household, role } = await getCurrentSession();
|
||||
requireOwner(role);
|
||||
await revokeHouseholdApiToken(household.id);
|
||||
revalidatePath("/settings");
|
||||
}
|
||||
|
||||
+55
-31
@@ -11,11 +11,13 @@ import { ResponsiveSidebar } from "@/components/settings-section";
|
||||
import type { SectionId } from "@/components/settings-section";
|
||||
import { AvatarFallbackWithName } from "@/components/avatar-fallback";
|
||||
import { revokeShareLinkAction } from "./actions";
|
||||
import { getHouseholdApiTokenStatus } from "@/modules/_core/api-token";
|
||||
import { ApiTokenSettings } from "@/components/api-token-settings";
|
||||
import { listCalendars } from "@/modules/calendar/server/queries";
|
||||
import { listLists } from "@/modules/lists/server/queries";
|
||||
import Link from "next/link";
|
||||
import { NavIcon } from "@/components/nav-icon";
|
||||
import { Mail, Globe, History, Sun, Bell, Pencil, Lock, Plus } from "lucide-react";
|
||||
import { Mail, Globe, History, Sun, Bell, Pencil, Lock, Plus, KeyRound } from "lucide-react";
|
||||
|
||||
const VALID_SECTIONS = new Set<SectionId>([
|
||||
"household",
|
||||
@@ -36,7 +38,7 @@ export default async function SettingsPage({
|
||||
? (sp.s as SectionId)
|
||||
: "household";
|
||||
|
||||
const { user, household } = await getCurrentSession();
|
||||
const { user, household, role } = await getCurrentSession();
|
||||
const ntfyConfigured = !!(process.env["NTFY_URL"] && process.env["NTFY_TOPIC"]);
|
||||
const vapidKey = process.env["VAPID_PUBLIC_KEY"] ?? "";
|
||||
|
||||
@@ -51,7 +53,7 @@ export default async function SettingsPage({
|
||||
)}
|
||||
{section === "calendars" && <CalendarsAndListsSection />}
|
||||
{section === "appearance" && <AppearanceSection user={user} />}
|
||||
{section === "data" && <DataSection />}
|
||||
{section === "data" && <DataSection householdId={household.id} role={role} />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -315,36 +317,58 @@ function AppearanceSection({
|
||||
);
|
||||
}
|
||||
|
||||
function DataSection() {
|
||||
async function DataSection({
|
||||
householdId,
|
||||
role,
|
||||
}: {
|
||||
householdId: string;
|
||||
role: "owner" | "member";
|
||||
}) {
|
||||
const tokenStatus = role === "owner" ? await getHouseholdApiTokenStatus(householdId) : null;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Data & backups</CardTitle>
|
||||
</CardHeader>
|
||||
<div>
|
||||
<div className="set-row">
|
||||
<History className="size-4 text-[var(--ink-soft)]" />
|
||||
<div className="label">
|
||||
<div className="t">Auto-backup</div>
|
||||
<div className="d">Daily 03:00 → /var/backups/famapp/. Configured via host cron.</div>
|
||||
<>
|
||||
{role === "owner" && tokenStatus && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>API access</CardTitle>
|
||||
<KeyRound className="size-4 text-[var(--ink-mute)]" />
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<ApiTokenSettings status={tokenStatus} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Data & backups</CardTitle>
|
||||
</CardHeader>
|
||||
<div>
|
||||
<div className="set-row">
|
||||
<History className="size-4 text-[var(--ink-soft)]" />
|
||||
<div className="label">
|
||||
<div className="t">Auto-backup</div>
|
||||
<div className="d">Daily 03:00 → /var/backups/famapp/. Configured via host cron.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="set-row">
|
||||
<Globe className="size-4 text-[var(--ink-soft)]" />
|
||||
<div className="label">
|
||||
<div className="t">Server</div>
|
||||
<div className="d">Self-hosted via Docker Compose</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="set-row" style={{ borderBottom: "0" }}>
|
||||
<Mail className="size-4 text-[var(--ink-soft)]" />
|
||||
<div className="label">
|
||||
<div className="t">Export</div>
|
||||
<div className="d">Not yet implemented — coming in v0.5</div>
|
||||
</div>
|
||||
<Bell className="size-4 text-[var(--ink-faint)]" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="set-row">
|
||||
<Globe className="size-4 text-[var(--ink-soft)]" />
|
||||
<div className="label">
|
||||
<div className="t">Server</div>
|
||||
<div className="d">Self-hosted via Docker Compose</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="set-row" style={{ borderBottom: "0" }}>
|
||||
<Mail className="size-4 text-[var(--ink-soft)]" />
|
||||
<div className="label">
|
||||
<div className="t">Export</div>
|
||||
<div className="d">Not yet implemented — coming in v0.5</div>
|
||||
</div>
|
||||
<Bell className="size-4 text-[var(--ink-faint)]" />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user