template-monorepo/test/k6/lib/auth.js

190 lines
6.5 KiB
JavaScript
Raw Permalink Normal View History

2026-05-26 06:05:33 +00:00
// 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';
2026-05-26 17:10:32 +00:00
import { generateTOTP } from './totp.js';
2026-05-26 06:05:33 +00:00
// 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;
}
2026-05-26 17:10:32 +00:00
// 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 }) {
2026-05-26 06:05:33 +00:00
const res = post('/api/v1/auth/login', {
tenant_slug: tenantSlug,
email,
password,
});
2026-05-26 17:10:32 +00:00
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)}`);
2026-05-26 06:05:33 +00:00
}
2026-05-26 17:10:32 +00:00
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;
2026-05-26 06:05:33 +00:00
}
// 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 };
}