190 lines
6.5 KiB
JavaScript
190 lines
6.5 KiB
JavaScript
// Auth flow helpers — register / confirm / login / refresh / logout.
|
|
// All requests go through lib/http.js; OTP comes from lib/otp.js.
|
|
import { post } from './http.js';
|
|
import { checkEnvelope } from './http.js';
|
|
import { cfg, unique } from './config.js';
|
|
import { fetchEmailOTP } from './otp.js';
|
|
import { generateTOTP } from './totp.js';
|
|
|
|
// makeIdentity returns a unique (email, password, display_name) tuple for the
|
|
// current VU iteration. Use to avoid collisions in concurrent runs.
|
|
export function makeIdentity(prefix = 'k6') {
|
|
const slug = unique(prefix);
|
|
return {
|
|
email: `${slug}@k6.local`,
|
|
password: 'K6-StrongPass-1!',
|
|
displayName: `K6 ${slug}`,
|
|
};
|
|
}
|
|
|
|
// registerEmail calls POST /api/v1/auth/register and returns the parsed
|
|
// RegisterData (challenge_id, expires_in, uid).
|
|
export function registerEmail({ tenantSlug = cfg.tenantSlug, inviteCode = cfg.inviteCode, email, password, displayName, language = 'zh-TW', termsVersion = '2025-01-01', marketingOptIn = false } = {}) {
|
|
const res = post('/api/v1/auth/register', {
|
|
tenant_slug: tenantSlug,
|
|
invite_code: inviteCode,
|
|
email,
|
|
password,
|
|
display_name: displayName,
|
|
language,
|
|
accept_terms_version: termsVersion,
|
|
marketing_opt_in: marketingOptIn,
|
|
});
|
|
const body = checkEnvelope(res, 'POST /auth/register');
|
|
if (!body.data || !body.data.challenge_id) {
|
|
throw new Error(`register: missing challenge_id in ${res.body}`);
|
|
}
|
|
return body.data;
|
|
}
|
|
|
|
// confirmRegister fetches the OTP from MailHog then calls
|
|
// POST /api/v1/auth/register/confirm. Returns AuthTokenData.
|
|
export function confirmRegister({ tenantSlug = cfg.tenantSlug, email, challengeId }) {
|
|
const code = fetchEmailOTP(email);
|
|
const res = post('/api/v1/auth/register/confirm', {
|
|
tenant_slug: tenantSlug,
|
|
challenge_id: challengeId,
|
|
code,
|
|
});
|
|
const body = checkEnvelope(res, 'POST /auth/register/confirm');
|
|
if (!body.data || !body.data.access_token) {
|
|
throw new Error(`register/confirm: missing access_token in ${res.body}`);
|
|
}
|
|
return body.data;
|
|
}
|
|
|
|
// resendRegister calls POST /api/v1/auth/register/resend.
|
|
// Returns RegisterData (challenge_id, expires_in, uid).
|
|
export function resendRegister({ tenantSlug = cfg.tenantSlug, challengeId }) {
|
|
const res = post('/api/v1/auth/register/resend', {
|
|
tenant_slug: tenantSlug,
|
|
challenge_id: challengeId,
|
|
});
|
|
return checkEnvelope(res, 'POST /auth/register/resend').data;
|
|
}
|
|
|
|
// registerResume calls POST /api/v1/auth/register/resume.
|
|
export function registerResume({ tenantSlug = cfg.tenantSlug, email }) {
|
|
const res = post('/api/v1/auth/register/resume', {
|
|
tenant_slug: tenantSlug,
|
|
email,
|
|
});
|
|
return checkEnvelope(res, 'POST /auth/register/resume').data;
|
|
}
|
|
|
|
// passwordForgot calls POST /api/v1/auth/password/forgot.
|
|
export function passwordForgot({ tenantSlug = cfg.tenantSlug, email }) {
|
|
const res = post('/api/v1/auth/password/forgot', {
|
|
tenant_slug: tenantSlug,
|
|
email,
|
|
});
|
|
return checkEnvelope(res, 'POST /auth/password/forgot').data;
|
|
}
|
|
|
|
// passwordReset calls POST /api/v1/auth/password/reset.
|
|
export function passwordReset({
|
|
tenantSlug = cfg.tenantSlug,
|
|
challengeId,
|
|
code,
|
|
newPassword,
|
|
}) {
|
|
const res = post('/api/v1/auth/password/reset', {
|
|
tenant_slug: tenantSlug,
|
|
challenge_id: challengeId,
|
|
code,
|
|
new_password: newPassword,
|
|
});
|
|
return checkEnvelope(res, 'POST /auth/password/reset').data;
|
|
}
|
|
|
|
// loginStep calls POST /api/v1/auth/login and returns LoginData (tokens or MFA challenge).
|
|
export function loginStep({ tenantSlug = cfg.tenantSlug, email, password }) {
|
|
const res = post('/api/v1/auth/login', {
|
|
tenant_slug: tenantSlug,
|
|
email,
|
|
password,
|
|
});
|
|
return checkEnvelope(res, 'POST /auth/login').data;
|
|
}
|
|
|
|
// loginMfaConfirm completes login after MFA challenge.
|
|
export function loginMfaConfirm({ tenantSlug = cfg.tenantSlug, challengeId, code }) {
|
|
const res = post('/api/v1/auth/login/mfa', {
|
|
tenant_slug: tenantSlug,
|
|
challenge_id: challengeId,
|
|
code,
|
|
});
|
|
return checkEnvelope(res, 'POST /auth/login/mfa').data;
|
|
}
|
|
|
|
// loginExpectMFA returns LoginData when mfa_required=true.
|
|
export function loginExpectMFA({ tenantSlug = cfg.tenantSlug, email, password }) {
|
|
const data = loginStep({ tenantSlug, email, password });
|
|
if (!data.mfa_required) {
|
|
throw new Error(`loginExpectMFA: expected mfa_required, got ${JSON.stringify(data)}`);
|
|
}
|
|
if (!data.mfa_challenge_id) {
|
|
throw new Error('loginExpectMFA: missing mfa_challenge_id');
|
|
}
|
|
return data;
|
|
}
|
|
|
|
// login calls POST /api/v1/auth/login. When TOTP is enrolled, pass totpCode or otpauthUrl.
|
|
export function login({ tenantSlug = cfg.tenantSlug, email, password, totpCode, otpauthUrl } = {}) {
|
|
const step = loginStep({ tenantSlug, email, password });
|
|
if (step.mfa_required) {
|
|
let code = totpCode;
|
|
if (!code && otpauthUrl) {
|
|
code = generateTOTP(otpauthUrl);
|
|
}
|
|
if (!code) {
|
|
throw new Error('login: MFA required but totpCode/otpauthUrl not provided');
|
|
}
|
|
const tokens = loginMfaConfirm({
|
|
tenantSlug,
|
|
challengeId: step.mfa_challenge_id,
|
|
code,
|
|
});
|
|
if (!tokens.access_token) {
|
|
throw new Error('login/mfa: missing access_token');
|
|
}
|
|
return tokens;
|
|
}
|
|
if (!step.access_token) {
|
|
throw new Error(`login: missing access_token in ${JSON.stringify(step)}`);
|
|
}
|
|
return step;
|
|
}
|
|
|
|
// refreshToken calls POST /api/v1/auth/token/refresh.
|
|
export function refreshToken({ refreshToken }) {
|
|
const res = post('/api/v1/auth/token/refresh', { refresh_token: refreshToken });
|
|
const body = checkEnvelope(res, 'POST /auth/token/refresh');
|
|
if (!body.data || !body.data.access_token) {
|
|
throw new Error(`token/refresh: missing access_token in ${res.body}`);
|
|
}
|
|
return body.data;
|
|
}
|
|
|
|
// logout calls POST /api/v1/auth/logout (requires Bearer access_token).
|
|
export function logout({ accessToken }) {
|
|
const res = post('/api/v1/auth/logout', null, { Authorization: `Bearer ${accessToken}` });
|
|
const body = checkEnvelope(res, 'POST /auth/logout');
|
|
return body.data;
|
|
}
|
|
|
|
// registerAndConfirm is the most common building block: makes an identity,
|
|
// runs register → confirm, returns { identity, tokens, registerData }.
|
|
export function registerAndConfirm({ tenantSlug = cfg.tenantSlug, inviteCode = cfg.inviteCode } = {}) {
|
|
const identity = makeIdentity();
|
|
const reg = registerEmail({
|
|
tenantSlug,
|
|
inviteCode,
|
|
email: identity.email,
|
|
password: identity.password,
|
|
displayName: identity.displayName,
|
|
});
|
|
const tokens = confirmRegister({ tenantSlug, email: identity.email, challengeId: reg.challenge_id });
|
|
return { identity, tokens, registerData: reg };
|
|
}
|