#!/usr/bin/env node const fs = require('node:fs') const path = require('node:path') const { authenticator } = require('otplib') function parseEnvFile(content) { const env = {} for (const rawLine of content.split(/\r?\n/)) { const line = rawLine.trim() if (!line || line.startsWith('#')) continue const normalized = line.startsWith('export ') ? line.slice(7).trim() : line const separatorIndex = normalized.indexOf('=') if (separatorIndex === -1) continue const key = normalized.slice(0, separatorIndex).trim() if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue let value = normalized.slice(separatorIndex + 1).trim() if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { const quote = value[0] value = value.slice(1, -1) if (quote === '"') { value = value .replace(/\\n/g, '\n') .replace(/\\r/g, '\r') .replace(/\\t/g, '\t') .replace(/\\"/g, '"') .replace(/\\\\/g, '\\') } } env[key] = value } return env } function loadLocalEnv() { const envPath = path.resolve(process.cwd(), '.env.local') if (!fs.existsSync(envPath)) return {} return parseEnvFile(fs.readFileSync(envPath, 'utf8')) } function normalizeApiBase(value) { const base = (value || 'http://localhost:4000').replace(/\/$/, '') return base.endsWith('/api/v1') ? base : `${base}/api/v1` } function readArg(name) { const prefix = `--${name}=` const match = process.argv.slice(2).find((arg) => arg.startsWith(prefix)) return match ? match.slice(prefix.length) : undefined } function readValue(env, argName, envNames, fallback) { const argValue = readArg(argName) if (argValue !== undefined) return argValue for (const envName of envNames) { if (env[envName] !== undefined) return env[envName] } return fallback } function updateCookieJar(cookieJar, setCookieHeaders) { for (const header of setCookieHeaders) { const pair = header.split(';')[0] const separatorIndex = pair.indexOf('=') if (separatorIndex === -1) continue const name = pair.slice(0, separatorIndex) const value = pair.slice(separatorIndex + 1) if (value) cookieJar.set(name, value) else cookieJar.delete(name) } } function cookieHeader(cookieJar) { return Array.from(cookieJar.entries()) .map(([name, value]) => `${name}=${value}`) .join('; ') } function originFromUrl(value) { try { return new URL(value).origin } catch { return undefined } } function isMutatingMethod(method) { return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(String(method || 'GET').toUpperCase()) } async function request(apiBase, cookieJar, pathName, options = {}, trustedOrigin) { const headers = { ...(options.body ? { 'Content-Type': 'application/json' } : {}), ...(cookieJar.size ? { Cookie: cookieHeader(cookieJar) } : {}), ...(trustedOrigin && isMutatingMethod(options.method) ? { Origin: trustedOrigin } : {}), ...options.headers, } const response = await fetch(`${apiBase}${pathName}`, { ...options, headers, }) updateCookieJar(cookieJar, response.headers.getSetCookie?.() ?? []) const text = await response.text() let json = null if (text) { try { json = JSON.parse(text) } catch { throw new Error(`Non-JSON response from ${pathName}: ${text.slice(0, 200)}`) } } return { response, json } } async function main() { const localEnv = loadLocalEnv() const env = { ...localEnv, ...process.env } const email = readValue(env, 'email', ['ADMIN_EMAIL', 'ADMIN_SEED_EMAIL'], 'admin@rentaldrivego.ma') const password = readValue(env, 'password', ['ADMIN_PASSWORD', 'ADMIN_SEED_PASSWORD']) const apiBase = normalizeApiBase(readValue(env, 'api-url', ['NEXT_PUBLIC_API_URL', 'API_URL'])) const trustedOrigin = originFromUrl(readValue(env, 'origin', ['ADMIN_URL', 'NEXT_PUBLIC_ADMIN_URL', 'DASHBOARD_URL', 'NEXT_PUBLIC_DASHBOARD_URL'], 'http://localhost:3000/admin')) if (!password || /^(placeholder|changeme|change-me)$/i.test(password)) { throw new Error('Set ADMIN_PASSWORD or ADMIN_SEED_PASSWORD, or pass --password=...') } const cookieJar = new Map() const login = await request(apiBase, cookieJar, '/admin/auth/login', { method: 'POST', body: JSON.stringify({ email, password }), }, trustedOrigin) if (!login.response.ok) { const error = login.json?.error || login.response.status if (error === 'totp_required') { throw new Error('This admin already has 2FA enabled. Use your authenticator or recovery code to sign in.') } throw new Error(login.json?.message || `Admin login failed: ${error}`) } const me = await request(apiBase, cookieJar, '/admin/auth/me', {}, trustedOrigin) const admin = me.json?.data ?? me.json if (admin?.totpEnabled) { console.log(`Admin 2FA is already enabled for ${email}.`) return } const setup = await request(apiBase, cookieJar, '/admin/auth/2fa/setup', { method: 'POST' }, trustedOrigin) if (!setup.response.ok) { throw new Error(setup.json?.message || 'Failed to create admin 2FA setup secret.') } const secret = setup.json?.data?.secret ?? setup.json?.secret if (!secret) throw new Error('2FA setup response did not include a secret.') const code = authenticator.generate(secret) const verify = await request(apiBase, cookieJar, '/admin/auth/2fa/verify', { method: 'POST', body: JSON.stringify({ code }), }, trustedOrigin) if (!verify.response.ok) { throw new Error(verify.json?.message || 'Failed to verify generated 2FA code.') } const result = verify.json?.data ?? verify.json console.log(`Admin 2FA enabled for ${email}.`) console.log(`TOTP secret: ${secret}`) console.log('Add this secret to your authenticator app. The generated QR code data URL is available from /admin/auth/2fa/setup if you prefer scanning.') if (Array.isArray(result?.recoveryCodes) && result.recoveryCodes.length > 0) { console.log('\nRecovery codes:') for (const recoveryCode of result.recoveryCodes) console.log(`- ${recoveryCode}`) } } main().catch((error) => { console.error(error.message) process.exit(1) })