"use server";
import { revalidatePath } from "next/cache";
import { renewSession, signIn } from "@/lib/auth";
import { addSshKey, deleteSshKey, KeyError } from "@/lib/keys";
import { canWrite, findRepoById, repoAccess } from "@/lib/repos";
import { createToken, deleteToken, TokenError, type TokenPermission } from "@/lib/tokens";
import { setPassword, updateProfile, UserError, validatePassword, verifyUserPassword } from "@/lib/users";
import { viewerOrThrow } from "./guard";
import type { FormState } from "./types";
export async function updateProfileAction(_prev: FormState, formData: FormData): Promise<FormState> {
const viewer = await viewerOrThrow();
try {
const { emailChanged } = await updateProfile(viewer.id, {
name: String(formData.get("name") ?? ""),
email: String(formData.get("email") ?? ""),
bio: String(formData.get("bio") ?? ""),
});
// PocketBase revokes a user's tokens when their email changes; keep this browser signed in.
if (emailChanged) await renewSession(viewer.id);
} catch (e) {
if (e instanceof UserError) return { error: e.message };
throw e;
}
revalidatePath("/", "layout");
return { success: "Profile updated." };
}
export async function changePasswordAction(_prev: FormState, formData: FormData): Promise<FormState> {
const viewer = await viewerOrThrow();
const current = String(formData.get("current") ?? "");
const next = String(formData.get("password") ?? "");
if (!(await verifyUserPassword(viewer.id, current))) return { error: "Your current password is incorrect." };
if (next !== formData.get("confirm")) return { error: "The new passwords don't match." };
const problem = validatePassword(next);
if (problem) return { error: problem };
// This signs the user out everywhere, so sign this browser back in.
await setPassword(viewer.id, next);
await signIn(viewer.email, next);
return { success: "Password changed." };
}
export async function addSshKeyAction(_prev: FormState, formData: FormData): Promise<FormState> {
const viewer = await viewerOrThrow();
try {
await addSshKey(viewer.id, String(formData.get("title") ?? ""), String(formData.get("key") ?? ""));
} catch (e) {
if (e instanceof KeyError) return { error: e.message };
throw e;
}
revalidatePath("/settings/keys");
return { success: "SSH key added." };
}
export async function deleteSshKeyAction(keyId: string) {
const viewer = await viewerOrThrow();
await deleteSshKey(viewer.id, String(keyId));
revalidatePath("/settings/keys");
}
export async function createTokenAction(_prev: FormState, formData: FormData): Promise<FormState> {
const viewer = await viewerOrThrow();
const permission: TokenPermission = formData.get("permission") === "read" ? "read" : "write";
const expiry = String(formData.get("expiry") ?? "30");
let repoIds: string[] | null = null;
if (formData.get("scope") === "selected") {
repoIds = [];
for (const id of new Set(formData.getAll("repos").map(String))) {
const repo = await findRepoById(id);
const access = repo ? await repoAccess(repo, viewer.id) : null;
if (access && (permission === "read" || canWrite(access))) repoIds.push(id);
}
}
try {
const token = await createToken(viewer.id, {
name: String(formData.get("name") ?? ""),
permission,
expiresInDays: expiry === "never" ? null : Number(expiry) || 30,
repoIds,
});
revalidatePath("/settings/tokens");
return { token };
} catch (e) {
if (e instanceof TokenError) return { error: e.message };
throw e;
}
}
export async function deleteTokenAction(tokenId: string) {
const viewer = await viewerOrThrow();
await deleteToken(viewer.id, String(tokenId));
revalidatePath("/settings/tokens");
}