|
1 | | -import { NextResponse } from 'next/server'; |
| 1 | +import { NextRequest, NextResponse } from 'next/server'; |
2 | 2 | import dbConnect from '@/lib/mongodb'; |
3 | 3 | import { Notification } from '@/models/Notification'; |
4 | 4 | import { notifyPostSchema, notifyGetSchema } from '@/lib/validations'; |
5 | | -import { notifyRateLimiter } from '@/lib/rate-limit'; |
6 | 5 | import { getClientIp } from '@/utils/getClientIp'; |
7 | 6 | import { DistributedCache } from '@/lib/cache'; |
8 | 7 | import { gitHubUserValidator } from '@/services/github/validate-user'; |
| 8 | +import { getRateLimitHeaders, notifyRateLimiter } from '@/lib/rate-limit'; |
9 | 9 |
|
10 | 10 | const notifyWriteCache = new DistributedCache<number>(5000, 60000); |
11 | 11 | const NOTIFY_WRITE_COOLDOWN_MS = 5 * 60 * 1000; |
@@ -44,11 +44,12 @@ export async function POST(req: Request) { |
44 | 44 | // fallback ensures rate limit is ALWAYS applied |
45 | 45 | const rateLimitKey = |
46 | 46 | ip && ip !== 'unknown' ? ip : `unknown:${req.headers.get('user-agent') ?? 'no-agent'}`; |
| 47 | + const rateLimitResult = await notifyRateLimiter.checkWithResult(rateLimitKey); |
47 | 48 |
|
48 | | - if (!(await notifyRateLimiter.check(rateLimitKey))) { |
| 49 | + if (!rateLimitResult.success) { |
49 | 50 | return NextResponse.json( |
50 | 51 | { success: false, message: 'Too many requests, please try again later.' }, |
51 | | - { status: 429 } |
| 52 | + { status: 429, headers: getRateLimitHeaders(rateLimitResult) } |
52 | 53 | ); |
53 | 54 | } |
54 | 55 |
|
@@ -172,19 +173,102 @@ export async function POST(req: Request) { |
172 | 173 | } |
173 | 174 | } |
174 | 175 |
|
175 | | -// ─── GET /api/notify ───────────────────────────────────────────────────────── |
176 | | -// Fetch notification preferences for a user |
177 | | -export async function GET(req: Request) { |
| 176 | +// ─── DELETE /api/notify ────────────────────────────────────────────────────── |
| 177 | +// Remove notification preferences for a user (unsubscribe / right to erasure) |
| 178 | +export async function DELETE(req: NextRequest) { |
178 | 179 | // Rate limiting |
179 | 180 | const ip = getClientIp(req); |
180 | 181 |
|
181 | | - if (ip !== 'unknown' && !(await notifyRateLimiter.check(ip))) { |
| 182 | + const rateLimitKey = |
| 183 | + ip && ip !== 'unknown' ? ip : `unknown:${req.headers.get('user-agent') ?? 'no-agent'}`; |
| 184 | + |
| 185 | + if (!(await notifyRateLimiter.check(rateLimitKey))) { |
182 | 186 | return NextResponse.json( |
183 | 187 | { success: false, message: 'Too many requests, please try again later.' }, |
184 | 188 | { status: 429 } |
185 | 189 | ); |
186 | 190 | } |
187 | 191 |
|
| 192 | + // Validate query params with Zod (reuse notifyGetSchema — expects ?user=) |
| 193 | + const { searchParams } = new URL(req.url); |
| 194 | + const parsed = notifyGetSchema.safeParse({ |
| 195 | + user: searchParams.get('user') ?? undefined, |
| 196 | + }); |
| 197 | + |
| 198 | + if (!parsed.success) { |
| 199 | + const fieldErrors = parsed.error.flatten(); |
| 200 | + const firstError = |
| 201 | + Object.values(fieldErrors.fieldErrors).flat()[0] ?? |
| 202 | + fieldErrors.formErrors[0] ?? |
| 203 | + 'Invalid request parameters.'; |
| 204 | + return NextResponse.json({ success: false, message: firstError }, { status: 400 }); |
| 205 | + } |
| 206 | + |
| 207 | + const { user: username } = parsed.data; |
| 208 | + |
| 209 | + try { |
| 210 | + // Graceful MONGODB_URI handling |
| 211 | + if (!process.env.MONGODB_URI) { |
| 212 | + if (process.env.NODE_ENV === 'production') { |
| 213 | + console.error( |
| 214 | + 'CRITICAL: MONGODB_URI is not set in production environment. Notification deletion is disabled.' |
| 215 | + ); |
| 216 | + return NextResponse.json( |
| 217 | + { success: false, message: 'Database configuration error.' }, |
| 218 | + { status: 500 } |
| 219 | + ); |
| 220 | + } |
| 221 | + |
| 222 | + console.warn( |
| 223 | + 'MONGODB_URI is not set. Bypassing notification deletion for local development.' |
| 224 | + ); |
| 225 | + return NextResponse.json({ |
| 226 | + success: true, |
| 227 | + message: 'Notification deletion bypassed (no database configured).', |
| 228 | + }); |
| 229 | + } |
| 230 | + |
| 231 | + await dbConnect(); |
| 232 | + |
| 233 | + const result = await Notification.deleteOne({ username: username.toLowerCase() }); |
| 234 | + |
| 235 | + if (result.deletedCount === 0) { |
| 236 | + return NextResponse.json( |
| 237 | + { success: false, message: 'No notification preferences found for this user.' }, |
| 238 | + { status: 404 } |
| 239 | + ); |
| 240 | + } |
| 241 | + |
| 242 | + return NextResponse.json( |
| 243 | + { success: true, message: 'Notification preferences deleted successfully.' }, |
| 244 | + { status: 200 } |
| 245 | + ); |
| 246 | + } catch (error) { |
| 247 | + console.error('[/api/notify] Error deleting notification preferences:', error); |
| 248 | + return NextResponse.json( |
| 249 | + { success: false, message: 'Internal server error.' }, |
| 250 | + { status: 500 } |
| 251 | + ); |
| 252 | + } |
| 253 | +} |
| 254 | + |
| 255 | +// ─── GET /api/notify ───────────────────────────────────────────────────────── |
| 256 | +// Fetch notification preferences for a user |
| 257 | +export async function GET(req: Request) { |
| 258 | + // Rate limiting |
| 259 | + const ip = getClientIp(req); |
| 260 | + |
| 261 | + if (ip !== 'unknown') { |
| 262 | + const rateLimitResult = await notifyRateLimiter.checkWithResult(ip); |
| 263 | + |
| 264 | + if (!rateLimitResult.success) { |
| 265 | + return NextResponse.json( |
| 266 | + { success: false, message: 'Too many requests, please try again later.' }, |
| 267 | + { status: 429, headers: getRateLimitHeaders(rateLimitResult) } |
| 268 | + ); |
| 269 | + } |
| 270 | + } |
| 271 | + |
188 | 272 | // Validate query params with Zod |
189 | 273 | const { searchParams } = new URL(req.url); |
190 | 274 | const parsed = notifyGetSchema.safeParse({ |
|
0 commit comments