admin login fixed.
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
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
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const bcrypt = require('bcryptjs')
|
||||
const path = require('node:path')
|
||||
const {
|
||||
ensureDatabaseUrl,
|
||||
loadDefaultEnvFiles,
|
||||
normalizeDatabaseUrlForExecution,
|
||||
} = require('../packages/database/src/runtime-config')
|
||||
|
||||
const ADMIN_ROLES = new Set(['SUPER_ADMIN', 'ADMIN', 'SUPPORT', 'FINANCE', 'VIEWER'])
|
||||
|
||||
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(argName, envName, fallback) {
|
||||
return readArg(argName) ?? process.env[envName] ?? fallback
|
||||
}
|
||||
|
||||
function requireValue(argName, envName) {
|
||||
const value = readValue(argName, envName)
|
||||
if (!value || !value.trim()) {
|
||||
throw new Error(`Missing required value. Provide --${argName}=... or ${envName}=...`)
|
||||
}
|
||||
return value.trim()
|
||||
}
|
||||
|
||||
function readBoolean(argName, envName, fallback = false) {
|
||||
const value = readValue(argName, envName)
|
||||
if (value === undefined) return fallback
|
||||
return ['1', 'true', 'yes', 'y'].includes(String(value).toLowerCase())
|
||||
}
|
||||
|
||||
async function main() {
|
||||
loadDefaultEnvFiles(path.resolve(__dirname, '..'))
|
||||
ensureDatabaseUrl()
|
||||
normalizeDatabaseUrlForExecution()
|
||||
|
||||
const email = requireValue('email', 'ADMIN_EMAIL').toLowerCase()
|
||||
const password = requireValue('password', 'ADMIN_PASSWORD')
|
||||
const firstName = readValue('first-name', 'ADMIN_FIRST_NAME', 'Admin').trim()
|
||||
const lastName = readValue('last-name', 'ADMIN_LAST_NAME', 'User').trim()
|
||||
const role = readValue('role', 'ADMIN_ROLE', 'SUPER_ADMIN').trim().toUpperCase()
|
||||
const updateExisting = readBoolean('update-existing', 'ADMIN_UPDATE_EXISTING', false)
|
||||
|
||||
if (!ADMIN_ROLES.has(role)) {
|
||||
throw new Error(`Invalid ADMIN_ROLE "${role}". Use one of: ${Array.from(ADMIN_ROLES).join(', ')}`)
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
throw new Error('Admin password must be at least 8 characters long.')
|
||||
}
|
||||
|
||||
const { PrismaClient } = require('../packages/database/generated')
|
||||
const prisma = new PrismaClient()
|
||||
try {
|
||||
const existing = await prisma.adminUser.findUnique({ where: { email } })
|
||||
const passwordHash = await bcrypt.hash(password, 12)
|
||||
|
||||
if (existing) {
|
||||
if (!updateExisting) {
|
||||
console.log(`Admin user already exists: ${email}`)
|
||||
console.log('Set ADMIN_UPDATE_EXISTING=true or pass --update-existing=true to update the name, role, password, and active status.')
|
||||
return
|
||||
}
|
||||
|
||||
const admin = await prisma.adminUser.update({
|
||||
where: { email },
|
||||
data: {
|
||||
firstName,
|
||||
lastName,
|
||||
role,
|
||||
passwordHash,
|
||||
isActive: true,
|
||||
passwordResetToken: null,
|
||||
passwordResetExpiresAt: null,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`Updated ${admin.role}: ${admin.email} (id: ${admin.id})`)
|
||||
return
|
||||
}
|
||||
|
||||
const admin = await prisma.adminUser.create({
|
||||
data: {
|
||||
email,
|
||||
firstName,
|
||||
lastName,
|
||||
passwordHash,
|
||||
role,
|
||||
isActive: true,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(`Created ${admin.role}: ${admin.email} (id: ${admin.id})`)
|
||||
} finally {
|
||||
await prisma.$disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.message || error)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -52,6 +52,23 @@ function normalizeApiBase(value) {
|
||||
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]
|
||||
@@ -71,10 +88,23 @@ function cookieHeader(cookieJar) {
|
||||
.join('; ')
|
||||
}
|
||||
|
||||
async function request(apiBase, cookieJar, pathName, options = {}) {
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -102,12 +132,13 @@ 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)
|
||||
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_SEED_PASSWORD in .env.local or in the command environment.')
|
||||
throw new Error('Set ADMIN_PASSWORD or ADMIN_SEED_PASSWORD, or pass --password=...')
|
||||
}
|
||||
|
||||
const cookieJar = new Map()
|
||||
@@ -115,7 +146,7 @@ async function main() {
|
||||
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
|
||||
@@ -125,14 +156,14 @@ async function main() {
|
||||
throw new Error(login.json?.message || `Admin login failed: ${error}`)
|
||||
}
|
||||
|
||||
const me = await request(apiBase, cookieJar, '/admin/auth/me')
|
||||
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' })
|
||||
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.')
|
||||
}
|
||||
@@ -144,7 +175,7 @@ async function main() {
|
||||
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.')
|
||||
|
||||
Reference in New Issue
Block a user