Files
carmanagement/scripts/enroll-admin-2fa.cjs
T
root 50c74b9007
Build & Push / Pipeline Tests (push) Failing after 1m34s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Successful in 56s
Test / API Unit Tests (push) Successful in 1m8s
Test / Homepage Unit Tests (push) Successful in 47s
Test / Carplace Unit Tests (push) Successful in 43s
Test / Admin Unit Tests (push) Successful in 46s
Test / Dashboard Unit Tests (push) Failing after 43s
Test / API Integration Tests (push) Successful in 1m8s
admin login fixed.
2026-07-28 22:56:18 -04:00

199 lines
6.1 KiB
JavaScript

#!/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)
})