89 lines
3 KiB
TypeScript
89 lines
3 KiB
TypeScript
/**
|
|
* Bind a portal customer to a Verae central user id.
|
|
* The public credential stays the zappier API key. The Verae JWT never leaves the server.
|
|
*/
|
|
import { createHash, randomBytes } from 'crypto';
|
|
|
|
export function normalizeVeraeUsername(username: string): string {
|
|
return username.trim().toLowerCase();
|
|
}
|
|
|
|
export function stableVeraeUserId(username: string): string {
|
|
const n = normalizeVeraeUsername(username);
|
|
return `vu_${createHash('sha256').update(n).digest('hex').slice(0, 16)}`;
|
|
}
|
|
|
|
export type VeraeBind = {
|
|
veraeUserId: string;
|
|
veraeUsername: string;
|
|
bound: boolean;
|
|
};
|
|
|
|
function mockBind(email: string): VeraeBind {
|
|
const veraeUsername = normalizeVeraeUsername(email);
|
|
return { veraeUserId: stableVeraeUserId(veraeUsername), veraeUsername, bound: true };
|
|
}
|
|
|
|
/**
|
|
* Register or look up the customer on api.veraetime.net.
|
|
* MOCK_VERAE (default) or missing VERAE_API_BASE_URL → stable id, no network.
|
|
*/
|
|
export async function bindVeraeUser(email: string, customerId?: string): Promise<VeraeBind> {
|
|
const identity = (process.env.IDENTITY_URL || '').replace(/\/$/, '');
|
|
if (identity) {
|
|
try {
|
|
const r = await fetch(`${identity}/bind`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ email, customerId }),
|
|
});
|
|
if (r.ok) return (await r.json()) as VeraeBind;
|
|
} catch {
|
|
/* fall through */
|
|
}
|
|
}
|
|
const mock = process.env.MOCK_VERAE !== 'false';
|
|
const base = (process.env.VERAE_API_BASE_URL || '').replace(/\/$/, '');
|
|
if (mock || !base) return mockBind(email);
|
|
|
|
const veraeUsername = normalizeVeraeUsername(email);
|
|
const adminUser = process.env.VERAE_ADMIN_USER;
|
|
const adminPass = process.env.VERAE_ADMIN_PASSWORD;
|
|
const password = `vt_${randomBytes(18).toString('base64url')}`;
|
|
|
|
const login = async (username: string, pass: string) => {
|
|
const r = await fetch(`${base}/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ username, password: pass }),
|
|
});
|
|
if (!r.ok) throw new Error(`verae login ${r.status}`);
|
|
return r.json() as Promise<{ token: string; user?: { id?: string; username?: string } }>;
|
|
};
|
|
|
|
if (!adminUser || !adminPass) return mockBind(email);
|
|
|
|
try {
|
|
const admin = await login(adminUser, adminPass);
|
|
const created = await fetch(`${base}/auth/users`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
authorization: `Bearer ${admin.token}`,
|
|
},
|
|
body: JSON.stringify({ username: veraeUsername, password, role: 'user' }),
|
|
});
|
|
if (!created.ok && created.status !== 409) {
|
|
return mockBind(email);
|
|
}
|
|
const user = (await created.json().catch(() => ({}))) as { id?: string; username?: string };
|
|
const id = user.id || (await login(veraeUsername, password)).user?.id;
|
|
return {
|
|
veraeUserId: id || stableVeraeUserId(veraeUsername),
|
|
veraeUsername,
|
|
bound: true,
|
|
};
|
|
} catch {
|
|
return mockBind(email);
|
|
}
|
|
}
|