Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 58 additions & 88 deletions src/actions/admin-management.ts
Original file line number Diff line number Diff line change
@@ -1,135 +1,105 @@
"use server";

import {
addMockAdmin,
MOCK_ADMIN_ACCOUNTS,
updateMockAdminEmail,
updateMockAdminRole,
updateMockAdminStatus,
} from "@/mocks/admin-management";
import type {
AdminAccountsPage,
AdminAccountsQueryParams,
AdminManagementResult,
ChangeAdminEmailInput,
ChangeAdminRoleInput,
GetAdminAccountsResponse,
InviteAdminInput,
ManagedAdminAccount,
} from "@/types/api/admin-management";
import { authApi } from "@/lib/api/clients";
import { toApiError } from "@/lib/api/errors";
import { unwrapData } from "./utils";

function isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function adminExists(email: string, ignoreId?: string): boolean {
return MOCK_ADMIN_ACCOUNTS.some(
(admin) =>
admin.email.toLowerCase() === email.toLowerCase() &&
admin.id !== ignoreId,
);
}

function success(message: string): AdminManagementResult {
function ok(message: string): AdminManagementResult {
return { ok: true, message };
}

function fail(message: string): AdminManagementResult {
return { ok: false, message };
}

export async function getAdminAccounts(): Promise<ManagedAdminAccount[]> {
return MOCK_ADMIN_ACCOUNTS;
async function callAdminApi(
request: () => Promise<unknown>,
successMessage: string,
): Promise<AdminManagementResult> {
try {
await request();
return ok(successMessage);
} catch (err) {
return fail(toApiError(err).message);
}
}

export async function getAdminAccounts(
params: AdminAccountsQueryParams = {},
): Promise<AdminAccountsPage> {
const res = await authApi.get<GetAdminAccountsResponse>("/admin/admins", {
params,
});
// Response is double-wrapped: ApiEnvelope.data = { status, data: { items, ... } }
return unwrapData(res).data;
}

export async function inviteAdmin(
input: InviteAdminInput,
): Promise<AdminManagementResult> {
const email = input.email.trim().toLowerCase();

if (!isValidEmail(email)) {
return fail("Enter a valid email address.");
}

if (adminExists(email)) {
return fail("An admin account already exists with this email.");
}

addMockAdmin(email);

return success(`Invite sent to ${email}.`);
return callAdminApi(
() => authApi.post("/admin/admins/invite", { email: input.email }),
`Invite sent to ${input.email}.`,
);
}

export async function resetAdminPassword(
id: string,
): Promise<AdminManagementResult> {
const admin = MOCK_ADMIN_ACCOUNTS.find((account) => account.id === id);

if (!admin) {
return fail("Admin account not found.");
}

return success(`Password reset for ${admin.name}.`);
return callAdminApi(
() => authApi.post(`/admin/admins/${id}/reset-password`),
"Password reset email sent.",
);
}

export async function changeAdminEmail(
input: ChangeAdminEmailInput,
): Promise<AdminManagementResult> {
const email = input.email.trim().toLowerCase();
const admin = MOCK_ADMIN_ACCOUNTS.find((account) => account.id === input.id);

if (!admin) {
return fail("Admin account not found.");
}

if (!isValidEmail(email)) {
return fail("Enter a valid email address.");
}

if (adminExists(email, input.id)) {
return fail("An admin account already exists with this email.");
}

updateMockAdminEmail(input.id, email);

return success(`Email updated for ${admin.name}.`);
return callAdminApi(
() =>
authApi.patch(`/admin/admins/${input.id}/email`, {
email: input.email,
}),
"Email updated successfully.",
);
}

export async function changeAdminRole(
input: ChangeAdminRoleInput,
): Promise<AdminManagementResult> {
const admin = MOCK_ADMIN_ACCOUNTS.find((account) => account.id === input.id);

if (!admin) {
return fail("Admin account not found.");
}

updateMockAdminRole(input.id, input.role);

return success(`Role updated for ${admin.name}.`);
return callAdminApi(
() =>
authApi.patch(`/admin/admins/${input.id}/role`, {
role: input.role,
confirm_downgrade: input.confirm_downgrade,
}),
"Role updated successfully.",
);
}

export async function deactivateAdminAccount(
id: string,
): Promise<AdminManagementResult> {
const admin = MOCK_ADMIN_ACCOUNTS.find((account) => account.id === id);

if (!admin) {
return fail("Admin account not found.");
}

updateMockAdminStatus(id, "Deactivated");

return success("Account deactivated.");
return callAdminApi(
() => authApi.patch(`/admin/admins/${id}/deactivate`),
"Account deactivated.",
);
}

export async function reactivateAdminAccount(
id: string,
): Promise<AdminManagementResult> {
const admin = MOCK_ADMIN_ACCOUNTS.find((account) => account.id === id);

if (!admin) {
return fail("Admin account not found.");
}

updateMockAdminStatus(id, "Active");

return success("Account reactivated.");
return callAdminApi(
() => authApi.patch(`/admin/admins/${id}/reactivate`),
"Account reactivated.",
);
}
32 changes: 23 additions & 9 deletions src/actions/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
RefreshResponseData,
} from "@/types/api/auth";
import { publicApi } from "@/lib/api/clients";
import { toApiError } from "@/lib/api/errors";
import {
getServerCookieHeader,
parseSetCookieHeader,
Expand All @@ -15,11 +16,23 @@ import {
} from "@/lib/api/cookies";
import { unwrapData } from "./utils";

export async function login(body: LoginInput): Promise<LoginResponseData> {
const res = await publicApi.post<ApiEnvelope<LoginResponseData>>(
"/admin/auth/login",
body,
);
export type LoginResult =
| { success: true; data: LoginResponseData }
| { success: false; error: string };

export async function login(body: LoginInput): Promise<LoginResult> {
let res: Awaited<
ReturnType<typeof publicApi.post<ApiEnvelope<LoginResponseData>>>
>;

try {
res = await publicApi.post<ApiEnvelope<LoginResponseData>>(
"/admin/auth/login",
body,
);
} catch (err) {
return { success: false, error: toApiError(err).message };
}

// Forward API auth cookies to the browser so subsequent server-action API
// calls can read and proxy them via getServerCookieHeader().
Expand All @@ -28,14 +41,15 @@ export async function login(body: LoginInput): Promise<LoginResponseData> {
.filter((c): c is NonNullable<typeof c> => c != null);

if (cookies.length === 0) {
throw new Error(
"Authentication failed: no session cookies returned by the API.",
);
return {
success: false,
error: "Authentication failed: no session cookies returned by the API.",
};
}

await persistServerCookies(cookies);

return unwrapData(res);
return { success: true, data: unwrapData(res) };
}

export async function refreshTokens(): Promise<RefreshResponseData> {
Expand Down
Loading
Loading