@@ -3998,9 +3998,88 @@ All errors return JSON with an \`error\` field and optional \`code\`:
39983998 }
39993999 } ) ;
40004000
4001- // ── Supporters ─────────────────────────────────────────────────────────
4001+ // ── Profile Reports (#771) ─────────────────────────────────────────────
4002+
4003+ // Rate limiter: 1 report per IP per profile per hour
4004+ const reportLimiter = rateLimit ( {
4005+ windowMs : 60 * 60 * 1000 ,
4006+ limit : 1 ,
4007+ standardHeaders : true ,
4008+ legacyHeaders : false ,
4009+ skip : ( ) => process . env . NODE_ENV === "test" ,
4010+ keyGenerator : ( req ) => `${ req . ip } -${ req . params . username } ` ,
4011+ message : {
4012+ error : "You have already reported this profile. Please wait an hour before submitting another report." ,
4013+ code : "REPORT_RATE_LIMIT_EXCEEDED" ,
4014+ } ,
4015+ } ) ;
4016+
4017+ const reportSchema = z . object ( {
4018+ reason : z . enum ( [ "spam" , "impersonation" , "inappropriate" , "scam" ] ) ,
4019+ details : z . string ( ) . max ( 500 ) . optional ( ) ,
4020+ } ) ;
4021+
4022+ v1Router . post ( "/profiles/:username/report" , reportLimiter , async ( req , res ) => {
4023+ try {
4024+ const { username } = req . params as { username : string } ;
4025+ const profile = await prisma . profile . findUnique ( {
4026+ where : { username } ,
4027+ select : { id : true , username : true } ,
4028+ } ) ;
4029+
4030+ if ( ! profile ) {
4031+ return sendError ( res , 404 , "Profile not found" ) ;
4032+ }
4033+
4034+ const parsed = reportSchema . safeParse ( req . body ) ;
4035+ if ( ! parsed . success ) {
4036+ return sendError ( res , 400 , "Invalid request body: reason must be one of spam, impersonation, inappropriate, scam" ) ;
4037+ }
4038+
4039+ const reporterIp = req . ip ?? "unknown" ;
4040+
4041+ await ( prisma as any ) . profileReport . create ( {
4042+ data : {
4043+ profileId : profile . id ,
4044+ reason : parsed . data . reason ,
4045+ details : parsed . data . details ?? null ,
4046+ reporterIp,
4047+ } ,
4048+ } ) ;
4049+
4050+ // Check if this profile has accumulated 3+ reports and alert admin
4051+ const reportCount = await ( prisma as any ) . profileReport . count ( {
4052+ where : { profileId : profile . id } ,
4053+ } ) ;
40024054
4003- v1Router . get ( "/supporters/:address" , async ( req , res ) => {
4055+ const ADMIN_EMAIL = process . env . ADMIN_EMAIL ;
4056+ if ( reportCount >= 3 && ADMIN_EMAIL ) {
4057+ try {
4058+ const { sendEmail } = await import ( "./mailer.js" ) ;
4059+ await sendEmail ( {
4060+ to : ADMIN_EMAIL ,
4061+ subject : `[NovaSupport] Profile @${ username } has ${ reportCount } report(s)` ,
4062+ html : `
4063+ <p>Profile <strong>@${ username } </strong> has accumulated <strong>${ reportCount } </strong> report(s).</p>
4064+ <p>Latest report reason: <strong>${ parsed . data . reason } </strong></p>
4065+ ${ parsed . data . details ? `<p>Details: ${ parsed . data . details } </p>` : "" }
4066+ <p>Please review this profile in the admin panel.</p>
4067+ ` ,
4068+ } ) ;
4069+ } catch ( emailErr ) {
4070+ // Don't fail the request if email fails — just log it
4071+ logger . warn ( { err : emailErr , username } , "failed to send admin alert email for profile report" ) ;
4072+ }
4073+ }
4074+
4075+ return res . status ( 201 ) . json ( { message : "Report submitted successfully." } ) ;
4076+ } catch ( e : unknown ) {
4077+ logger . error ( { err : e } , "failed to submit profile report" ) ;
4078+ return sendError ( res , 500 , "Internal server error" ) ;
4079+ }
4080+ } ) ;
4081+
4082+ // ── Supporters ─────────────────────────────────────────────────────────
40044083 try {
40054084 const { address } = req . params ;
40064085
0 commit comments