[OWL] Security fixes: remove hardcoded password, add setup form, soft-delete migration

- Replace hardcoded admin123 with user-defined password via /setup form
- Add proper GET/POST /setup routes
- Create setup.ejs view with password validation (min 6 chars)
- Add migration 003: soft-delete columns (deleted_at) on loads/payments/shippers/vehicles
- Add load_count column to shippers
- requireRole middleware already present in auth.js
This commit is contained in:
FreightDesk 2026-06-07 19:34:22 +00:00
parent 66da3e32f8
commit 5e10afebf1
3 changed files with 97 additions and 8 deletions

View file

@ -0,0 +1,24 @@
-- ============================================================
-- FreightDesk — Migration 003: Soft Delete + Security
-- ============================================================
-- Add soft-delete columns
ALTER TABLE loads ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE payments ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE shippers ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
ALTER TABLE vehicles ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
-- Add role column to portal_users if not exists
ALTER TABLE portal_users ADD COLUMN IF NOT EXISTS role TEXT DEFAULT 'admin';
-- Add index for soft-delete queries
CREATE INDEX IF NOT EXISTS idx_loads_deleted_at ON loads(deleted_at) WHERE deleted_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_payments_deleted_at ON payments(deleted_at) WHERE deleted_at IS NULL;
-- Add load_count to shippers for quick reference
ALTER TABLE shippers ADD COLUMN IF NOT EXISTS load_count INTEGER DEFAULT 0;
-- Update load_count for existing shippers
UPDATE shippers SET load_count = (
SELECT COUNT(*) FROM loads WHERE loads.shipper_id = shippers.id
);

View file

@ -145,26 +145,45 @@ app.get('/logout', (req, res) => {
}); });
app.get('/setup', asyncHandler(async (req, res) => { app.get('/setup', asyncHandler(async (req, res) => {
// Check if admin exists // Check if any user exists
const { count } = await supabase const { count } = await supabase
.from('portal_users') .from('portal_users')
.select('*', { count: 'exact', head: true }) .select('*', { count: 'exact', head: true });
.eq('username', 'admin');
if (count > 0) { if (count > 0) {
return res.redirect('/login'); return res.redirect('/login');
} }
// Create default admin res.render('pages/setup', { error: null });
const hash = await bcrypt.hash('admin123', 10); }));
await supabase.from('portal_users').insert({
username: 'admin', 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, password_hash: hash,
role: 'admin', role: 'admin',
is_active: true, is_active: true,
}); });
res.send('<h1>Admin created!</h1><p>Username: <strong>admin</strong></p><p>Password: <strong>admin123</strong></p><p><a href="/login">Go to login</a></p>'); if (error) {
return res.render('pages/setup', { error: 'Failed to create admin. ' + error.message });
}
res.redirect('/login');
})); }));
// ============================================================ // ============================================================

View file

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Setup — <%= appName %></title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+Devanagari:wght@400;600;700&family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/css/style.css">
</head>
<body class="auth-page">
<div class="login-page">
<div class="login-container">
<div class="login-header">
<div class="login-emblem">&#127760;</div>
<h1 class="login-title-hi"><%= appNameHi %></h1>
<h2 class="login-title-en"><%= appName %> — Initial Setup</h2>
<p class="login-tagline">Create your admin account to get started</p>
</div>
<% if (typeof error !== 'undefined' && error) { %>
<div class="alert alert-error"><%= error %></div>
<% } %>
<form method="POST" action="/setup" class="login-form">
<input type="hidden" name="_csrf" value="<%= _csrf %>">
<div class="form-group">
<label class="form-label">Admin Username</label>
<input type="text" name="username" class="form-input" required autofocus placeholder="Choose a username" minlength="3">
</div>
<div class="form-group">
<label class="form-label">Admin Password</label>
<input type="password" name="password" class="form-input" required placeholder="Choose a strong password" minlength="6">
<p class="text-muted" style="font-size:12px;margin-top:4px;">Minimum 6 characters</p>
</div>
<button type="submit" class="btn btn-primary btn-block">Create Admin Account</button>
</form>
<div class="login-footer">
<div class="footer-tricolor"><span></span><span></span><span></span></div>
<p>Secured by Government of India</p>
</div>
</div>
</div>
<script src="/js/app.js"></script>
</body>
</html>