mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-07-24 21:11:53 +00:00
444 lines
12 KiB
TypeScript
444 lines
12 KiB
TypeScript
"use server";
|
|
|
|
import { revalidatePath } from "next/cache";
|
|
import { z } from "zod";
|
|
|
|
import { signOut } from "@/auth.config";
|
|
import { apiBaseUrl, getAuthHeaders } from "@/lib/helper";
|
|
import { handleApiError, handleApiResponse } from "@/lib/server-actions-helper";
|
|
|
|
export const getAllTenants = async () => {
|
|
const headers = await getAuthHeaders({ contentType: false });
|
|
const url = new URL(`${apiBaseUrl}/tenants`);
|
|
|
|
try {
|
|
const response = await fetch(url.toString(), {
|
|
method: "GET",
|
|
headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch tenants data: ${response.statusText}`);
|
|
}
|
|
|
|
return handleApiResponse(response);
|
|
} catch (error) {
|
|
console.error("Error fetching tenants:", error);
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const editTenantFormSchema = z
|
|
.object({
|
|
tenantId: z.string(),
|
|
name: z.string().trim().min(1, { message: "Name is required" }),
|
|
currentName: z.string(),
|
|
})
|
|
.refine((data) => data.name !== data.currentName, {
|
|
message: "Name must be different from the current name",
|
|
path: ["name"],
|
|
});
|
|
|
|
export type UpdateTenantNameState =
|
|
| { errors: { name?: string } }
|
|
| { success: string }
|
|
| { error: string };
|
|
|
|
export async function updateTenantName(
|
|
_prevState: UpdateTenantNameState | null,
|
|
formData: FormData,
|
|
): Promise<UpdateTenantNameState> {
|
|
const headers = await getAuthHeaders({ contentType: true });
|
|
const formDataObject = Object.fromEntries(formData);
|
|
const validatedData = editTenantFormSchema.safeParse(formDataObject);
|
|
|
|
if (!validatedData.success) {
|
|
const formFieldErrors = validatedData.error.flatten().fieldErrors;
|
|
|
|
return {
|
|
errors: {
|
|
name: formFieldErrors?.name?.[0],
|
|
},
|
|
};
|
|
}
|
|
|
|
const { tenantId, name } = validatedData.data;
|
|
|
|
const payload = {
|
|
data: {
|
|
type: "tenants",
|
|
id: tenantId,
|
|
attributes: {
|
|
name: name.trim(),
|
|
},
|
|
},
|
|
};
|
|
|
|
try {
|
|
const url = new URL(`${apiBaseUrl}/tenants/${tenantId}`);
|
|
const response = await fetch(url.toString(), {
|
|
method: "PATCH",
|
|
headers,
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to update tenant name: ${response.statusText}`);
|
|
}
|
|
|
|
await handleApiResponse(response, "/profile", false);
|
|
return { success: "Tenant name updated successfully!" };
|
|
} catch (error) {
|
|
return handleApiError(error);
|
|
}
|
|
}
|
|
|
|
const switchTenantSchema = z.object({
|
|
tenantId: z.uuid(),
|
|
});
|
|
|
|
interface SwitchTenantSuccess {
|
|
success: true;
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
}
|
|
|
|
interface SwitchTenantError {
|
|
error: string;
|
|
}
|
|
|
|
export type SwitchTenantState = SwitchTenantSuccess | SwitchTenantError;
|
|
|
|
export async function switchTenant(
|
|
_prevState: SwitchTenantState | null,
|
|
formData: FormData,
|
|
): Promise<SwitchTenantState> {
|
|
const formDataObject = Object.fromEntries(formData);
|
|
const validatedData = switchTenantSchema.safeParse(formDataObject);
|
|
|
|
if (!validatedData.success) {
|
|
return { error: "Invalid tenant ID" };
|
|
}
|
|
|
|
const { tenantId } = validatedData.data;
|
|
const headers = await getAuthHeaders({ contentType: true });
|
|
|
|
const payload = {
|
|
data: {
|
|
type: "tokens-switch-tenant",
|
|
attributes: {
|
|
tenant_id: tenantId,
|
|
},
|
|
},
|
|
};
|
|
|
|
try {
|
|
const url = new URL(`${apiBaseUrl}/tokens/switch`);
|
|
const response = await fetch(url.toString(), {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => null);
|
|
const errorDetail =
|
|
errorData?.errors?.[0]?.detail ||
|
|
`Failed to switch tenant: ${response.statusText}`;
|
|
throw new Error(errorDetail);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const accessToken = data?.data?.attributes?.access;
|
|
const refreshToken = data?.data?.attributes?.refresh;
|
|
|
|
if (!accessToken || !refreshToken) {
|
|
throw new Error("Missing tokens in switch tenant response");
|
|
}
|
|
|
|
return { success: true, accessToken, refreshToken };
|
|
} catch (error) {
|
|
return handleApiError(error);
|
|
}
|
|
}
|
|
|
|
const createTenantSchema = z.object({
|
|
name: z
|
|
.string()
|
|
.trim()
|
|
.min(1, { message: "Name is required" })
|
|
.max(100, { message: "Name must be 100 characters or less" }),
|
|
});
|
|
|
|
interface CreateTenantSuccess {
|
|
success: true;
|
|
tenantId: string;
|
|
}
|
|
|
|
interface CreateTenantError {
|
|
error: string;
|
|
}
|
|
|
|
export type CreateTenantState = CreateTenantSuccess | CreateTenantError;
|
|
|
|
export async function createTenant(
|
|
_prevState: CreateTenantState | null,
|
|
formData: FormData,
|
|
): Promise<CreateTenantState> {
|
|
const formDataObject = Object.fromEntries(formData);
|
|
const validatedData = createTenantSchema.safeParse(formDataObject);
|
|
|
|
if (!validatedData.success) {
|
|
const fieldErrors = validatedData.error.flatten().fieldErrors;
|
|
return { error: fieldErrors?.name?.[0] || "Invalid input" };
|
|
}
|
|
|
|
const { name } = validatedData.data;
|
|
const headers = await getAuthHeaders({ contentType: true });
|
|
|
|
const payload = {
|
|
data: {
|
|
type: "tenants",
|
|
attributes: { name },
|
|
},
|
|
};
|
|
|
|
try {
|
|
const url = new URL(`${apiBaseUrl}/tenants`);
|
|
const response = await fetch(url.toString(), {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => null);
|
|
const errorDetail =
|
|
errorData?.errors?.[0]?.detail ||
|
|
`Failed to create tenant: ${response.statusText}`;
|
|
throw new Error(errorDetail);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const tenantId = data?.data?.id;
|
|
|
|
if (!tenantId) {
|
|
throw new Error("Missing tenant ID in create response");
|
|
}
|
|
|
|
revalidatePath("/profile");
|
|
return { success: true, tenantId };
|
|
} catch (error) {
|
|
return handleApiError(error);
|
|
}
|
|
}
|
|
|
|
const deleteTenantSchema = z.object({
|
|
tenantId: z.uuid(),
|
|
});
|
|
|
|
const switchThenDeleteTenantSchema = z.object({
|
|
tenantId: z.uuid(),
|
|
targetTenantId: z.uuid(),
|
|
});
|
|
|
|
interface DeleteTenantSuccess {
|
|
success: true;
|
|
}
|
|
|
|
interface DeleteTenantError {
|
|
error: string;
|
|
}
|
|
|
|
export type DeleteTenantState = DeleteTenantSuccess | DeleteTenantError;
|
|
|
|
export async function deleteTenant(
|
|
_prevState: DeleteTenantState | null,
|
|
formData: FormData,
|
|
): Promise<DeleteTenantState> {
|
|
const formDataObject = Object.fromEntries(formData);
|
|
const validatedData = deleteTenantSchema.safeParse(formDataObject);
|
|
|
|
if (!validatedData.success) {
|
|
return { error: "Invalid tenant ID" };
|
|
}
|
|
|
|
const { tenantId } = validatedData.data;
|
|
const headers = await getAuthHeaders({ contentType: false });
|
|
|
|
try {
|
|
const url = new URL(`${apiBaseUrl}/tenants/${tenantId}`);
|
|
const response = await fetch(url.toString(), {
|
|
method: "DELETE",
|
|
headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => null);
|
|
const errorDetail =
|
|
errorData?.errors?.[0]?.detail ||
|
|
`Failed to delete tenant: ${response.statusText}`;
|
|
throw new Error(errorDetail);
|
|
}
|
|
|
|
revalidatePath("/profile");
|
|
return { success: true };
|
|
} catch (error) {
|
|
return handleApiError(error);
|
|
}
|
|
}
|
|
|
|
export async function deleteTenantThenSignOut(
|
|
_prevState: DeleteTenantState | null,
|
|
formData: FormData,
|
|
): Promise<DeleteTenantState> {
|
|
const formDataObject = Object.fromEntries(formData);
|
|
const validatedData = deleteTenantSchema.safeParse(formDataObject);
|
|
|
|
if (!validatedData.success) {
|
|
return { error: "Invalid tenant ID" };
|
|
}
|
|
|
|
const { tenantId } = validatedData.data;
|
|
const headers = await getAuthHeaders({ contentType: false });
|
|
|
|
try {
|
|
const url = new URL(`${apiBaseUrl}/tenants/${tenantId}`);
|
|
const response = await fetch(url.toString(), {
|
|
method: "DELETE",
|
|
headers,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => null);
|
|
const errorDetail =
|
|
errorData?.errors?.[0]?.detail ||
|
|
`Failed to delete tenant: ${response.statusText}`;
|
|
throw new Error(errorDetail);
|
|
}
|
|
} catch (error) {
|
|
return handleApiError(error);
|
|
}
|
|
|
|
// Deleting the last tenant also removes the user account server-side, so
|
|
// there is no profile left to revalidate; close the session instead.
|
|
// signOut redirects by throwing, so it must stay outside the try/catch.
|
|
await signOut({ redirectTo: "/sign-in" });
|
|
|
|
// Unreachable: signOut always redirects. Present to satisfy the return type.
|
|
return { success: true };
|
|
}
|
|
|
|
interface SwitchThenDeleteSuccess {
|
|
success: true;
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
}
|
|
|
|
interface SwitchThenDeleteError {
|
|
error: string;
|
|
accessToken?: string;
|
|
refreshToken?: string;
|
|
}
|
|
|
|
export type SwitchThenDeleteTenantState =
|
|
| SwitchThenDeleteSuccess
|
|
| SwitchThenDeleteError;
|
|
|
|
export async function switchThenDeleteTenant(
|
|
_prevState: SwitchThenDeleteTenantState | null,
|
|
formData: FormData,
|
|
): Promise<SwitchThenDeleteTenantState> {
|
|
const formDataObject = Object.fromEntries(formData);
|
|
const validatedData = switchThenDeleteTenantSchema.safeParse(formDataObject);
|
|
|
|
if (!validatedData.success) {
|
|
return { error: "Invalid tenant or target tenant ID" };
|
|
}
|
|
|
|
const { tenantId, targetTenantId } = validatedData.data;
|
|
const headers = await getAuthHeaders({ contentType: true });
|
|
|
|
// Step 1: Switch to the target tenant (current token is still valid)
|
|
const switchPayload = {
|
|
data: {
|
|
type: "tokens-switch-tenant",
|
|
attributes: {
|
|
tenant_id: targetTenantId,
|
|
},
|
|
},
|
|
};
|
|
|
|
let newAccessToken: string;
|
|
let newRefreshToken: string;
|
|
|
|
try {
|
|
const switchUrl = new URL(`${apiBaseUrl}/tokens/switch`);
|
|
const switchResponse = await fetch(switchUrl.toString(), {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify(switchPayload),
|
|
});
|
|
|
|
if (!switchResponse.ok) {
|
|
const errorData = await switchResponse.json().catch(() => null);
|
|
const errorDetail =
|
|
errorData?.errors?.[0]?.detail ||
|
|
`Failed to switch tenant: ${switchResponse.statusText}`;
|
|
throw new Error(errorDetail);
|
|
}
|
|
|
|
const switchData = await switchResponse.json();
|
|
newAccessToken = switchData?.data?.attributes?.access;
|
|
newRefreshToken = switchData?.data?.attributes?.refresh;
|
|
|
|
if (!newAccessToken || !newRefreshToken) {
|
|
throw new Error("Missing tokens in switch tenant response");
|
|
}
|
|
} catch (error) {
|
|
return handleApiError(error);
|
|
}
|
|
|
|
// Step 2: Delete the old tenant using the NEW token
|
|
const deleteHeaders: Record<string, string> = {
|
|
Accept: "application/vnd.api+json",
|
|
Authorization: `Bearer ${newAccessToken}`,
|
|
};
|
|
|
|
try {
|
|
const deleteUrl = new URL(`${apiBaseUrl}/tenants/${tenantId}`);
|
|
const deleteResponse = await fetch(deleteUrl.toString(), {
|
|
method: "DELETE",
|
|
headers: deleteHeaders,
|
|
});
|
|
|
|
if (!deleteResponse.ok) {
|
|
const errorData = await deleteResponse.json().catch(() => null);
|
|
const errorDetail =
|
|
errorData?.errors?.[0]?.detail ||
|
|
`Failed to delete tenant: ${deleteResponse.statusText}`;
|
|
// Switch succeeded but delete failed — return tokens so client can still update session
|
|
return {
|
|
error: errorDetail,
|
|
accessToken: newAccessToken,
|
|
refreshToken: newRefreshToken,
|
|
};
|
|
}
|
|
|
|
revalidatePath("/profile");
|
|
return {
|
|
success: true,
|
|
accessToken: newAccessToken,
|
|
refreshToken: newRefreshToken,
|
|
};
|
|
} catch (error) {
|
|
// Switch succeeded but delete threw — return tokens so client can still update session
|
|
const errorResult = handleApiError(error);
|
|
return {
|
|
...errorResult,
|
|
accessToken: newAccessToken,
|
|
refreshToken: newRefreshToken,
|
|
};
|
|
}
|
|
}
|