ilia 08914dc469 Implements a comprehensive structured logging system to replace verbose console.* calls throughout the codebase, addressing all cleanup tasks from CLEANUP.md. (#4)
# Structured Logging System Implementation

## Summary
Implements a comprehensive structured logging system to replace verbose console.* calls throughout the codebase, addressing all cleanup tasks from CLEANUP.md.

## What Changed

### Core Features
-  **Structured Logging System** - New `lib/logger.ts` with DEBUG, INFO, WARN, ERROR levels
-  **Environment-Based Control** - `LOG_LEVEL` env var controls verbosity (DEBUG/INFO/WARN/ERROR/NONE)
-  **JSON Logging Option** - `LOG_FORMAT=json` for structured JSON output
-  **Shared Constants** - Extracted session cookie name to `lib/constants.ts`

### Code Refactoring
-  Replaced all `console.*` calls in API routes with structured logger
-  Refactored `activity-log.ts` to use new logger system
-  Reduced verbose logging in auth, photos page, and upload routes
-  Updated proxy.ts to use structured logging
-  Removed unused legacy `/api/photos` route (replaced by `/api/photos/upload`)

### Security Improvements
-  Protected `/api/debug/session` endpoint with admin-only access
-  Added proper error logging with structured context

### Documentation
-  Documented multiple upload routes usage
-  Enhanced watch-activity.sh script documentation
-  Updated README.md with upload endpoint information
-  Added configuration documentation to next.config.ts

### Testing
-  Added 23 tests for logger system
-  Added 8 tests for refactored activity-log
-  All 43 tests passing

## Benefits

1. **Production-Ready Logging** - Environment-based control, defaults to INFO in production
2. **Reduced Verbosity** - DEBUG logs only show in development or when explicitly enabled
3. **Structured Output** - JSON format option for log aggregation tools
4. **Better Organization** - Shared constants, consistent logging patterns
5. **Improved Security** - Debug endpoint now requires admin access

## Testing

### Manual Testing
-  Server builds successfully
-  All tests pass (43/43)
-  Type checking passes
-  Linting passes
-  Production server runs with logs visible
-  Log levels work correctly (DEBUG shows all, INFO shows activity, etc.)

### Test Coverage
- Logger system: 100% coverage
- Activity log: 100% coverage
- All existing tests still pass

## Configuration

### Environment Variables
```bash
# Control log verbosity (DEBUG, INFO, WARN, ERROR, NONE)
LOG_LEVEL=INFO

# Use structured JSON logging
LOG_FORMAT=json
```

### Defaults
- Development: `LOG_LEVEL=DEBUG` (shows all logs)
- Production: `LOG_LEVEL=INFO` (shows activity and above)

## Migration Notes

- No breaking changes (legacy route was unused)
- All existing functionality preserved
- Logs are now structured and filterable
- Debug endpoint now requires admin authentication
- Legacy `/api/photos` endpoint removed (use `/api/photos/upload` instead)

## Checklist

- [x] All console.* calls replaced in API routes
- [x] Logger system implemented with tests
- [x] Activity logging refactored
- [x] Debug endpoint protected
- [x] Documentation updated
- [x] All tests passing
- [x] Type checking passes
- [x] Linting passes
- [x] Build succeeds
- [x] Manual testing completed

## Related Issues
Addresses cleanup tasks from CLEANUP.md:
- Task 1: Verbose logging in production 
- Task 2: Activity logging optimization 
- Task 3: Upload verification logging 
- Task 4: Middleware debug logging 
- Task 5: Legacy upload route documentation 
- Task 6: Multiple upload routes documentation 
- Task 7: Cookie name constant extraction 
- Task 8: Next.js config documentation 
- Task 9: ARCHITECTURE.md (already correct) 
- Task 10: Watch activity script documentation 

Reviewed-on: #4
2026-01-04 19:42:49 -05:00

164 lines
4.6 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server"
import { auth } from "@/lib/auth"
import { prisma } from "@/lib/prisma"
import { normalizeString } from "@/lib/utils"
import { logActivity } from "@/lib/activity-log"
import { logger } from "@/lib/logger"
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ photoId: string }> }
) {
try {
const session = await auth()
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const { photoId } = await params
const { guessText } = await req.json()
if (!guessText || !guessText.trim()) {
return NextResponse.json(
{ error: "Guess text is required" },
{ status: 400 }
)
}
const photo = await prisma.photo.findUnique({
where: { id: photoId },
})
if (!photo) {
return NextResponse.json({ error: "Photo not found" }, { status: 404 })
}
// Prevent users from guessing their own photos
if (photo.uploaderId === session.user.id) {
return NextResponse.json(
{ error: "You cannot guess on your own photos" },
{ status: 403 }
)
}
// Check if user already has a correct guess
const existingCorrectGuess = await prisma.guess.findFirst({
where: {
userId: session.user.id,
photoId: photoId,
correct: true,
},
})
if (existingCorrectGuess) {
return NextResponse.json(
{ error: "You already guessed this correctly" },
{ status: 400 }
)
}
// Check max attempts limit
const photoWithMaxAttempts = photo as typeof photo & { maxAttempts: number | null }
if (photoWithMaxAttempts.maxAttempts !== null && photoWithMaxAttempts.maxAttempts > 0) {
const userGuessCount = await prisma.guess.count({
where: {
userId: session.user.id,
photoId: photoId,
},
})
if (userGuessCount >= photoWithMaxAttempts.maxAttempts) {
return NextResponse.json(
{ error: `You have reached the maximum number of attempts (${photoWithMaxAttempts.maxAttempts}) for this photo` },
{ status: 400 }
)
}
}
// Check if guess is correct (case-insensitive, trimmed)
const normalizedGuess = normalizeString(guessText)
const normalizedAnswer = normalizeString(photo.answerName)
const isCorrect = normalizedGuess === normalizedAnswer
// Create the guess
const guess = await prisma.guess.create({
data: {
userId: session.user.id,
photoId: photoId,
guessText: guessText.trim(),
correct: isCorrect,
},
})
// Update user points based on guess result
let pointsChange = 0
const photoWithPenalty = photo as typeof photo & { penaltyEnabled: boolean; penaltyPoints: number }
if (isCorrect) {
// Award points for correct answer
pointsChange = photo.points
await prisma.user.update({
where: { id: session.user.id },
data: {
points: {
increment: photo.points, // Award points based on photo difficulty
},
},
})
} else if (photoWithPenalty.penaltyEnabled && photoWithPenalty.penaltyPoints > 0) {
// Deduct points for wrong answer if penalty is enabled
// First, get current user points to prevent going below 0
const currentUser = await prisma.user.findUnique({
where: { id: session.user.id },
select: { points: true },
})
if (currentUser) {
const currentPoints = currentUser.points
const penaltyAmount = photoWithPenalty.penaltyPoints
const newPoints = Math.max(0, currentPoints - penaltyAmount)
const actualDeduction = currentPoints - newPoints
pointsChange = -actualDeduction
await prisma.user.update({
where: { id: session.user.id },
data: {
points: newPoints,
},
})
}
}
// Log guess activity
logActivity(
"GUESS_SUBMIT",
`/api/photos/${photoId}/guess`,
"POST",
session.user,
{
photoId,
guessText: guess.guessText.substring(0, 50), // Truncate for privacy
correct: isCorrect,
pointsChange
},
req
)
return NextResponse.json({
guess,
correct: isCorrect,
pointsChange
})
} catch (error) {
logger.error("Error submitting guess", {
error: error instanceof Error ? error : new Error(String(error)),
})
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
)
}
}