fix: finalize existing employee accounts

This commit is contained in:
pavlov346346-source 2026-09-11 07:31:36 +03:00
parent 5aeed250a3
commit 31094bc4d4

View File

@ -9,6 +9,14 @@ const cors = {
}; };
const reply = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: cors }); const reply = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: cors });
type PrepData = {
status?: string;
user_id?: string | null;
email?: string;
display_name?: string;
role?: string;
};
Deno.serve(async (req: Request) => { Deno.serve(async (req: Request) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: cors }); if (req.method === "OPTIONS") return new Response("ok", { headers: cors });
if (req.method !== "POST") return reply({ error: "Method not allowed" }, 405); if (req.method !== "POST") return reply({ error: "Method not allowed" }, 405);
@ -18,6 +26,7 @@ Deno.serve(async (req: Request) => {
const anon = Deno.env.get("SUPABASE_ANON_KEY") || ""; const anon = Deno.env.get("SUPABASE_ANON_KEY") || "";
const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") || ""; const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") || "";
if (!url || !anon || !serviceKey) return reply({ error: "Server auth configuration is incomplete" }, 500); if (!url || !anon || !serviceKey) return reply({ error: "Server auth configuration is incomplete" }, 500);
const caller = createClient(url, anon, { const caller = createClient(url, anon, {
global: { headers: { Authorization: auth } }, global: { headers: { Authorization: auth } },
auth: { persistSession: false, autoRefreshToken: false }, auth: { persistSession: false, autoRefreshToken: false },
@ -28,21 +37,66 @@ Deno.serve(async (req: Request) => {
const email = String(body.email || "").trim().toLowerCase(); const email = String(body.email || "").trim().toLowerCase();
const displayName = String(body.display_name || "").trim(); const displayName = String(body.display_name || "").trim();
const role = String(body.role || "manager"); const role = String(body.role || "manager");
const prep = await caller.rpc("sun_employee_prepare_v28", {
p_workspace: workspaceId, const prepare = async () => {
p_email: email, const result = await caller.rpc("sun_employee_prepare_v28", {
p_display_name: displayName, p_workspace: workspaceId,
p_role: role, p_email: email,
}); p_display_name: displayName,
if (prep.error) return reply({ error: prep.error.message }, 400); p_role: role,
if (prep.data?.status !== "new") { });
return reply({ status: prep.data?.status, user_id: prep.data?.user_id, email, display_name: displayName, role }, 200); if (result.error) throw new Error(result.error.message);
return (result.data || {}) as PrepData;
};
const finalize = async (userId: string, created: boolean, temporaryPassword: string | null = null) => {
const fin = await caller.rpc("sun_employee_finalize_v28", {
p_workspace: workspaceId,
p_user_id: userId,
p_display_name: displayName,
p_role: role,
});
if (fin.error) throw new Error(fin.error.message);
return reply({
...fin.data,
created,
temporary_password: temporaryPassword,
must_change_password: created,
});
};
const prep = await prepare();
const prepStatus = String(prep.status || "");
if (prepStatus === "already_member") {
return reply({
status: "already_member",
user_id: prep.user_id,
email: prep.email || email,
display_name: prep.display_name || displayName,
role: prep.role || role,
created: false,
temporary_password: null,
must_change_password: false,
});
} }
if (prepStatus === "existing") {
const existingUserId = String(prep.user_id || "");
if (!existingUserId) return reply({ error: "Existing employee account has no user id" }, 409);
return await finalize(existingUserId, false, null);
}
if (prepStatus !== "new") {
return reply({ error: `Unexpected employee prepare status: ${prepStatus || "empty"}` }, 409);
}
const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"; const alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
const bytes = crypto.getRandomValues(new Uint8Array(12)); const bytes = crypto.getRandomValues(new Uint8Array(12));
let password = ""; let password = "";
for (const b of bytes) password += alphabet[b % alphabet.length]; for (const b of bytes) password += alphabet[b % alphabet.length];
password = password.slice(0, 6) + "-" + password.slice(6); password = password.slice(0, 6) + "-" + password.slice(6);
const created = await service.auth.admin.createUser({ const created = await service.auth.admin.createUser({
email, email,
password, password,
@ -53,15 +107,31 @@ Deno.serve(async (req: Request) => {
registration_source: "caterium_employee_admin", registration_source: "caterium_employee_admin",
}, },
}); });
if (created.error || !created.data.user) return reply({ error: created.error?.message || "Create failed" }, 400);
const fin = await caller.rpc("sun_employee_finalize_v28", { if (created.error || !created.data.user) {
p_workspace: workspaceId, // A concurrent request or a previous partial failure may have created Auth already.
p_user_id: created.data.user.id, // Re-read server state and finalize the existing account instead of leaving the workspace half-configured.
p_display_name: displayName, const retryPrep = await prepare();
p_role: role, const retryStatus = String(retryPrep.status || "");
}); if (retryStatus === "already_member") {
if (fin.error) return reply({ error: fin.error.message }, 400); return reply({
return reply({ ...fin.data, created: true, temporary_password: password, must_change_password: true }); status: "already_member",
user_id: retryPrep.user_id,
email: retryPrep.email || email,
display_name: retryPrep.display_name || displayName,
role: retryPrep.role || role,
created: false,
temporary_password: null,
must_change_password: false,
});
}
if (retryStatus === "existing" && retryPrep.user_id) {
return await finalize(String(retryPrep.user_id), false, null);
}
return reply({ error: created.error?.message || "Create failed" }, 400);
}
return await finalize(created.data.user.id, true, password);
} catch (e) { } catch (e) {
return reply({ error: e instanceof Error ? e.message : String(e) }, 500); return reply({ error: e instanceof Error ? e.message : String(e) }, 500);
} }