diff --git a/supabase/functions/caterium-create-employee/index.ts b/supabase/functions/caterium-create-employee/index.ts index 5e31f54..9c57489 100644 --- a/supabase/functions/caterium-create-employee/index.ts +++ b/supabase/functions/caterium-create-employee/index.ts @@ -9,6 +9,14 @@ const 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) => { if (req.method === "OPTIONS") return new Response("ok", { headers: cors }); 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 serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") || ""; if (!url || !anon || !serviceKey) return reply({ error: "Server auth configuration is incomplete" }, 500); + const caller = createClient(url, anon, { global: { headers: { Authorization: auth } }, auth: { persistSession: false, autoRefreshToken: false }, @@ -28,21 +37,66 @@ Deno.serve(async (req: Request) => { const email = String(body.email || "").trim().toLowerCase(); const displayName = String(body.display_name || "").trim(); const role = String(body.role || "manager"); - const prep = await caller.rpc("sun_employee_prepare_v28", { - p_workspace: workspaceId, - p_email: email, - p_display_name: displayName, - p_role: role, - }); - if (prep.error) return reply({ error: prep.error.message }, 400); - if (prep.data?.status !== "new") { - return reply({ status: prep.data?.status, user_id: prep.data?.user_id, email, display_name: displayName, role }, 200); + + const prepare = async () => { + const result = await caller.rpc("sun_employee_prepare_v28", { + p_workspace: workspaceId, + p_email: email, + p_display_name: displayName, + p_role: role, + }); + 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 bytes = crypto.getRandomValues(new Uint8Array(12)); let password = ""; for (const b of bytes) password += alphabet[b % alphabet.length]; password = password.slice(0, 6) + "-" + password.slice(6); + const created = await service.auth.admin.createUser({ email, password, @@ -53,15 +107,31 @@ Deno.serve(async (req: Request) => { 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", { - p_workspace: workspaceId, - p_user_id: created.data.user.id, - p_display_name: displayName, - p_role: role, - }); - if (fin.error) return reply({ error: fin.error.message }, 400); - return reply({ ...fin.data, created: true, temporary_password: password, must_change_password: true }); + + if (created.error || !created.data.user) { + // A concurrent request or a previous partial failure may have created Auth already. + // Re-read server state and finalize the existing account instead of leaving the workspace half-configured. + const retryPrep = await prepare(); + const retryStatus = String(retryPrep.status || ""); + if (retryStatus === "already_member") { + return reply({ + 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) { return reply({ error: e instanceof Error ? e.message : String(e) }, 500); }