168 lines
5.0 KiB
JavaScript
168 lines
5.0 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 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('; ')
|
|
}
|
|
|
|
async function request(apiBase, cookieJar, pathName, options = {}) {
|
|
const headers = {
|
|
...(options.body ? { 'Content-Type': 'application/json' } : {}),
|
|
...(cookieJar.size ? { Cookie: cookieHeader(cookieJar) } : {}),
|
|
...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 = env.ADMIN_SEED_EMAIL || 'admin@rentaldrivego.ma'
|
|
const password = env.ADMIN_SEED_PASSWORD
|
|
const apiBase = normalizeApiBase(env.NEXT_PUBLIC_API_URL || env.API_URL)
|
|
|
|
if (!password || /^(placeholder|changeme|change-me)$/i.test(password)) {
|
|
throw new Error('Set ADMIN_SEED_PASSWORD in .env.local or in the command environment.')
|
|
}
|
|
|
|
const cookieJar = new Map()
|
|
|
|
const login = await request(apiBase, cookieJar, '/admin/auth/login', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ email, password }),
|
|
})
|
|
|
|
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')
|
|
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' })
|
|
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 }),
|
|
})
|
|
|
|
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)
|
|
})
|