2026-05-03 19:51:57 -05:00
const express = require ( 'express' ) ;
const router = express . Router ( ) ;
2026-05-14 21:00:07 -05:00
let _appVersion ;
function getAppVersion ( ) {
if ( ! _appVersion ) {
try { _appVersion = require ( '../package.json' ) . version ; } catch { _appVersion = '0.0.0' ; }
}
return _appVersion ;
}
2026-05-03 19:51:57 -05:00
const { getDb , getSetting , setSetting } = require ( '../db/database' ) ;
2026-06-04 03:53:38 -05:00
const { login , logout , hashPassword , cookieOpts , COOKIE _NAME , SINGLE _COOKIE _NAME , rotateSessionId , invalidateOtherSessions , recordLogin , recordFailedLogin } = require ( '../services/authService' ) ;
2026-06-04 03:38:32 -05:00
const { decryptSecret } = require ( '../services/encryptionService' ) ;
2026-05-31 15:52:50 -05:00
const { getCsrfToken } = require ( '../middleware/csrf' ) ;
2026-05-03 19:51:57 -05:00
const { requireAuth , requireAdmin } = require ( '../middleware/requireAuth' ) ;
const { getPublicOidcInfo } = require ( '../services/oidcService' ) ;
2026-05-09 13:03:36 -05:00
const { ValidationError , formatError } = require ( '../utils/apiError' ) ;
const { standardizeError } = require ( '../middleware/errorFormatter' ) ;
const { passwordLimiter } = require ( '../middleware/rateLimiter' ) ;
2026-05-10 00:03:12 -05:00
const { logAudit } = require ( '../services/auditService' ) ;
2026-05-03 19:51:57 -05:00
// ─────────────────────────────────────────
// PUBLIC AUTH ROUTES
// ─────────────────────────────────────────
// POST /api/auth/login
2026-05-09 13:03:36 -05:00
router . post ( '/login' , ( req , res , next ) => {
// Exempt login from CSRF - no session exists yet to hijack
// CSRF validation happens on all other authenticated routes
req . csrfSkip = true ;
next ( ) ;
} , async ( req , res ) => {
2026-05-03 19:51:57 -05:00
// Respect admin-configured login method toggle
if ( getSetting ( 'local_login_enabled' ) === 'false' ) {
2026-05-09 13:03:36 -05:00
return res . status ( 403 ) . json ( standardizeError ( 'Local username/password login is not enabled on this server.' , 'FORBIDDEN' ) ) ;
2026-05-03 19:51:57 -05:00
}
const { username , password } = req . body ;
if ( ! username || ! password ) {
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'Username and password are required' , 'VALIDATION_ERROR' , ! username ? 'username' : 'password' ) ) ;
2026-05-03 19:51:57 -05:00
}
2026-05-10 12:20:50 -05:00
try {
const result = await login ( username , password ) ;
2026-06-04 03:38:32 -05:00
if ( ! result || result . error ) {
2026-05-10 12:20:50 -05:00
logAudit ( { user _id : null , action : 'login.failure' , details : { username } , ip _address : req . ip , user _agent : req . get ( 'user-agent' ) } ) ;
2026-06-04 03:38:32 -05:00
// Track failed attempt against known accounts (wrong password only — not unknown usernames)
if ( result ? . error === 'bad_password' ) {
recordFailedLogin ( result . userId , req . ip , req . get ( 'user-agent' ) ) ;
}
2026-05-10 12:20:50 -05:00
return res . status ( 401 ) . json ( standardizeError ( 'Invalid username or password' , 'AUTH_ERROR' ) ) ;
}
2026-05-03 19:51:57 -05:00
2026-06-04 04:10:14 -05:00
// TOTP required — don't create a session yet
if ( result . requires _totp ) {
return res . json ( { requires _totp : true , challenge _token : result . challenge _token } ) ;
}
2026-05-10 12:20:50 -05:00
logAudit ( { user _id : result . user . id , action : 'login.success' , ip _address : req . ip , user _agent : req . get ( 'user-agent' ) } ) ;
2026-06-04 03:38:32 -05:00
recordLogin ( result . user . id , req . ip , req . get ( 'user-agent' ) , result . sessionId ) ;
2026-05-10 00:03:12 -05:00
2026-05-10 12:20:50 -05:00
res . cookie ( COOKIE _NAME , result . sessionId , cookieOpts ( req ) ) ;
res . json ( { user : result . user } ) ;
} catch ( err ) {
console . error ( 'Login error:' , err ) ;
res . status ( 500 ) . json ( standardizeError ( 'Login failed' , 'SERVER_ERROR' ) ) ;
}
2026-05-03 19:51:57 -05:00
} ) ;
2026-05-31 15:52:50 -05:00
// GET /api/auth/csrf-token
// Public — returns the CSRF token from the httpOnly cookie so the SPA can
// store it in memory and send it in the x-csrf-token header for mutations.
// Cross-origin access is prevented by the same-origin fetch policy (no CORS).
router . get ( '/csrf-token' , ( req , res ) => {
const token = getCsrfToken ( req , res ) ;
res . json ( { token } ) ;
} ) ;
2026-05-03 19:51:57 -05:00
// POST /api/auth/logout
router . post ( '/logout' , requireAuth , ( req , res ) => {
logout ( req . cookies ? . [ COOKIE _NAME ] ) ;
2026-05-10 00:03:12 -05:00
logAudit ( { user _id : req . user . id , action : 'logout' , ip _address : req . ip , user _agent : req . get ( 'user-agent' ) } ) ;
2026-05-03 19:51:57 -05:00
res . clearCookie ( COOKIE _NAME , { path : '/' , ... cookieOpts ( req ) , maxAge : undefined } ) ;
res . json ( { success : true } ) ;
} ) ;
2026-05-10 03:55:14 -05:00
// POST /api/auth/logout-all
router . post ( '/logout-all' , requireAuth , ( req , res ) => {
// Delete ALL sessions for this user
invalidateOtherSessions ( req . user . id , null ) ; // null means delete all sessions
// Also clear the current session
logout ( req . cookies ? . [ COOKIE _NAME ] ) ;
logAudit ( { user _id : req . user . id , action : 'logout.all' , ip _address : req . ip , user _agent : req . get ( 'user-agent' ) } ) ;
res . clearCookie ( COOKIE _NAME , { path : '/' , ... cookieOpts ( req ) , maxAge : undefined } ) ;
res . json ( { success : true } ) ;
} ) ;
2026-05-03 19:51:57 -05:00
// GET /api/auth/me
router . get ( '/me' , requireAuth , ( req , res ) => {
2026-05-14 21:00:07 -05:00
const currentVersion = getAppVersion ( ) ;
2026-05-03 19:51:57 -05:00
res . json ( {
user : req . user ,
single _user _mode : ! ! req . singleUserMode ,
2026-05-14 21:00:07 -05:00
current _version : currentVersion ,
2026-05-15 22:45:38 -05:00
release _notes _version : currentVersion ,
2026-05-14 21:00:07 -05:00
has _new _version : req . user . last _seen _version !== currentVersion ,
2026-05-03 19:51:57 -05:00
} ) ;
} ) ;
2026-06-04 03:38:32 -05:00
// GET /api/auth/login-history — last 10 logins for the authenticated user (encrypted at rest, decrypted here)
2026-05-15 01:36:56 -05:00
router . get ( '/login-history' , requireAuth , ( req , res ) => {
const db = getDb ( ) ;
2026-06-04 03:38:32 -05:00
const rows = db . prepare ( `
2026-05-15 22:45:38 -05:00
SELECT id , logged _in _at , ip _address , user _agent ,
2026-06-04 03:38:32 -05:00
browser , os , device _type , device _fingerprint , session _fingerprint ,
success , location _city , location _country , location _region , location _isp
2026-05-15 01:36:56 -05:00
FROM user _login _history
WHERE user _id = ?
ORDER BY logged _in _at DESC
2026-06-04 03:38:32 -05:00
LIMIT 10
2026-05-15 01:36:56 -05:00
` ).all(req.user.id);
2026-06-04 03:38:32 -05:00
const safeDecrypt = v => {
if ( ! v ) return null ;
try { return decryptSecret ( v ) ; } catch { return null ; }
} ;
2026-06-04 03:53:38 -05:00
// Compute fingerprint of the current session cookie to mark "this session".
// Single-user mode has no COOKIE_NAME — use the presence cookie instead.
const currentCookie = req . singleUserMode
? req . cookies ? . [ SINGLE _COOKIE _NAME ]
: req . cookies ? . [ COOKIE _NAME ] ;
2026-06-04 03:38:32 -05:00
const currentFingerprint = currentCookie
? require ( 'crypto' ) . createHash ( 'sha256' ) . update ( currentCookie ) . digest ( 'hex' ) . slice ( 0 , 32 )
: null ;
const history = rows . map ( r => ( {
id : r . id ,
logged _in _at : r . logged _in _at ,
ip _address : safeDecrypt ( r . ip _address ) ,
user _agent : safeDecrypt ( r . user _agent ) ,
browser : r . browser ,
os : r . os ,
device _type : r . device _type ,
device _fingerprint : r . device _fingerprint ,
success : r . success !== 0 ,
is _current _session : ! ! ( currentFingerprint && r . session _fingerprint === currentFingerprint ) ,
location _city : safeDecrypt ( r . location _city ) ,
location _country : safeDecrypt ( r . location _country ) ,
location _region : safeDecrypt ( r . location _region ) ,
location _isp : safeDecrypt ( r . location _isp ) ,
} ) ) ;
2026-05-15 01:36:56 -05:00
res . json ( { history } ) ;
} ) ;
2026-06-04 04:10:14 -05:00
// ── TOTP / Authenticator App ─────────────────────────────────────────────────
const {
generateSecret , generateQrCode , verifyToken , verifyTokenRaw ,
generateRecoveryCodes , hashRecoveryCode , consumeRecoveryCode ,
createChallenge , consumeChallenge ,
} = require ( '../services/totpService' ) ;
const { encryptSecret : encTotpSecret } = require ( '../services/encryptionService' ) ;
// POST /api/auth/totp/challenge — second step of login when TOTP is enabled.
// Takes challenge_token (from first login step) + totp_code, creates a session.
router . post ( '/totp/challenge' , async ( req , res ) => {
req . csrfSkip = true ;
const { challenge _token , code , recovery _code } = req . body || { } ;
if ( ! challenge _token ) return res . status ( 400 ) . json ( standardizeError ( 'challenge_token is required' , 'VALIDATION_ERROR' ) ) ;
const db = getDb ( ) ;
const userId = consumeChallenge ( db , challenge _token ) ;
if ( ! userId ) return res . status ( 401 ) . json ( standardizeError ( 'Challenge expired or invalid. Please sign in again.' , 'AUTH_ERROR' ) ) ;
const user = db . prepare ( 'SELECT * FROM users WHERE id = ? AND active = 1' ) . get ( userId ) ;
if ( ! user ) return res . status ( 401 ) . json ( standardizeError ( 'User not found.' , 'AUTH_ERROR' ) ) ;
let verified = false ;
if ( recovery _code ) {
const result = consumeRecoveryCode ( db , userId , recovery _code ) ;
verified = result . used ;
if ( verified && result . remaining === 0 ) {
// Warn but don't block — they're in, but should regenerate codes
}
} else if ( code ) {
verified = verifyToken ( user . totp _secret , code ) ;
}
if ( ! verified ) {
logAudit ( { user _id : userId , action : 'totp.failure' , ip _address : req . ip , user _agent : req . get ( 'user-agent' ) } ) ;
return res . status ( 401 ) . json ( standardizeError ( 'Invalid authenticator code.' , 'AUTH_ERROR' ) ) ;
}
try {
const { createSession } = require ( '../services/authService' ) ;
const session = await createSession ( userId ) ;
if ( ! session ) return res . status ( 500 ) . json ( standardizeError ( 'Failed to create session' , 'SERVER_ERROR' ) ) ;
logAudit ( { user _id : userId , action : 'login.success' , ip _address : req . ip , user _agent : req . get ( 'user-agent' ) } ) ;
recordLogin ( userId , req . ip , req . get ( 'user-agent' ) , session . sessionId ) ;
res . cookie ( COOKIE _NAME , session . sessionId , cookieOpts ( req ) ) ;
res . json ( { user : session . user } ) ;
} catch ( err ) {
console . error ( '[totp/challenge]' , err ) ;
res . status ( 500 ) . json ( standardizeError ( 'Login failed' , 'SERVER_ERROR' ) ) ;
}
} ) ;
// GET /api/auth/totp/setup — generate a new pending secret + QR code for the authenticated user.
// The secret is NOT saved yet; the user must confirm a valid code via /totp/enable.
router . get ( '/totp/setup' , requireAuth , async ( req , res ) => {
if ( req . singleUserMode ) return res . status ( 400 ) . json ( standardizeError ( 'TOTP is not available in single-user mode.' , 'VALIDATION_ERROR' ) ) ;
try {
const secret = generateSecret ( ) ;
const user = getDb ( ) . prepare ( 'SELECT username FROM users WHERE id = ?' ) . get ( req . user . id ) ;
const { uri , qr _data _url } = await generateQrCode ( secret , user . username ) ;
res . json ( { secret , uri , qr _data _url } ) ;
} catch ( err ) {
console . error ( '[totp/setup]' , err ) ;
res . status ( 500 ) . json ( standardizeError ( 'Failed to generate setup data' , 'SERVER_ERROR' ) ) ;
}
} ) ;
// POST /api/auth/totp/enable — verify a code against the submitted secret, then enable TOTP.
router . post ( '/totp/enable' , requireAuth , ( req , res ) => {
if ( req . singleUserMode ) return res . status ( 400 ) . json ( standardizeError ( 'TOTP is not available in single-user mode.' , 'VALIDATION_ERROR' ) ) ;
const { secret , code } = req . body || { } ;
if ( ! secret || ! code ) return res . status ( 400 ) . json ( standardizeError ( 'secret and code are required' , 'VALIDATION_ERROR' ) ) ;
if ( ! verifyTokenRaw ( secret , code ) ) return res . status ( 400 ) . json ( standardizeError ( 'Invalid authenticator code. Check your app and try again.' , 'VALIDATION_ERROR' , 'code' ) ) ;
const plainCodes = generateRecoveryCodes ( ) ;
const hashedCodes = plainCodes . map ( hashRecoveryCode ) ;
const db = getDb ( ) ;
db . prepare ( `
UPDATE users SET totp _enabled = 1 , totp _secret = ? , totp _recovery _codes = ? , updated _at = datetime ( 'now' )
WHERE id = ?
` ).run(encTotpSecret(secret), encTotpSecret(JSON.stringify(hashedCodes)), req.user.id);
logAudit ( { user _id : req . user . id , action : 'totp.enabled' , ip _address : req . ip , user _agent : req . get ( 'user-agent' ) } ) ;
res . json ( { enabled : true , recovery _codes : plainCodes } ) ;
} ) ;
// POST /api/auth/totp/disable — disable TOTP. Requires a valid TOTP code or recovery code.
router . post ( '/totp/disable' , requireAuth , ( req , res ) => {
const { code , recovery _code } = req . body || { } ;
const db = getDb ( ) ;
const user = db . prepare ( 'SELECT * FROM users WHERE id = ?' ) . get ( req . user . id ) ;
if ( ! user ? . totp _enabled ) return res . status ( 400 ) . json ( standardizeError ( 'TOTP is not enabled.' , 'VALIDATION_ERROR' ) ) ;
let verified = false ;
if ( recovery _code ) {
verified = consumeRecoveryCode ( db , req . user . id , recovery _code ) . used ;
} else if ( code ) {
verified = verifyToken ( user . totp _secret , code ) ;
}
if ( ! verified ) return res . status ( 401 ) . json ( standardizeError ( 'Invalid authenticator code.' , 'AUTH_ERROR' , 'code' ) ) ;
db . prepare ( ` UPDATE users SET totp_enabled=0, totp_secret=NULL, totp_recovery_codes=NULL, updated_at=datetime('now') WHERE id=? ` )
. run ( req . user . id ) ;
logAudit ( { user _id : req . user . id , action : 'totp.disabled' , ip _address : req . ip , user _agent : req . get ( 'user-agent' ) } ) ;
res . json ( { enabled : false } ) ;
} ) ;
// GET /api/auth/totp/status — is TOTP enabled for the current user?
router . get ( '/totp/status' , requireAuth , ( req , res ) => {
const user = getDb ( ) . prepare ( 'SELECT totp_enabled FROM users WHERE id = ?' ) . get ( req . user . id ) ;
res . json ( { enabled : ! ! user ? . totp _enabled } ) ;
} ) ;
// POST /api/auth/totp/acknowledge-version — user has seen the release notes
2026-05-14 21:00:07 -05:00
router . post ( '/acknowledge-version' , requireAuth , ( req , res ) => {
const currentVersion = getAppVersion ( ) ;
getDb ( )
. prepare ( "UPDATE users SET last_seen_version = ?, updated_at = datetime('now') WHERE id = ?" )
. run ( currentVersion , req . user . id ) ;
2026-05-15 22:45:38 -05:00
res . json ( { success : true , last _seen _version : currentVersion , release _notes _version : currentVersion } ) ;
2026-05-14 21:00:07 -05:00
} ) ;
2026-05-03 19:51:57 -05:00
// GET /api/auth/mode
// Public — tells the login page which options are available.
// Never returns secrets. local_enabled/oidc_enabled reflect admin settings.
router . get ( '/mode' , ( req , res ) => {
const oidcInfo = getPublicOidcInfo ( ) ;
const localEnabled = getSetting ( 'local_login_enabled' ) !== 'false' ;
res . json ( {
auth _mode : getSetting ( 'auth_mode' ) || 'multi' ,
local _enabled : localEnabled ,
... oidcInfo ,
} ) ;
} ) ;
// POST /api/auth/restore-multi-user-mode
// Recovery path for single-user mode. In single-user mode requireAuth attaches
// the configured default user, so this lets that Settings page restore normal
// login without needing access to Admin routes.
router . post ( '/restore-multi-user-mode' , requireAuth , ( req , res ) => {
if ( ! req . singleUserMode && getSetting ( 'auth_mode' ) !== 'single' ) {
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'Single-user mode is not enabled.' , 'VALIDATION_ERROR' , 'auth_mode' ) ) ;
2026-05-03 19:51:57 -05:00
}
setSetting ( 'auth_mode' , 'multi' ) ;
setSetting ( 'default_user_id' , '' ) ;
res . json ( { success : true , auth _mode : 'multi' } ) ;
} ) ;
// POST /api/auth/acknowledge-privacy
router . post ( '/acknowledge-privacy' , requireAuth , ( req , res ) => {
getDb ( ) . prepare (
"UPDATE users SET first_login = 0, updated_at = datetime('now') WHERE id = ?"
) . run ( req . user . id ) ;
res . json ( { success : true } ) ;
} ) ;
// POST /api/auth/change-password
2026-05-09 13:03:36 -05:00
// Password change endpoint with dedicated rate limiter
v0.25.0: roadmap redesign, import CSRF fix, AdminDashboard removed
- RoadmapPage: kanban-style priority lanes, shadcn Collapsible/Tabs,
lazy-loaded activity log, admin-only /api/about/roadmap + /dev-log endpoints
- Import CSRF fix: added x-csrf-token header to importAdminBackup,
previewSpreadsheetImport, previewUserDbImport raw fetch() calls
- Removed AdminDashboard.jsx, replaced by RoadmapPage
- Added @radix-ui/react-collapsible + collapsible shadcn component
- Security audit by Private_Hudson: PASS (CSRF fix verified,
admin endpoints gated, path traversal mitigated, XSS safe)
2026-05-11 21:42:36 -05:00
// CSRF protected via csrfMiddleware on /api/auth mount
2026-05-09 13:03:36 -05:00
router . post ( '/change-password' , passwordLimiter , requireAuth , async ( req , res ) => {
2026-05-03 19:51:57 -05:00
const { current _password , new _password } = req . body ;
if ( ! new _password || new _password . length < 8 ) {
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'New password must be at least 8 characters' , 'VALIDATION_ERROR' , 'new_password' ) ) ;
2026-05-03 19:51:57 -05:00
}
const db = getDb ( ) ;
const user = db . prepare ( 'SELECT * FROM users WHERE id = ?' ) . get ( req . user . id ) ;
2026-05-31 15:06:10 -05:00
try {
if ( ! user . must _change _password ) {
const bcrypt = require ( 'bcryptjs' ) ;
const valid = await bcrypt . compare ( current _password || '' , user . password _hash ) ;
if ( ! valid ) return res . status ( 401 ) . json ( standardizeError ( 'Current password is incorrect' , 'AUTH_ERROR' , 'current_password' ) ) ;
}
const hash = await hashPassword ( new _password ) ;
2026-05-03 19:51:57 -05:00
2026-05-31 15:06:10 -05:00
db . prepare (
"UPDATE users SET password_hash = ?, must_change_password = 0, last_password_change_at = datetime('now'), updated_at = datetime('now') WHERE id = ?"
) . run ( hash , req . user . id ) ;
// Invalidate all other sessions for this user
const currentSessionId = req . cookies ? . [ COOKIE _NAME ] ;
if ( currentSessionId ) {
invalidateOtherSessions ( req . user . id , currentSessionId ) ;
// Rotate the current session ID for security
const newSessionId = rotateSessionId ( currentSessionId , req . user . id ) ;
if ( newSessionId ) {
res . cookie ( COOKIE _NAME , newSessionId , cookieOpts ( req ) ) ;
}
2026-05-10 03:55:14 -05:00
}
2026-05-31 15:06:10 -05:00
logAudit ( { user _id : req . user . id , action : 'password.change' , ip _address : req . ip , user _agent : req . get ( 'user-agent' ) } ) ;
2026-05-10 00:03:12 -05:00
2026-05-31 15:06:10 -05:00
res . json ( { success : true } ) ;
} catch ( err ) {
console . error ( '[auth] change-password error:' , err . message ) ;
res . status ( 500 ) . json ( standardizeError ( 'Password change failed' , 'SERVER_ERROR' ) ) ;
}
2026-05-03 19:51:57 -05:00
} ) ;
// ─────────────────────────────────────────
// ADMIN ROUTES (MOUNTED AT /api/admin)
// ─────────────────────────────────────────
// GET /api/admin/has-users
router . get ( '/has-users' , ( req , res ) => {
const count = getDb ( )
. prepare ( "SELECT COUNT(*) AS n FROM users WHERE role = 'user'" )
. get ( ) . n ;
res . json ( { has _users : count > 0 } ) ;
} ) ;
// GET /api/admin/users
router . get ( '/users' , requireAuth , requireAdmin , ( req , res ) => {
const users = getDb ( ) . prepare (
"SELECT id, username, role, must_change_password, first_login, created_at FROM users ORDER BY role DESC, username ASC"
) . all ( ) ;
res . json ( users ) ;
} ) ;
// POST /api/admin/users
router . post ( '/users' , requireAuth , requireAdmin , async ( req , res ) => {
const { username , password } = req . body ;
if ( ! username || username . length < 3 ) {
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'Username must be at least 3 characters' , 'VALIDATION_ERROR' , 'username' ) ) ;
2026-05-03 19:51:57 -05:00
}
if ( ! password || password . length < 8 ) {
2026-05-09 13:03:36 -05:00
return res . status ( 400 ) . json ( standardizeError ( 'Password must be at least 8 characters' , 'VALIDATION_ERROR' , 'password' ) ) ;
2026-05-03 19:51:57 -05:00
}
const db = getDb ( ) ;
const existing = db . prepare ( 'SELECT id FROM users WHERE username = ?' ) . get ( username ) ;
2026-05-09 13:03:36 -05:00
if ( existing ) return res . status ( 409 ) . json ( standardizeError ( 'Username already taken' , 'CONFLICT' , 'username' ) ) ;
2026-05-03 19:51:57 -05:00
2026-05-31 15:06:10 -05:00
try {
const hash = await hashPassword ( password ) ;
2026-05-03 19:51:57 -05:00
2026-05-31 15:06:10 -05:00
const result = db . prepare (
"INSERT INTO users (username, password_hash, role, first_login, last_seen_version) VALUES (?, ?, 'user', 1, ?)"
) . run ( username , hash , getAppVersion ( ) ) ;
2026-05-03 19:51:57 -05:00
2026-05-31 15:06:10 -05:00
const created = db . prepare (
'SELECT id, username, role, must_change_password, first_login, created_at FROM users WHERE id = ?'
) . get ( result . lastInsertRowid ) ;
2026-05-03 19:51:57 -05:00
2026-05-31 15:06:10 -05:00
res . status ( 201 ) . json ( created ) ;
} catch ( err ) {
console . error ( '[auth] create-user error:' , err . message ) ;
res . status ( 500 ) . json ( standardizeError ( 'Failed to create user' , 'SERVER_ERROR' ) ) ;
}
2026-05-03 19:51:57 -05:00
} ) ;
module . exports = router ;