137 lines
3.6 KiB
JavaScript
137 lines
3.6 KiB
JavaScript
/**
|
|
* @fileoverview Self-serve signup and admin tenant provisioning.
|
|
* @module services/tenantService
|
|
*/
|
|
|
|
import { randomUUID } from 'node:crypto';
|
|
import { createTenant, getTenant, listTenants } from '../store/tenants.js';
|
|
import { veraeClient } from '../clients/veraeClient.js';
|
|
import { AppError } from '../errors.js';
|
|
import { createDebugger } from '../debug/logger.js';
|
|
|
|
const log = createDebugger('auth');
|
|
const ALLOWED_PLANS = new Set(['free', 'starter', 'pro', 'enterprise']);
|
|
|
|
function slugify(value) {
|
|
return value
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-|-$/g, '')
|
|
.slice(0, 48);
|
|
}
|
|
|
|
async function validateVeraeCredentials(username, password) {
|
|
try {
|
|
await veraeClient.login({ username, password });
|
|
return true;
|
|
} catch (err) {
|
|
throw new AppError('Invalid Verae credentials', {
|
|
status: 400,
|
|
code: 'INVALID_VERAE_CREDENTIALS',
|
|
details: { message: err.message },
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Public free-tier signup.
|
|
* @param {{ email: string, name: string, veraeUsername: string, veraePassword: string }} params
|
|
*/
|
|
export async function selfServeSignup({ email, name, veraeUsername, veraePassword }) {
|
|
if (!email || !name || !veraeUsername || !veraePassword) {
|
|
throw new AppError('email, name, veraeUsername, and veraePassword are required', {
|
|
status: 400,
|
|
code: 'VALIDATION_ERROR',
|
|
});
|
|
}
|
|
|
|
await validateVeraeCredentials(veraeUsername, veraePassword);
|
|
|
|
const id = `tenant-${slugify(email)}-${randomUUID().slice(0, 8)}`;
|
|
const { tenant, apiKey } = createTenant({
|
|
id,
|
|
name,
|
|
plan: 'free',
|
|
veraeUsername,
|
|
veraePassword,
|
|
contract: null,
|
|
metadata: { email, audience: 'self-serve', createdVia: 'signup' },
|
|
});
|
|
|
|
log.info('self-serve signup', { tenantId: tenant.id });
|
|
|
|
return {
|
|
tenant: { id: tenant.id, name: tenant.name, plan: tenant.plan, email },
|
|
apiKey,
|
|
zapierSetup: {
|
|
authType: 'custom',
|
|
field: 'api_key',
|
|
middlewareUrl: '/zapier/v1/auth/me',
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Admin provision (internal / enterprise).
|
|
*/
|
|
export async function provisionTenant({
|
|
id,
|
|
name,
|
|
plan,
|
|
veraeUsername,
|
|
veraePassword,
|
|
contract = null,
|
|
metadata = {},
|
|
audience = 'admin',
|
|
}) {
|
|
if (!name || !plan || !veraeUsername || !veraePassword) {
|
|
throw new AppError('name, plan, veraeUsername, and veraePassword are required', {
|
|
status: 400,
|
|
code: 'VALIDATION_ERROR',
|
|
});
|
|
}
|
|
|
|
if (!ALLOWED_PLANS.has(plan)) {
|
|
throw new AppError(`Invalid plan: ${plan}`, { status: 400, code: 'VALIDATION_ERROR' });
|
|
}
|
|
|
|
if (plan === 'enterprise' && !contract) {
|
|
throw new AppError('enterprise tenants require a contract object', {
|
|
status: 400,
|
|
code: 'VALIDATION_ERROR',
|
|
});
|
|
}
|
|
|
|
await validateVeraeCredentials(veraeUsername, veraePassword);
|
|
|
|
const tenantId = id ?? `tenant-${slugify(name)}-${randomUUID().slice(0, 8)}`;
|
|
if (getTenant(tenantId)) {
|
|
throw new AppError('Tenant already exists', { status: 409, code: 'CONFLICT' });
|
|
}
|
|
|
|
const { tenant, apiKey } = createTenant({
|
|
id: tenantId,
|
|
name,
|
|
plan,
|
|
veraeUsername,
|
|
veraePassword,
|
|
contract,
|
|
metadata: { ...metadata, audience, createdVia: 'provision' },
|
|
});
|
|
|
|
log.info('tenant provisioned', { tenantId: tenant.id, plan, audience });
|
|
return { tenant: { id: tenant.id, name: tenant.name, plan: tenant.plan }, apiKey };
|
|
}
|
|
|
|
/**
|
|
* @returns {Array<object>}
|
|
*/
|
|
export function listProvisionedTenants() {
|
|
return listTenants().map((tenant) => ({
|
|
id: tenant.id,
|
|
name: tenant.name,
|
|
plan: tenant.plan,
|
|
audience: tenant.metadata?.audience ?? 'unknown',
|
|
createdAt: tenant.createdAt,
|
|
}));
|
|
}
|