auth: make ERPNext the single source of truth for customer passwords
Customer portal login previously checked a bcrypt customers.password_hash in Postgres, while portal.performancewest.net validated against ERPNext — two stores that drifted (the Paul Wilson lockout). Consolidate on ERPNext: - erpnext-client: add verifyWebsiteUserPassword() — delegates the credential check to Frappe /api/method/login (Host header = site name; 200=ok,401=bad). - portal-auth /login: verify against ERPNext, then mint the pw_customer cookie. - portal-auth /register: create+set the ERPNext password (authority) and upsert a password-less customers profile row; takeover guard still honors any legacy PG password until the column is dropped. - portal-auth /reset-password + /forgot-password: write the new password to ERPNext; forgot-password now also works for ERPNext-only users (creates the PG profile row on demand). - Legacy customers with only a PG bcrypt password reset via forgot-password. - checkout: refresh the stale comment (customers row is now a profile, no pw). Build + typecheck green.
This commit is contained in:
parent
557b45f65d
commit
9c87759501
3 changed files with 205 additions and 82 deletions
|
|
@ -1011,6 +1011,48 @@ export async function setWebsiteUserPassword(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a customer's password against ERPNext — the single source of truth for
|
||||
* customer credentials. The API portal does NOT keep its own password hash; it
|
||||
* delegates the check here and (on success) mints its own session cookie.
|
||||
*
|
||||
* Uses Frappe's form login endpoint (`/api/method/login`, usr/pwd). That
|
||||
* endpoint resolves the site by the Host header (NOT the X-Frappe-Site-Name
|
||||
* token header), so we must send Host explicitly or Frappe 404s with
|
||||
* "<site> does not exist". A 200 means the password is correct; 401 means it
|
||||
* is not. Network/other errors throw so the caller can fail closed.
|
||||
*
|
||||
* NB: this is a plain credential check — we discard any session cookie ERPNext
|
||||
* returns; the API issues its own `pw_customer` cookie.
|
||||
*/
|
||||
export async function verifyWebsiteUserPassword(
|
||||
email: string,
|
||||
password: string,
|
||||
): Promise<boolean> {
|
||||
const res = await fetch(`${ERPNEXT_URL}/api/method/login`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
// Frappe resolves the site for this endpoint from Host, not the token
|
||||
// site header. Send both so it works regardless of routing.
|
||||
Host: ERPNEXT_SITE_NAME,
|
||||
"X-Frappe-Site-Name": ERPNEXT_SITE_NAME,
|
||||
},
|
||||
body: JSON.stringify({ usr: email, pwd: password }),
|
||||
});
|
||||
|
||||
if (res.status === 200) return true;
|
||||
if (res.status === 401) return false;
|
||||
// 4xx/5xx other than auth failure (e.g. user disabled, site error) — surface
|
||||
// as an error so the route returns a 500 rather than a misleading "bad
|
||||
// password". Read the body for diagnostics.
|
||||
const body = await res.text().catch(() => "");
|
||||
throw new ERPNextError(res.status, {
|
||||
message: `ERPNext login check failed (status ${res.status})`,
|
||||
exception: body.slice(0, 500),
|
||||
} as FrappeErrorResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Link a Frappe User to a Customer record (portal_user_name field).
|
||||
* This is required for the ERPNext portal to show the correct customer's data.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue