CI - Test & Build / Lint & Type Check (push) Canceled after 0s
CI - Test & Build / Backend Unit Tests (push) Canceled after 0s
CI - Test & Build / Frontend Build (push) Canceled after 0s
CI - Test & Build / Security Scan (push) Canceled after 0s
CI - Test & Build / E2E Tests (Playwright) (push) Canceled after 0s
CI - Test & Build / CI Summary (push) Canceled after 0s
* feat: move login credentials to a DB-backed users table with an admin management page Replaces the hardcoded AUTH_USERNAME/AUTH_PASSWORD login check with a new auth_accounts table (bcrypt-hashed passwords, admin/user roles). Adds admin-only /api/users CRUD routes and a "Brugere" admin page in the frontend for managing logins without redeploying. Removes the unused, unmounted duplicate login route in src/routes/auth.js. * docs: add architecture codemaps with diagrams for the whole system Adds codemaps/architecture.md, backend.md, frontend.md, and data.md — Mermaid-diagrammed design documentation verified against the live codebase and database rather than assumed from CLAUDE.md. Covers the unified-server.js request flow (mounted routers + ~183 inline routes), 68 backend services grouped by domain, the frontend's state-driven view-switch (no React Router in practice despite BrowserRouter being present), and the full 122-table DB schema with the auth_accounts vs unrelated users table naming trap flagged explicitly. Links added from the root README. Co-Authored-By: Claude Sonnet 5 <[email protected]> * feat: ship canonical roof quote workflow * fix: keep migration dry-run idempotent * [verified] feat: complete Smart Pakker management * [verified] fix: ignore blank task dependencies * [verified] fix: align package duplication with schema * [verified] fix: enforce Discord status limits * [verified] fix: link Smart Pakke materials safely * [verified] fix: harden material link review * [verified] feat: improve material matching * fix: scope pitch validation to roof packages * fix: support canonical snapshots on production schema * [verified] fix: hide internal package metadata from PDF * [verified] feat: deliver sales-ready customer PDF * [verified] feat: ship sales-ready PDF with AI overview * [verified] fix: authenticate project list requests * [verified] fix: refresh project-list authentication * [verified] fix: open existing project details * [verified] fix: keep roof components searchable in builder * [verified] fix: expose all Smart Package categories * [verified] fix: authenticate project creation * [verified] feat: make Smart Pakker the universal project flow * [verified] feat: preview Smart Package contents * [verified] test: keep generic release isolated from downpipe work * feat: add first-class Smart Pakke rentals * [verified] feat: add gutter and downpipe smart packages * [verified] fix: prepare six-house gutter quote flow * [verified] fix: open generic quotes without roof geometry * [verified] fix: review generic quotes with authenticated APIs * [verified] fix: calculate generic Smart Package quotes * [verified] fix: return generic calculation breakdown * feat: checkpoint generic signed snapshot validation with red-green tests * feat: complete fail-closed generic quote approval and customer PDF flow * feat: use generic signed snapshot in final review * feat: redesign generic quote final review * fix: harden generic review summaries * feat: add auditable six-house package basis * [verified] feat: finish auditable Smart Pakke UI * [verified] fix: bind auditable quantity and price bases * [verified] fix: keep six-house basis across package versions * [verified] fix: complete smart package discovery management * [verified] fix: simplify composition and generic scope * [verified] test: keep explicit roof contracts fail closed * [verified] fix: harden generic quote snapshots * fix: make generic quote delivery customer safe * [verified] fix: secure package catalog reads * [verified] fix: close workspace provenance blockers * fix: harden customer document language boundary * [verified] fix: secure smart package internal reads * fix: version package child mutations atomically * feat: add generic customer quote text flow * [verified] fix: allow manual customer numbers * [verified] fix: expose optional roof geometry * [verified] fix: rebase hydrated packages after geometry edits * [verified] feat: add free editable site area map * [verified] fix: harden map recovery and geocoding gate * fix: bind map quantities to authoritative geometry * fix: release geocoder lock before dispatch * fix: separate roof and site geometry provenance * fix: revoke stale admin authorization * fix: migrate task geometry basis * fix: make backend CI dependency-complete * ci: seed isolated e2e login account * fix: allow clean database bootstrap * fix: skip indexes for optional tables * test: use canonical mansard geometry in e2e * [verified] fix(auth): enforce live operator boundary * fix: fail close Ordrestyring offer transport * fix(frontend): authenticate customer project requests * fix: align canonical roof type contract * [verified] fix: reconcile legacy package labor safely * [verified] fix: audit site geometry deletion * docs: add PR 31 reviewer guide * docs: synchronize Obsidian vault * docs: sync integrated reviewer guide to Obsidian * ci: seed isolated auth account explicitly * fix: close offer bootstrap and service readiness gaps * fix: authenticate protected package callers * fix: provision initial admin and disable generic send * [verified] fix: close final quote release blockers * [verified] fix: seed gutter packages before deployment --------- Co-authored-by: alexpolo1 <[email protected]> Co-authored-by: Claude Sonnet 5 <[email protected]>
168 lines
4.9 KiB
JavaScript
168 lines
4.9 KiB
JavaScript
const jwt = require('jsonwebtoken');
|
|
|
|
// JWT secrets - REQUIRED in production, fail-fast if not set
|
|
const JWT_ACCESS_SECRET = process.env.JWT_ACCESS_SECRET;
|
|
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
|
|
|
|
// Fail-fast: Require both secrets to be set at startup
|
|
if (!JWT_ACCESS_SECRET) {
|
|
throw new Error('FATAL: JWT_ACCESS_SECRET environment variable is required. Set it in your .env file.');
|
|
}
|
|
if (!JWT_REFRESH_SECRET) {
|
|
throw new Error('FATAL: JWT_REFRESH_SECRET environment variable is required. Set it in your .env file.');
|
|
}
|
|
|
|
// Token expiry configuration
|
|
const JWT_EXPIRY = process.env.JWT_EXPIRY || '24h';
|
|
const REFRESH_TOKEN_EXPIRY = process.env.REFRESH_TOKEN_EXPIRY || '7d';
|
|
|
|
/**
|
|
* SECURITY NOTES:
|
|
* - Access tokens should be short-lived (default: 24h)
|
|
* - Refresh tokens should be stored server-side (database) or as HttpOnly cookies
|
|
* - Refresh tokens should be rotated on each use to prevent replay attacks
|
|
* - Never store refresh tokens in localStorage or any client-side storage
|
|
* - Consider implementing refresh token families for better security
|
|
*/
|
|
|
|
/**
|
|
* Middleware to verify JWT token
|
|
*/
|
|
function verifyToken(req, res, next) {
|
|
const authHeader = req.headers['authorization'];
|
|
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
|
|
|
|
if (!token) {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: 'Access token required'
|
|
});
|
|
}
|
|
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_ACCESS_SECRET);
|
|
req.user = decoded;
|
|
next();
|
|
} catch (error) {
|
|
if (error.name === 'TokenExpiredError') {
|
|
return res.status(401).json({
|
|
success: false,
|
|
message: 'Token expired',
|
|
expired: true
|
|
});
|
|
}
|
|
return res.status(403).json({
|
|
success: false,
|
|
message: 'Invalid token'
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate access token
|
|
*/
|
|
function generateAccessToken(user) {
|
|
return jwt.sign(
|
|
{
|
|
username: user.username,
|
|
id: user.id
|
|
},
|
|
JWT_ACCESS_SECRET,
|
|
{ expiresIn: JWT_EXPIRY }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Generate refresh token
|
|
* NOTE: Refresh tokens MUST be stored server-side (in database) or as HttpOnly cookies.
|
|
* They should be rotated on each use to prevent replay attacks.
|
|
*/
|
|
function generateRefreshToken(user) {
|
|
return jwt.sign(
|
|
{
|
|
username: user.username,
|
|
id: user.id,
|
|
type: 'refresh'
|
|
},
|
|
JWT_REFRESH_SECRET,
|
|
{ expiresIn: REFRESH_TOKEN_EXPIRY }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Verify refresh token and return user data
|
|
* NOTE: In production, refresh tokens should be validated against a server-side store
|
|
* and rotated on each use.
|
|
*/
|
|
function verifyRefreshToken(token) {
|
|
try {
|
|
const decoded = jwt.verify(token, JWT_REFRESH_SECRET);
|
|
if (decoded.type !== 'refresh') {
|
|
throw new Error('Invalid token type');
|
|
}
|
|
return decoded;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Middleware to require an admin role. Must run after verifyToken.
|
|
*/
|
|
async function requireAdmin(req, res, next) {
|
|
try {
|
|
const userService = require('../services/userService');
|
|
const current = req.user?.username ? await userService.findByUsername(req.user.username) : null;
|
|
if (!current || Number(current.id) !== Number(req.user?.id) || current.username !== req.user?.username || current.role !== 'admin') {
|
|
return res.status(403).json({
|
|
success: false,
|
|
message: 'Admin access required'
|
|
});
|
|
}
|
|
req.user = { ...req.user, id: current.id, username: current.username, role: current.role };
|
|
next();
|
|
} catch (_error) {
|
|
return res.status(503).json({
|
|
success: false,
|
|
message: 'Admin authorization unavailable'
|
|
});
|
|
}
|
|
}
|
|
|
|
async function findConfiguredOperator(user) {
|
|
const configuredUsername = String(process.env.AUTH_USERNAME || '').trim();
|
|
if (!configuredUsername || user?.username !== configuredUsername) return null;
|
|
const current = await require('../services/userService').findByUsername(user.username);
|
|
if (!current || current.username !== user.username
|
|
|| Number(current.id) !== Number(user.id) || current.role !== 'admin') return null;
|
|
return current;
|
|
}
|
|
|
|
// Must follow verifyToken. Never trust roles or identity from a stale JWT.
|
|
async function requireConfiguredOperator(req, res, next) {
|
|
const message = res.locals.operatorDeniedMessage || 'Ingen adgang til projektdata';
|
|
const denied = () => res.status(403).json({
|
|
success: false,
|
|
error: message,
|
|
...(res.locals.operatorDeniedCode ? { code: res.locals.operatorDeniedCode } : {})
|
|
});
|
|
try {
|
|
const current = await findConfiguredOperator(req.user);
|
|
if (!current) return denied();
|
|
req.user = { ...req.user, id: current.id, username: current.username, role: current.role };
|
|
return next();
|
|
} catch (_error) {
|
|
return res.status(503).json({ success: false, error: 'Operator authorization unavailable' });
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
verifyToken,
|
|
requireAdmin,
|
|
findConfiguredOperator,
|
|
requireConfiguredOperator,
|
|
generateAccessToken,
|
|
generateRefreshToken,
|
|
verifyRefreshToken
|
|
};
|