"use server"; import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { notifyInvitation } from "@/lib/emails"; import { answerInvitation, cancelInvitation, createOrg, deleteOrg, inviteMember, isOrgRole, OrgError, removeMember, removeOrgAvatar, ROLE_INFO, setMemberRole, setOrgAvatar, updateOrgProfile, } from "@/lib/orgs"; import { ActionError, orgForAction, viewerOrThrow } from "./guard"; import type { FormState } from "./types"; async function attempt(fn: () => Promise): Promise { try { return (await fn()) ?? undefined; } catch (e) { if (e instanceof OrgError || e instanceof ActionError) return { error: e.message }; throw e; } } function roleOf(formData: FormData) { const role = formData.get("role"); if (!isOrgRole(role)) throw new ActionError("Choose a role."); return role; } // Memberships change what's in everyone's sidebar and which repositories they see. const refresh = () => revalidatePath("/", "layout"); export async function createOrgAction(_prev: FormState, formData: FormData): Promise { let path = ""; const result = await attempt(async () => { const viewer = await viewerOrThrow(); const org = await createOrg(viewer, { name: String(formData.get("name") ?? ""), displayName: String(formData.get("displayName") ?? ""), description: String(formData.get("description") ?? ""), }); path = `/organizations/${org.username}/settings/members`; }); if (result?.error) return result; refresh(); redirect(path); } export async function updateOrgAction(name: string, _prev: FormState, formData: FormData) { return attempt(async () => { const { org } = await orgForAction(name, "owner"); await updateOrgProfile(org.id, { displayName: String(formData.get("displayName") ?? ""), description: String(formData.get("description") ?? ""), }); revalidatePath(`/${org.username}`); return { success: "Profile updated." }; }); } export async function updateOrgAvatarAction(name: string, _prev: FormState, formData: FormData) { return attempt(async () => { const { org } = await orgForAction(name, "owner"); const file = formData.get("image"); if (!(file instanceof File)) throw new OrgError("Choose an image to upload."); await setOrgAvatar(org.id, new Uint8Array(await file.arrayBuffer())); // The avatar shows up in sidebars and lists everywhere, beside the organization's repositories too. refresh(); return { success: "Avatar updated." }; }); } export async function removeOrgAvatarAction(name: string) { const { org } = await orgForAction(name, "owner"); await removeOrgAvatar(org.id); refresh(); } export async function deleteOrgAction(name: string, _prev: FormState, formData: FormData) { const result = await attempt(async () => { const { org } = await orgForAction(name, "owner"); if (formData.get("confirm") !== org.username) return { error: `Type ${org.username} to confirm.` }; await deleteOrg(org); }); if (result?.error) return result; refresh(); redirect("/settings/organizations"); } export async function inviteMemberAction(name: string, _prev: FormState, formData: FormData) { return attempt(async () => { const { org, viewer } = await orgForAction(name, "owner"); const role = roleOf(formData); const user = await inviteMember(org, viewer, String(formData.get("username") ?? ""), role); notifyInvitation(user, org, viewer, role); revalidatePath(`/organizations/${org.username}/settings/members`); return { success: `Invited ${user.username} as ${ROLE_INFO[role].title.toLowerCase()}. They’ll find the invitation on their dashboard.` }; }); } export async function cancelInvitationAction(name: string, invitationId: string) { const { org } = await orgForAction(name, "owner"); await cancelInvitation(org.id, String(invitationId)); revalidatePath(`/organizations/${org.username}/settings/members`); } export async function setMemberRoleAction(name: string, userId: string, _prev: FormState, formData: FormData) { return attempt(async () => { const { org } = await orgForAction(name, "owner"); const role = roleOf(formData); await setMemberRole(org.id, String(userId), role); refresh(); return { success: `Role changed to ${ROLE_INFO[role].title.toLowerCase()}.` }; }); } export async function removeMemberAction(name: string, userId: string): Promise { let left = false; const result = await attempt(async () => { const { org, viewer } = await orgForAction(name, "owner"); await removeMember(org.id, String(userId)); left = userId === viewer.id; }); if (result?.error) return result; refresh(); if (left) redirect("/settings/organizations"); return { success: "Member removed." }; } /** Any member can leave, unless they're its last owner. */ export async function leaveOrgAction(name: string): Promise { const result = await attempt(async () => { const { org, viewer } = await orgForAction(name, "read"); await removeMember(org.id, viewer.id); }); if (result?.error) return result; refresh(); return { success: `You left ${name}.` }; } export async function answerInvitationAction(invitationId: string, accept: boolean): Promise { accept = accept === true; let path = ""; const result = await attempt(async () => { const viewer = await viewerOrThrow(); const invitation = await answerInvitation(viewer.id, String(invitationId), accept); if (accept) path = `/${invitation.org.username}`; }); if (result?.error) return result; refresh(); if (path) redirect(path); return { success: "Invitation declined." }; }