Fix employee creation for existing accounts
* fix: finalize existing employee accounts * test: cover existing employee finalize flow * test: include employee creation regression check * test: assert existing employee finalize helper path
This commit is contained in:
parent
5aeed250a3
commit
6b8a1a604e
@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js",
|
"check:syntax": "node --check public/app-runtime.js && node --check public/service-worker.js && node --check public/legacy/bootstrap.js && node --check public/core/sun-safe.js && node --check public/core/performance.js && node --check public/core/auth-security-v1774.js && node --check public/core/data-layer-v1773.js && node --check public/core/server-automation-v1770.js && node --check public/core/hotfix-v1763.js && node --check public/core/ops-ux-v1762.js && node --check public/core/ux-fixes-v1764.js && node --check public/core/pdf-engine.js && node --check public/core/classic-offer-pdf-v1767.js && node --check public/core/developer-console-v1768.js && node --check public/core/offer-workspace-v1769.js",
|
||||||
"test:static": "node tests/static-security.mjs && node tests/auth-security-v1774.mjs",
|
"test:static": "node tests/static-security.mjs && node tests/auth-security-v1774.mjs && node tests/employee-create-v1774.mjs",
|
||||||
"check:release": "node tests/release-check.mjs",
|
"check:release": "node tests/release-check.mjs",
|
||||||
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release",
|
"check:deploy": "npm run check:syntax && npm run test:static && npm run check:release",
|
||||||
"test:e2e": "playwright test --config=tests/playwright.config.mjs",
|
"test:e2e": "playwright test --config=tests/playwright.config.mjs",
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
10
tests/employee-create-v1774.mjs
Normal file
10
tests/employee-create-v1774.mjs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
const src=fs.readFileSync('supabase/functions/caterium-create-employee/index.ts','utf8');
|
||||||
|
let bad=0;const check=(v,m)=>{console.log(`${v?'OK':'FAIL'}: ${m}`);if(!v)bad++};
|
||||||
|
check(src.includes('prepStatus === "already_member"'),'already_member returns without duplicate creation');
|
||||||
|
check(src.includes('prepStatus === "existing"'),'existing auth user is handled explicitly');
|
||||||
|
check(src.includes('return await finalize(existingUserId, false, null)'),'existing auth user is finalized into workspace membership');
|
||||||
|
check(src.includes('retryStatus === "existing"'),'partial/concurrent auth creation retries finalize');
|
||||||
|
check(src.includes('sun_employee_finalize_v28'),'employee finalize RPC remains required');
|
||||||
|
check(!src.includes('if (prep.data?.status !== "new")'),'old early-return bug is removed');
|
||||||
|
if(bad)process.exit(1);
|
||||||
Loading…
Reference in New Issue
Block a user