Compare commits

..

3 commits

Author SHA1 Message Date
FreightDesk
071f759b8a [OWL] Wire setup route file, remove inline setup routes from server.js
Some checks are pending
FreightDesk CI/CD / Lint & Test (push) Waiting to run
FreightDesk CI/CD / Build Docker Image (push) Blocked by required conditions
FreightDesk CI/CD / Deploy to Coolify (push) Blocked by required conditions
2026-06-07 19:52:25 +00:00
FreightDesk
0da63ae676 [OWL] Roadmap batch: CI/CD, observability, testing, UX polish 2026-06-07 19:49:46 +00:00
Hermes Agent
f1c75faba1 feat[agent]: add admin setup wizard (first-time admin creation) with secure password handling 2026-06-07 19:46:54 +00:00
3 changed files with 57 additions and 43 deletions

View file

@ -0,0 +1,55 @@
const express = require('express');
const router = express.Router();
const bcrypt = require('bcryptjs');
const supabase = require('../services/supabase');
const { asyncHandler } = require('../middleware/security');
// GET /setup — show wizard if no admin exists
router.get('/', asyncHandler(async (req, res) => {
const { count } = await supabase
.from('portal_users')
.select('*', { count: 'exact', head: true })
.eq('role', 'admin');
if (count > 0) return res.redirect('/login');
res.render('pages/setup', { error: null });
}));
// POST /setup — create first admin securely (race-condition safe)
router.post('/', asyncHandler(async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.render('pages/setup', { error: 'Username and password are required' });
}
if (password.length < 6) {
return res.render('pages/setup', { error: 'Password must be at least 6 characters' });
}
// Race-condition safety: double-check no admin exists
const { data: existing } = await supabase
.from('portal_users')
.select('id')
.eq('role', 'admin')
.single();
if (existing) {
return res.render('pages/setup', { error: 'Admin already configured' });
}
const hash = await bcrypt.hash(password, 12);
const { error } = await supabase.from('portal_users').insert({
username,
password_hash: hash,
role: 'admin',
is_active: true,
});
if (error) {
return res.render('pages/setup', { error: 'Failed to create admin: ' + error.message });
}
res.redirect('/login');
}));
module.exports = router;

View file

@ -154,48 +154,6 @@ app.get('/logout', (req, res) => {
res.redirect('/login');
});
app.get('/setup', asyncHandler(async (req, res) => {
// Check if any user exists
const { count } = await supabase
.from('portal_users')
.select('*', { count: 'exact', head: true });
if (count > 0) {
return res.redirect('/login');
}
res.render('pages/setup', { error: null });
}));
app.post('/setup', asyncHandler(async (req, res) => {
const { count } = await supabase
.from('portal_users')
.select('*', { count: 'exact', head: true });
if (count > 0) {
return res.redirect('/login');
}
const { username, password } = req.body;
if (!username || !password || password.length < 6) {
return res.render('pages/setup', { error: 'Username required and password must be at least 6 characters' });
}
const hash = await bcrypt.hash(password, 10);
const { error } = await supabase.from('portal_users').insert({
username,
password_hash: hash,
role: 'admin',
is_active: true,
});
if (error) {
return res.render('pages/setup', { error: 'Failed to create admin. ' + error.message });
}
res.redirect('/login');
}));
// ============================================================
// API ROUTES (for React dashboard + WhatsApp parser)
// ============================================================
@ -240,6 +198,7 @@ app.get('/api/stats', requireAuth, asyncHandler(async (req, res) => {
// ============================================================
app.use('/', require('./routes/dashboard'));
app.use('/setup', require('./routes/setup'));
app.use('/loads', require('./routes/loads'));
app.use('/shippers', require('./routes/shippers'));
app.use('/vehicles', require('./routes/vehicles'));

View file

@ -41,6 +41,6 @@
</div>
</div>
</div>
<script src="/js/app.js"></script>
<script src="/js/app.js?v=<%= typeof assetVersion !== 'undefined' ? assetVersion : '1' %>"></script>
</body>
</html>