@@ -9,7 +9,7 @@ import { z } from "zod";
99import { PrismaClient , Prisma } from "./generated/client" ;
1010import { config } from "./config" ;
1111import { requireAuth , optionalAuth } from "./middleware/auth" ;
12- import { sanitizeText } from "./security" ;
12+ import { sanitizeText , getCsrfTokenHeader , validateCsrfToken } from "./security" ;
1313import rateLimit , { MemoryStore } from "express-rate-limit" ;
1414import { logAuditEvent } from "./utils/audit" ;
1515import crypto from "crypto" ;
@@ -281,6 +281,36 @@ const requireAdmin = (
281281 return true ;
282282} ;
283283
284+ const getClientId = ( req : Request ) : string => {
285+ const ip = req . ip || req . connection . remoteAddress || "unknown" ;
286+ const userAgent = req . headers [ "user-agent" ] || "unknown" ;
287+ return `${ ip } :${ userAgent } ` . slice ( 0 , 256 ) ;
288+ } ;
289+
290+ const requireCsrf = ( req : Request , res : Response ) : boolean => {
291+ const headerName = getCsrfTokenHeader ( ) ;
292+ const tokenHeader = req . headers [ headerName ] ;
293+ const token = Array . isArray ( tokenHeader ) ? tokenHeader [ 0 ] : tokenHeader ;
294+
295+ if ( ! token ) {
296+ res . status ( 403 ) . json ( {
297+ error : "CSRF token missing" ,
298+ message : `Missing ${ headerName } header` ,
299+ } ) ;
300+ return false ;
301+ }
302+
303+ if ( ! validateCsrfToken ( getClientId ( req ) , token ) ) {
304+ res . status ( 403 ) . json ( {
305+ error : "CSRF token invalid" ,
306+ message : "Invalid or expired CSRF token. Please refresh and try again." ,
307+ } ) ;
308+ return false ;
309+ }
310+
311+ return true ;
312+ } ;
313+
284314const countActiveAdmins = async ( ) => {
285315 return prisma . user . count ( {
286316 where : { role : "ADMIN" , isActive : true } ,
@@ -968,6 +998,8 @@ router.get("/status", optionalAuth, async (req: Request, res: Response) => {
968998 */
969999router . post ( "/auth-enabled" , optionalAuth , async ( req : Request , res : Response ) => {
9701000 try {
1001+ if ( ! requireCsrf ( req , res ) ) return ;
1002+
9711003 const parsed = authEnabledToggleSchema . safeParse ( req . body ) ;
9721004 if ( ! parsed . success ) {
9731005 return res
@@ -1477,6 +1509,15 @@ router.patch("/users/:id", requireAuth, async (req: Request, res: Response) => {
14771509
14781510 res . json ( { user : updated } ) ;
14791511 } catch ( error ) {
1512+ if (
1513+ error instanceof Prisma . PrismaClientKnownRequestError &&
1514+ error . code === "P2002"
1515+ ) {
1516+ return res . status ( 409 ) . json ( {
1517+ error : "Conflict" ,
1518+ message : "User with this username already exists" ,
1519+ } ) ;
1520+ }
14801521 console . error ( "Update user error:" , error ) ;
14811522 res . status ( 500 ) . json ( {
14821523 error : "Internal server error" ,
@@ -1745,7 +1786,7 @@ router.post("/password-reset-request", loginAttemptRateLimiter, async (req: Requ
17451786 : `http://${ baseUrlRaw } `
17461787 : "http://localhost:6767" ;
17471788 const baseUrl = baseUrlWithProtocol . replace ( / \/ $ / , "" ) ;
1748- console . log ( `[DEV] Reset URL: ${ baseUrl } /reset-password?token=${ resetToken } ` ) ;
1789+ console . log ( `[DEV] Reset URL: ${ baseUrl } /reset-password-confirm ?token=${ resetToken } ` ) ;
17491790 }
17501791 }
17511792
0 commit comments