Files
sofa/app/(pages)/settings/_components/actions.ts
T
jakeandClaude Opus 4.6 8487c3d7b1 Convert settings page to server component with server actions
Replace client-side data fetching with server-side queries and server
actions, eliminating 3 API route files. Extract page into granular
client components (account, integrations, server, webhook card) with
optimistic updates. Rename sections: Media Servers → Integrations,
Administration → Server. Add admin badge to account section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 09:32:18 -05:00

141 lines
3.4 KiB
TypeScript

"use server";
import crypto from "node:crypto";
import { and, eq } from "drizzle-orm";
import { headers } from "next/headers";
import { auth } from "@/lib/auth/server";
import { db } from "@/lib/db/client";
import { webhookConnections } from "@/lib/db/schema";
import { setSetting } from "@/lib/services/settings";
async function getSession() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) throw new Error("Unauthorized");
return session;
}
export async function saveWebhookConnection(
provider: "plex" | "jellyfin",
mediaServerUsername: string,
enabled?: boolean,
) {
const session = await getSession();
if (!["plex", "jellyfin"].includes(provider)) {
throw new Error("Invalid provider");
}
if (!mediaServerUsername?.trim()) {
throw new Error("Media server username is required");
}
const existing = await db
.select()
.from(webhookConnections)
.where(
and(
eq(webhookConnections.userId, session.user.id),
eq(webhookConnections.provider, provider),
),
)
.get();
if (existing) {
const connection = await db
.update(webhookConnections)
.set({
mediaServerUsername: mediaServerUsername.trim(),
enabled: typeof enabled === "boolean" ? enabled : existing.enabled,
})
.where(eq(webhookConnections.id, existing.id))
.returning()
.get();
return {
...connection,
lastEventAt: connection.lastEventAt?.toISOString() ?? null,
createdAt: connection.createdAt.toISOString(),
};
}
const token = crypto.randomBytes(32).toString("hex");
const now = new Date();
const connection = await db
.insert(webhookConnections)
.values({
userId: session.user.id,
provider,
token,
mediaServerUsername: mediaServerUsername.trim(),
enabled: true,
createdAt: now,
})
.returning()
.get();
return {
...connection,
lastEventAt: connection.lastEventAt?.toISOString() ?? null,
createdAt: connection.createdAt.toISOString(),
};
}
export async function deleteWebhookConnection(provider: "plex" | "jellyfin") {
const session = await getSession();
if (!["plex", "jellyfin"].includes(provider)) {
throw new Error("Invalid provider");
}
await db
.delete(webhookConnections)
.where(
and(
eq(webhookConnections.userId, session.user.id),
eq(webhookConnections.provider, provider),
),
)
.run();
}
export async function regenerateWebhookToken(provider: "plex" | "jellyfin") {
const session = await getSession();
if (!["plex", "jellyfin"].includes(provider)) {
throw new Error("Invalid provider");
}
const newToken = crypto.randomBytes(32).toString("hex");
const connection = await db
.update(webhookConnections)
.set({ token: newToken })
.where(
and(
eq(webhookConnections.userId, session.user.id),
eq(webhookConnections.provider, provider),
),
)
.returning()
.get();
if (!connection) {
throw new Error("Connection not found");
}
return {
...connection,
lastEventAt: connection.lastEventAt?.toISOString() ?? null,
createdAt: connection.createdAt.toISOString(),
};
}
export async function toggleRegistration(open: boolean) {
const session = await getSession();
if (session.user.role !== "admin") {
throw new Error("Forbidden");
}
await setSetting("registrationOpen", String(open));
}