# Cleanup Checklist
All checks were successful
CI / skip-ci-check (pull_request) Successful in 1m23s
CI / lint-and-type-check (pull_request) Successful in 1m47s
CI / test (pull_request) Successful in 1m51s
CI / build (pull_request) Successful in 1m52s
CI / secret-scanning (pull_request) Successful in 1m25s
CI / dependency-scan (pull_request) Successful in 1m28s
CI / sast-scan (pull_request) Successful in 2m32s
CI / workflow-summary (pull_request) Successful in 1m22s
All checks were successful
CI / skip-ci-check (pull_request) Successful in 1m23s
CI / lint-and-type-check (pull_request) Successful in 1m47s
CI / test (pull_request) Successful in 1m51s
CI / build (pull_request) Successful in 1m52s
CI / secret-scanning (pull_request) Successful in 1m25s
CI / dependency-scan (pull_request) Successful in 1m28s
CI / sast-scan (pull_request) Successful in 2m32s
CI / workflow-summary (pull_request) Successful in 1m22s
This document lists code and features that were added during development/debugging that might be candidates for cleanup or removal in the future. ## Debug/Development Code ### 1. Verbose Logging in Production **Location:** Multiple files **Status:** Consider reducing in production - `lib/auth.ts` - Session callback logging (lines 78-103, 105-113) - Logs full session details on every session creation - Could be reduced to warnings only or removed in production - `app/photos/page.tsx` - Page render logging (lines 12-33) - Logs auth() calls and session details - Useful for debugging but verbose for production - `app/api/debug/session/route.ts` - Entire debug endpoint - Created for debugging session issues - Consider removing or protecting with admin-only access - Or move to development-only route ### 2. Activity Logging **Location:** `lib/activity-log.ts`, `proxy.ts`, API routes **Status:** Keep but consider optimization - Activity logging is useful for monitoring - Consider: - Moving to structured logging (JSON format) - Adding log rotation/retention policies - Option to disable in production if not needed - Rate limiting logs to prevent spam ### 3. Upload Verification Logging **Location:** `app/api/photos/upload/route.ts` **Status:** Keep but reduce verbosity - Lines 89-91: Directory creation/existence logging - Lines 101: File save verification logging - Useful for debugging but could be reduced to errors only ### 4. Middleware Debug Logging **Location:** `proxy.ts` **Status:** Keep but consider reducing - Lines 22-37: Activity logging for all requests - Useful for monitoring but generates many logs - Consider: log only important events or add log level filtering ## Unused/Redundant Code ### 5. Legacy Upload Route **Location:** `app/api/photos/route.ts` **Status:** Consider deprecating - Legacy URL-based upload endpoint - New uploads use `/api/photos/upload` - Consider: - Marking as deprecated - Removing if not used - Or consolidating with upload route ### 6. Multiple Upload Routes **Location:** `app/api/photos/upload/route.ts` and `app/api/photos/upload-multiple/route.ts` **Status:** Keep but document usage - Two separate upload endpoints - Consider if both are needed or can be consolidated ### 7. Proxy.ts Cookie Name Variable **Location:** `proxy.ts` line 15 **Status:** Minor cleanup - `cookieName` variable defined but could use constant - Consider moving to shared constant or env var ## Configuration Cleanup ### 8. Next.js Config **Location:** `next.config.ts` **Status:** Review - Image optimization settings (line 19: `unoptimized: false`) - Consider if all remote patterns are needed - Review Turbopack configuration if not using ## Documentation Cleanup ### 10. ARCHITECTURE.md References **Location:** `ARCHITECTURE.md` line 156 **Status:** Update - Still references `middleware.ts` in some places - Should reference `proxy.ts` instead - Update all middleware references ## Testing/Debugging Utilities ### 11. Watch Activity Script **Location:** `watch-activity.sh` (if created) **Status:** Keep or document - Useful utility for monitoring - Consider adding to README or removing if not needed ## Recommendations ### High Priority (Consider Removing) 1. `app/api/debug/session/route.ts` - Debug endpoint (protect or remove) 2. Verbose logging in `app/photos/page.tsx` - Reduce to errors only 3. Update ARCHITECTURE.md middleware references ### Medium Priority (Optimize) 1. Activity logging - Add log levels or filtering 2. Upload logging - Reduce verbosity 3. Session callback logging - Reduce in production ### Low Priority (Keep) 1. Activity logging utility - Useful for monitoring 2. Multiple upload routes - Document usage 3. Watch activity script - Useful utility ## Notes - **Consider** adding environment-based log levels (DEBUG, INFO, WARN, ERROR) - **Consider** moving debug endpoints behind admin authentication - **Consider** adding log rotation/retention for production --- Do all these in stages. create new tests and test and docuemtn as u go. add DEBUG, INFO, WARN, ERROR flags and only show when asked for. create new branch.
This commit is contained in:
parent
01480586ff
commit
efb6519ffe
3
.gitignore
vendored
3
.gitignore
vendored
@ -50,3 +50,6 @@ next-env.d.ts
|
|||||||
# Test coverage
|
# Test coverage
|
||||||
/coverage
|
/coverage
|
||||||
/.nyc_output
|
/.nyc_output
|
||||||
|
|
||||||
|
# Application logs
|
||||||
|
*.log
|
||||||
|
|||||||
@ -233,7 +233,7 @@ model Guess {
|
|||||||
**Flow:**
|
**Flow:**
|
||||||
1. User navigates to `/upload`
|
1. User navigates to `/upload`
|
||||||
2. Uploads photo file or enters photo URL and answer name
|
2. Uploads photo file or enters photo URL and answer name
|
||||||
3. Form submits to `POST /api/photos/upload` (file upload) or `POST /api/photos` (URL)
|
3. Form submits to `POST /api/photos/upload` (supports both file and URL uploads)
|
||||||
4. API route:
|
4. API route:
|
||||||
- Verifies session
|
- Verifies session
|
||||||
- For file uploads:
|
- For file uploads:
|
||||||
@ -253,8 +253,8 @@ model Guess {
|
|||||||
5. User redirected to photo detail page
|
5. User redirected to photo detail page
|
||||||
|
|
||||||
**API Routes:**
|
**API Routes:**
|
||||||
- `app/api/photos/upload/route.ts` - File upload endpoint
|
- `app/api/photos/upload/route.ts` - Single photo upload endpoint (supports both file and URL uploads)
|
||||||
- `app/api/photos/route.ts` - URL upload endpoint (legacy)
|
- `app/api/photos/upload-multiple/route.ts` - Multiple photo upload endpoint
|
||||||
- `app/api/uploads/[filename]/route.ts` - Serves uploaded files
|
- `app/api/uploads/[filename]/route.ts` - Serves uploaded files
|
||||||
|
|
||||||
**File Storage:**
|
**File Storage:**
|
||||||
|
|||||||
@ -166,12 +166,17 @@ npm start
|
|||||||
- Photos are uploaded to `public/uploads/` directory
|
- Photos are uploaded to `public/uploads/` directory
|
||||||
- Files are served via `/api/uploads/[filename]` API route
|
- Files are served via `/api/uploads/[filename]` API route
|
||||||
- Ensure the uploads directory has proper write permissions
|
- Ensure the uploads directory has proper write permissions
|
||||||
|
|
||||||
|
**Upload Endpoints:**
|
||||||
|
- `POST /api/photos/upload` - Single photo upload (supports both file and URL uploads)
|
||||||
|
- `POST /api/photos/upload-multiple` - Multiple photo uploads in batch (used by upload page)
|
||||||
- Files are stored on the filesystem (not in database)
|
- Files are stored on the filesystem (not in database)
|
||||||
|
|
||||||
**Monitoring Activity:**
|
**Monitoring Activity:**
|
||||||
- User activity is logged to console/systemd logs
|
- User activity is logged to console/systemd logs
|
||||||
- Watch logs in real-time: `sudo journalctl -u app-backend -f | grep -E "\[ACTIVITY\]|\[PHOTO_UPLOAD\]|\[GUESS_SUBMIT\]"`
|
- Watch logs in real-time: `sudo journalctl -u app-backend -f | grep -E "\[ACTIVITY\]|\[PHOTO_UPLOAD\]|\[GUESS_SUBMIT\]"`
|
||||||
- Activity logs include: page visits, photo uploads, guess submissions
|
- Activity logs include: page visits, photo uploads, guess submissions
|
||||||
|
- **Note:** For local development, use `./watch-activity.sh` script (if systemd/journalctl is not available, check application logs directly)
|
||||||
|
|
||||||
## Database Commands
|
## Database Commands
|
||||||
|
|
||||||
|
|||||||
209
__tests__/lib/activity-log.test.ts
Normal file
209
__tests__/lib/activity-log.test.ts
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
import { logActivity } from '@/lib/activity-log';
|
||||||
|
import { logger } from '@/lib/logger';
|
||||||
|
|
||||||
|
// Mock the logger
|
||||||
|
jest.mock('@/lib/logger', () => ({
|
||||||
|
logger: {
|
||||||
|
info: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Helper to create a mock Request object
|
||||||
|
function createMockRequest(headers: Record<string, string> = {}): Request {
|
||||||
|
const mockHeaders = new Headers();
|
||||||
|
Object.entries(headers).forEach(([key, value]) => {
|
||||||
|
mockHeaders.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: mockHeaders,
|
||||||
|
} as unknown as Request;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('activity-log', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('logActivity', () => {
|
||||||
|
it('should create activity log with all fields', () => {
|
||||||
|
const mockRequest = createMockRequest({
|
||||||
|
'x-forwarded-for': '192.168.1.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
id: 'user-123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
role: 'USER',
|
||||||
|
};
|
||||||
|
|
||||||
|
const details = { photoId: 'photo-456' };
|
||||||
|
|
||||||
|
const result = logActivity(
|
||||||
|
'PHOTO_UPLOAD',
|
||||||
|
'/api/photos/upload',
|
||||||
|
'POST',
|
||||||
|
user,
|
||||||
|
details,
|
||||||
|
mockRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
action: 'PHOTO_UPLOAD',
|
||||||
|
path: '/api/photos/upload',
|
||||||
|
method: 'POST',
|
||||||
|
userId: 'user-123',
|
||||||
|
userEmail: 'test@example.com',
|
||||||
|
userRole: 'USER',
|
||||||
|
ip: '192.168.1.1',
|
||||||
|
details: { photoId: 'photo-456' },
|
||||||
|
});
|
||||||
|
expect(result.timestamp).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle unauthenticated users', () => {
|
||||||
|
const result = logActivity(
|
||||||
|
'PAGE_VIEW',
|
||||||
|
'/photos',
|
||||||
|
'GET',
|
||||||
|
null,
|
||||||
|
undefined,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
action: 'PAGE_VIEW',
|
||||||
|
path: '/photos',
|
||||||
|
method: 'GET',
|
||||||
|
userId: undefined,
|
||||||
|
userEmail: undefined,
|
||||||
|
userRole: undefined,
|
||||||
|
ip: 'unknown',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract IP from x-forwarded-for header', () => {
|
||||||
|
const mockRequest = createMockRequest({
|
||||||
|
'x-forwarded-for': '192.168.1.1, 10.0.0.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = logActivity(
|
||||||
|
'ACTION',
|
||||||
|
'/path',
|
||||||
|
'GET',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
mockRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.ip).toBe('192.168.1.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract IP from x-real-ip header when x-forwarded-for is missing', () => {
|
||||||
|
const mockRequest = createMockRequest({
|
||||||
|
'x-real-ip': '10.0.0.1',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = logActivity(
|
||||||
|
'ACTION',
|
||||||
|
'/path',
|
||||||
|
'GET',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
mockRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.ip).toBe('10.0.0.1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use "unknown" for IP when no headers are present', () => {
|
||||||
|
const mockRequest = createMockRequest();
|
||||||
|
|
||||||
|
const result = logActivity(
|
||||||
|
'ACTION',
|
||||||
|
'/path',
|
||||||
|
'GET',
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
mockRequest
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.ip).toBe('unknown');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call logger.info with structured data', () => {
|
||||||
|
const user = {
|
||||||
|
id: 'user-123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
role: 'USER',
|
||||||
|
};
|
||||||
|
|
||||||
|
const details = { photoId: 'photo-456' };
|
||||||
|
|
||||||
|
logActivity(
|
||||||
|
'PHOTO_UPLOAD',
|
||||||
|
'/api/photos/upload',
|
||||||
|
'POST',
|
||||||
|
user,
|
||||||
|
details
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logger.info).toHaveBeenCalledWith(
|
||||||
|
'Activity: PHOTO_UPLOAD',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'POST',
|
||||||
|
path: '/api/photos/upload',
|
||||||
|
userId: 'user-123',
|
||||||
|
userEmail: 'test@example.com',
|
||||||
|
userRole: 'USER',
|
||||||
|
details: { photoId: 'photo-456' },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include details in logger call when details are not provided', () => {
|
||||||
|
const user = {
|
||||||
|
id: 'user-123',
|
||||||
|
email: 'test@example.com',
|
||||||
|
role: 'USER',
|
||||||
|
};
|
||||||
|
|
||||||
|
logActivity(
|
||||||
|
'PAGE_VIEW',
|
||||||
|
'/photos',
|
||||||
|
'GET',
|
||||||
|
user
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logger.info).toHaveBeenCalledWith(
|
||||||
|
'Activity: PAGE_VIEW',
|
||||||
|
expect.objectContaining({
|
||||||
|
method: 'GET',
|
||||||
|
path: '/photos',
|
||||||
|
userId: 'user-123',
|
||||||
|
userEmail: 'test@example.com',
|
||||||
|
userRole: 'USER',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const callArgs = (logger.info as jest.Mock).mock.calls[0][1];
|
||||||
|
expect(callArgs).not.toHaveProperty('details');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty details object', () => {
|
||||||
|
const result = logActivity(
|
||||||
|
'ACTION',
|
||||||
|
'/path',
|
||||||
|
'GET',
|
||||||
|
undefined,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.details).toEqual({});
|
||||||
|
expect(logger.info).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
210
__tests__/lib/logger.test.ts
Normal file
210
__tests__/lib/logger.test.ts
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import { logger, LogLevel, getLogLevel, formatLog, createLogger } from '@/lib/logger';
|
||||||
|
|
||||||
|
// Mock console methods
|
||||||
|
const originalConsole = { ...console };
|
||||||
|
const mockConsole = {
|
||||||
|
log: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Logger', () => {
|
||||||
|
const originalEnv = { ...process.env };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
console.log = mockConsole.log;
|
||||||
|
console.warn = mockConsole.warn;
|
||||||
|
console.error = mockConsole.error;
|
||||||
|
// Reset environment variables
|
||||||
|
process.env = { ...originalEnv };
|
||||||
|
// Use type assertion to allow deletion
|
||||||
|
delete (process.env as { LOG_LEVEL?: string }).LOG_LEVEL;
|
||||||
|
delete (process.env as { LOG_FORMAT?: string }).LOG_FORMAT;
|
||||||
|
delete (process.env as { NODE_ENV?: string }).NODE_ENV;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = originalEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
console.log = originalConsole.log;
|
||||||
|
console.warn = originalConsole.warn;
|
||||||
|
console.error = originalConsole.error;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getLogLevel', () => {
|
||||||
|
it('should return DEBUG when LOG_LEVEL=DEBUG', () => {
|
||||||
|
process.env.LOG_LEVEL = 'DEBUG';
|
||||||
|
expect(getLogLevel()).toBe(LogLevel.DEBUG);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return INFO when LOG_LEVEL=INFO', () => {
|
||||||
|
process.env.LOG_LEVEL = 'INFO';
|
||||||
|
expect(getLogLevel()).toBe(LogLevel.INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return WARN when LOG_LEVEL=WARN', () => {
|
||||||
|
process.env.LOG_LEVEL = 'WARN';
|
||||||
|
expect(getLogLevel()).toBe(LogLevel.WARN);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return ERROR when LOG_LEVEL=ERROR', () => {
|
||||||
|
process.env.LOG_LEVEL = 'ERROR';
|
||||||
|
expect(getLogLevel()).toBe(LogLevel.ERROR);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return NONE when LOG_LEVEL=NONE', () => {
|
||||||
|
process.env.LOG_LEVEL = 'NONE';
|
||||||
|
expect(getLogLevel()).toBe(LogLevel.NONE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default to DEBUG in development', () => {
|
||||||
|
(process.env as { NODE_ENV?: string }).NODE_ENV = 'development';
|
||||||
|
expect(getLogLevel()).toBe(LogLevel.DEBUG);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default to INFO in production', () => {
|
||||||
|
(process.env as { NODE_ENV?: string }).NODE_ENV = 'production';
|
||||||
|
expect(getLogLevel()).toBe(LogLevel.INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore invalid LOG_LEVEL values and use defaults', () => {
|
||||||
|
(process.env as { LOG_LEVEL?: string }).LOG_LEVEL = 'INVALID';
|
||||||
|
(process.env as { NODE_ENV?: string }).NODE_ENV = 'production';
|
||||||
|
expect(getLogLevel()).toBe(LogLevel.INFO);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('formatLog', () => {
|
||||||
|
it('should format log in human-readable format by default', () => {
|
||||||
|
const result = formatLog(LogLevel.INFO, 'Test message', { key: 'value' });
|
||||||
|
expect(result).toContain('[INFO]');
|
||||||
|
expect(result).toContain('Test message');
|
||||||
|
expect(result).toContain('{"key":"value"}');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format log as JSON when LOG_FORMAT=json', () => {
|
||||||
|
process.env.LOG_FORMAT = 'json';
|
||||||
|
const result = formatLog(LogLevel.INFO, 'Test message', { key: 'value' });
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed.level).toBe('INFO');
|
||||||
|
expect(parsed.message).toBe('Test message');
|
||||||
|
expect(parsed.key).toBe('value');
|
||||||
|
expect(parsed.timestamp).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format Error objects correctly', () => {
|
||||||
|
const error = new Error('Test error');
|
||||||
|
const result = formatLog(LogLevel.ERROR, 'Error occurred', error);
|
||||||
|
expect(result).toContain('[ERROR]');
|
||||||
|
expect(result).toContain('Error occurred');
|
||||||
|
expect(result).toContain('Error: Error: Test error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should format Error objects as JSON when LOG_FORMAT=json', () => {
|
||||||
|
process.env.LOG_FORMAT = 'json';
|
||||||
|
const error = new Error('Test error');
|
||||||
|
const result = formatLog(LogLevel.ERROR, 'Error occurred', error);
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed.level).toBe('ERROR');
|
||||||
|
expect(parsed.message).toBe('Error occurred');
|
||||||
|
expect(parsed.error.name).toBe('Error');
|
||||||
|
expect(parsed.error.message).toBe('Test error');
|
||||||
|
expect(parsed.error.stack).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle logs without context', () => {
|
||||||
|
const result = formatLog(LogLevel.INFO, 'Simple message');
|
||||||
|
expect(result).toContain('[INFO]');
|
||||||
|
expect(result).toContain('Simple message');
|
||||||
|
// Format always includes pipe separator, but no context data after it
|
||||||
|
expect(result).toContain('|');
|
||||||
|
expect(result.split('|').length).toBe(2); // timestamp | message (no context)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Logger instance', () => {
|
||||||
|
it('should log DEBUG messages when level is DEBUG', () => {
|
||||||
|
process.env.LOG_LEVEL = 'DEBUG';
|
||||||
|
const testLogger = createLogger();
|
||||||
|
testLogger.debug('Debug message', { data: 'test' });
|
||||||
|
expect(mockConsole.log).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not log DEBUG messages when level is INFO', () => {
|
||||||
|
process.env.LOG_LEVEL = 'INFO';
|
||||||
|
const testLogger = createLogger();
|
||||||
|
testLogger.debug('Debug message');
|
||||||
|
expect(mockConsole.log).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log INFO messages when level is INFO', () => {
|
||||||
|
process.env.LOG_LEVEL = 'INFO';
|
||||||
|
const testLogger = createLogger();
|
||||||
|
testLogger.info('Info message');
|
||||||
|
expect(mockConsole.log).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log WARN messages when level is WARN', () => {
|
||||||
|
process.env.LOG_LEVEL = 'WARN';
|
||||||
|
const testLogger = createLogger();
|
||||||
|
testLogger.warn('Warning message');
|
||||||
|
expect(mockConsole.warn).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not log INFO messages when level is WARN', () => {
|
||||||
|
process.env.LOG_LEVEL = 'WARN';
|
||||||
|
const testLogger = createLogger();
|
||||||
|
testLogger.info('Info message');
|
||||||
|
expect(mockConsole.log).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log ERROR messages when level is ERROR', () => {
|
||||||
|
process.env.LOG_LEVEL = 'ERROR';
|
||||||
|
const testLogger = createLogger();
|
||||||
|
testLogger.error('Error message');
|
||||||
|
expect(mockConsole.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not log any messages when level is NONE', () => {
|
||||||
|
process.env.LOG_LEVEL = 'NONE';
|
||||||
|
const testLogger = createLogger();
|
||||||
|
testLogger.debug('Debug message');
|
||||||
|
testLogger.info('Info message');
|
||||||
|
testLogger.warn('Warning message');
|
||||||
|
testLogger.error('Error message');
|
||||||
|
expect(mockConsole.log).not.toHaveBeenCalled();
|
||||||
|
expect(mockConsole.warn).not.toHaveBeenCalled();
|
||||||
|
expect(mockConsole.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Error objects in error method', () => {
|
||||||
|
process.env.LOG_LEVEL = 'ERROR';
|
||||||
|
const testLogger = createLogger();
|
||||||
|
const error = new Error('Test error');
|
||||||
|
testLogger.error('Error occurred', error);
|
||||||
|
expect(mockConsole.error).toHaveBeenCalled();
|
||||||
|
const callArgs = mockConsole.error.mock.calls[0][0];
|
||||||
|
expect(callArgs).toContain('Error occurred');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isLevelEnabled should return correct values', () => {
|
||||||
|
process.env.LOG_LEVEL = 'WARN';
|
||||||
|
const testLogger = createLogger();
|
||||||
|
expect(testLogger.isLevelEnabled(LogLevel.DEBUG)).toBe(false);
|
||||||
|
expect(testLogger.isLevelEnabled(LogLevel.INFO)).toBe(false);
|
||||||
|
expect(testLogger.isLevelEnabled(LogLevel.WARN)).toBe(true);
|
||||||
|
expect(testLogger.isLevelEnabled(LogLevel.ERROR)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Default logger instance', () => {
|
||||||
|
it('should be available and functional', () => {
|
||||||
|
process.env.LOG_LEVEL = 'INFO';
|
||||||
|
logger.info('Test message');
|
||||||
|
expect(mockConsole.log).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,12 +1,14 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server"
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
import { auth } from "@/lib/auth"
|
import { auth } from "@/lib/auth"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
|
import { logger } from "@/lib/logger"
|
||||||
import { hashPassword } from "@/lib/utils"
|
import { hashPassword } from "@/lib/utils"
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
{ params }: { params: Promise<{ userId: string }> }
|
{ params }: { params: Promise<{ userId: string }> }
|
||||||
) {
|
) {
|
||||||
|
let userId: string | undefined
|
||||||
try {
|
try {
|
||||||
const session = await auth()
|
const session = await auth()
|
||||||
|
|
||||||
@ -14,7 +16,7 @@ export async function POST(
|
|||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const { userId } = await params
|
userId = (await params).userId
|
||||||
const { password } = await req.json()
|
const { password } = await req.json()
|
||||||
|
|
||||||
if (!password || password.length < 6) {
|
if (!password || password.length < 6) {
|
||||||
@ -33,7 +35,10 @@ export async function POST(
|
|||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error resetting password:", error)
|
logger.error("Error resetting password", {
|
||||||
|
userId,
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
})
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Internal server error" },
|
{ error: "Internal server error" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server"
|
|||||||
import { auth } from "@/lib/auth"
|
import { auth } from "@/lib/auth"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
import { hashPassword } from "@/lib/utils"
|
import { hashPassword } from "@/lib/utils"
|
||||||
|
import { logger } from "@/lib/logger"
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@ -53,7 +54,9 @@ export async function POST(req: NextRequest) {
|
|||||||
{ status: 201 }
|
{ status: 201 }
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating user:", error)
|
logger.error("Error creating user", {
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
})
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Internal server error" },
|
{ error: "Internal server error" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@ -1,9 +1,29 @@
|
|||||||
import { auth } from "@/lib/auth"
|
import { auth } from "@/lib/auth"
|
||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import { cookies } from "next/headers"
|
import { cookies } from "next/headers"
|
||||||
|
import { SESSION_COOKIE_NAME } from "@/lib/constants"
|
||||||
|
import { logger } from "@/lib/logger"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debug endpoint for session inspection
|
||||||
|
* ADMIN ONLY - Protected endpoint for debugging session issues
|
||||||
|
*
|
||||||
|
* This endpoint should only be accessible to administrators.
|
||||||
|
* Consider removing in production or restricting further.
|
||||||
|
*/
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
try {
|
try {
|
||||||
|
// Require admin authentication
|
||||||
|
const session = await auth()
|
||||||
|
|
||||||
|
if (!session || session.user.role !== "ADMIN") {
|
||||||
|
logger.warn("Unauthorized access attempt to debug endpoint", {
|
||||||
|
userId: session?.user?.id,
|
||||||
|
userRole: session?.user?.role,
|
||||||
|
path: "/api/debug/session",
|
||||||
|
})
|
||||||
|
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
||||||
|
}
|
||||||
const cookieHeader = request.headers.get("cookie") || ""
|
const cookieHeader = request.headers.get("cookie") || ""
|
||||||
|
|
||||||
// Parse cookies from header first
|
// Parse cookies from header first
|
||||||
@ -16,29 +36,29 @@ export async function GET(request: Request) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Try to get session token from cookies
|
// Try to get session token from cookies
|
||||||
const sessionTokenFromHeader = cookieMap["__Secure-authjs.session-token"] || "NOT FOUND"
|
const sessionTokenFromHeader = cookieMap[SESSION_COOKIE_NAME] || "NOT FOUND"
|
||||||
|
|
||||||
// Try to call auth() - this might fail or return null
|
// Try to call auth() again for debugging (we already have session above, but this is for testing)
|
||||||
let session = null
|
|
||||||
let authError = null
|
let authError = null
|
||||||
try {
|
try {
|
||||||
console.log("Debug endpoint: Calling auth()...")
|
// Already called above, but keeping for backward compatibility in response
|
||||||
session = await auth()
|
logger.debug("Debug endpoint: Session retrieved", {
|
||||||
console.log("Debug endpoint: auth() returned", {
|
|
||||||
hasSession: !!session,
|
hasSession: !!session,
|
||||||
sessionUser: session?.user,
|
userId: session?.user?.id,
|
||||||
sessionKeys: session ? Object.keys(session) : []
|
userRole: session?.user?.role,
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authError = err instanceof Error ? err.message : String(err)
|
authError = err instanceof Error ? err.message : String(err)
|
||||||
console.error("Debug endpoint: auth() error", authError)
|
logger.error("Debug endpoint: auth() error", {
|
||||||
|
error: err instanceof Error ? err : new Error(String(err)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get cookie from Next.js cookie store
|
// Try to get cookie from Next.js cookie store
|
||||||
let sessionTokenFromStore = "NOT ACCESSIBLE"
|
let sessionTokenFromStore = "NOT ACCESSIBLE"
|
||||||
try {
|
try {
|
||||||
const cookieStore = await cookies()
|
const cookieStore = await cookies()
|
||||||
sessionTokenFromStore = cookieStore.get("__Secure-authjs.session-token")?.value || "NOT FOUND"
|
sessionTokenFromStore = cookieStore.get(SESSION_COOKIE_NAME)?.value || "NOT FOUND"
|
||||||
} catch {
|
} catch {
|
||||||
// Cookie store might not be accessible in all contexts
|
// Cookie store might not be accessible in all contexts
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { auth } from "@/lib/auth"
|
|||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
import { normalizeString } from "@/lib/utils"
|
import { normalizeString } from "@/lib/utils"
|
||||||
import { logActivity } from "@/lib/activity-log"
|
import { logActivity } from "@/lib/activity-log"
|
||||||
|
import { logger } from "@/lib/logger"
|
||||||
|
|
||||||
export async function POST(
|
export async function POST(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
@ -151,7 +152,9 @@ export async function POST(
|
|||||||
pointsChange
|
pointsChange
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error submitting guess:", error)
|
logger.error("Error submitting guess", {
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
})
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Internal server error" },
|
{ error: "Internal server error" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server"
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
import { auth } from "@/lib/auth"
|
import { auth } from "@/lib/auth"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
|
import { logger } from "@/lib/logger"
|
||||||
import { unlink } from "fs/promises"
|
import { unlink } from "fs/promises"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { existsSync } from "fs"
|
import { existsSync } from "fs"
|
||||||
@ -47,7 +48,10 @@ export async function DELETE(
|
|||||||
try {
|
try {
|
||||||
await unlink(filepath)
|
await unlink(filepath)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete file:", filepath, error)
|
logger.error("Failed to delete file", {
|
||||||
|
filepath,
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
})
|
||||||
// Continue with database deletion even if file deletion fails
|
// Continue with database deletion even if file deletion fails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,7 +64,9 @@ export async function DELETE(
|
|||||||
|
|
||||||
return NextResponse.json({ success: true, message: "Photo deleted successfully" })
|
return NextResponse.json({ success: true, message: "Photo deleted successfully" })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deleting photo:", error)
|
logger.error("Error deleting photo", {
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
})
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Internal server error" },
|
{ error: "Internal server error" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server"
|
|
||||||
import { auth } from "@/lib/auth"
|
|
||||||
import { prisma } from "@/lib/prisma"
|
|
||||||
import { sendNewPhotoEmail } from "@/lib/email"
|
|
||||||
|
|
||||||
// Legacy endpoint for URL-based uploads (kept for backward compatibility)
|
|
||||||
export async function POST(req: NextRequest) {
|
|
||||||
try {
|
|
||||||
const session = await auth()
|
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { url, answerName, points, maxAttempts } = await req.json()
|
|
||||||
|
|
||||||
if (!url || !answerName) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "URL and answer name are required" },
|
|
||||||
{ status: 400 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate points (must be positive integer, default to 1)
|
|
||||||
const pointsValue = points ? Math.max(1, parseInt(points, 10)) : 1
|
|
||||||
const maxAttemptsValue = maxAttempts && parseInt(maxAttempts, 10) > 0
|
|
||||||
? parseInt(maxAttempts, 10)
|
|
||||||
: null
|
|
||||||
|
|
||||||
// Check for duplicate URL
|
|
||||||
const existingPhoto = await prisma.photo.findFirst({
|
|
||||||
where: { url },
|
|
||||||
})
|
|
||||||
|
|
||||||
if (existingPhoto) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "This photo URL has already been uploaded (duplicate URL detected)" },
|
|
||||||
{ status: 409 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const photo = await prisma.photo.create({
|
|
||||||
data: {
|
|
||||||
uploaderId: session.user.id,
|
|
||||||
url,
|
|
||||||
answerName: answerName.trim(),
|
|
||||||
points: pointsValue,
|
|
||||||
maxAttempts: maxAttemptsValue,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
} as any,
|
|
||||||
include: {
|
|
||||||
uploader: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Send emails to all other users
|
|
||||||
const allUsers = await prisma.user.findMany({
|
|
||||||
where: {
|
|
||||||
id: { not: session.user.id },
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
email: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// Send emails asynchronously (don't wait for them)
|
|
||||||
Promise.all(
|
|
||||||
allUsers.map((user: { id: string; email: string; name: string }) =>
|
|
||||||
sendNewPhotoEmail(user.email, user.name, photo.id, photo.uploader.name).catch(
|
|
||||||
(err) => {
|
|
||||||
console.error("Failed to send email to:", user.email, err)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return NextResponse.json({ photo }, { status: 201 })
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating photo:", error)
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "Internal server error" },
|
|
||||||
{ status: 500 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Multiple Photo Upload Endpoint
|
||||||
|
*
|
||||||
|
* POST /api/photos/upload-multiple
|
||||||
|
*
|
||||||
|
* Uploads multiple photos in a single request. Supports both file uploads and URL-based uploads.
|
||||||
|
*
|
||||||
|
* This endpoint is used by the upload page for batch uploads. It processes multiple photos
|
||||||
|
* in parallel and sends email notifications for all successfully uploaded photos.
|
||||||
|
*
|
||||||
|
* Form Data:
|
||||||
|
* - photo_{index}_file: File object (optional, if using file upload)
|
||||||
|
* - photo_{index}_url: URL string (optional, if using URL upload)
|
||||||
|
* - photo_{index}_answerName: Answer name (required)
|
||||||
|
* - photo_{index}_points: Points value (optional, defaults to 1)
|
||||||
|
* - photo_{index}_penaltyEnabled: "true" or "false" (optional)
|
||||||
|
* - photo_{index}_penaltyPoints: Penalty points (optional)
|
||||||
|
* - photo_{index}_maxAttempts: Maximum attempts (optional)
|
||||||
|
* - count: Number of photos being uploaded
|
||||||
|
*
|
||||||
|
* Related endpoints:
|
||||||
|
* - POST /api/photos/upload - Single photo upload (supports both file and URL)
|
||||||
|
*/
|
||||||
import { NextRequest, NextResponse } from "next/server"
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
import { auth } from "@/lib/auth"
|
import { auth } from "@/lib/auth"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
import { sendNewPhotoEmail } from "@/lib/email"
|
import { sendNewPhotoEmail } from "@/lib/email"
|
||||||
|
import { logger } from "@/lib/logger"
|
||||||
import { writeFile } from "fs/promises"
|
import { writeFile } from "fs/promises"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { existsSync, mkdirSync } from "fs"
|
import { existsSync, mkdirSync } from "fs"
|
||||||
@ -204,7 +228,11 @@ export async function POST(req: NextRequest) {
|
|||||||
photo.id,
|
photo.id,
|
||||||
photo.uploader.name
|
photo.uploader.name
|
||||||
).catch((err) => {
|
).catch((err) => {
|
||||||
console.error("Failed to send email to:", user.email, "for photo:", photo.id, err)
|
logger.error("Failed to send email", {
|
||||||
|
email: user.email,
|
||||||
|
photoId: photo.id,
|
||||||
|
error: err instanceof Error ? err : new Error(String(err)),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -216,7 +244,9 @@ export async function POST(req: NextRequest) {
|
|||||||
{ status: 201 }
|
{ status: 201 }
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading photos:", error)
|
logger.error("Error uploading photos", {
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
})
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Internal server error" },
|
{ error: "Internal server error" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { auth } from "@/lib/auth"
|
|||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
import { sendNewPhotoEmail } from "@/lib/email"
|
import { sendNewPhotoEmail } from "@/lib/email"
|
||||||
import { logActivity } from "@/lib/activity-log"
|
import { logActivity } from "@/lib/activity-log"
|
||||||
|
import { logger } from "@/lib/logger"
|
||||||
import { writeFile } from "fs/promises"
|
import { writeFile } from "fs/promises"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { existsSync, mkdirSync } from "fs"
|
import { existsSync, mkdirSync } from "fs"
|
||||||
@ -86,9 +87,9 @@ export async function POST(req: NextRequest) {
|
|||||||
const uploadsDir = join(process.cwd(), "public", "uploads")
|
const uploadsDir = join(process.cwd(), "public", "uploads")
|
||||||
if (!existsSync(uploadsDir)) {
|
if (!existsSync(uploadsDir)) {
|
||||||
mkdirSync(uploadsDir, { recursive: true })
|
mkdirSync(uploadsDir, { recursive: true })
|
||||||
console.log(`[UPLOAD] Created uploads directory: ${uploadsDir}`)
|
// DEBUG level: directory creation is normal operation
|
||||||
|
logger.debug("Created uploads directory", { path: uploadsDir })
|
||||||
}
|
}
|
||||||
console.log(`[UPLOAD] Using uploads directory: ${uploadsDir} (exists: ${existsSync(uploadsDir)})`)
|
|
||||||
|
|
||||||
// Filename is generated server-side (timestamp + random), safe for path.join
|
// Filename is generated server-side (timestamp + random), safe for path.join
|
||||||
const filepath = join(uploadsDir, filename)
|
const filepath = join(uploadsDir, filename)
|
||||||
@ -98,9 +99,14 @@ export async function POST(req: NextRequest) {
|
|||||||
const { access } = await import("fs/promises")
|
const { access } = await import("fs/promises")
|
||||||
try {
|
try {
|
||||||
await access(filepath)
|
await access(filepath)
|
||||||
console.log(`[UPLOAD] File saved successfully: ${filepath}`)
|
// DEBUG level: file save verification is normal operation
|
||||||
|
logger.debug("File saved successfully", { filepath })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[UPLOAD] File write verification failed: ${filepath}`, error)
|
// ERROR level: file write failure is an error condition
|
||||||
|
logger.error("File write verification failed", {
|
||||||
|
filepath,
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
})
|
||||||
throw new Error("Failed to save file to disk")
|
throw new Error("Failed to save file to disk")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -166,7 +172,11 @@ export async function POST(req: NextRequest) {
|
|||||||
allUsers.map((user: { id: string; email: string; name: string }) =>
|
allUsers.map((user: { id: string; email: string; name: string }) =>
|
||||||
sendNewPhotoEmail(user.email, user.name, photo.id, photo.uploader.name).catch(
|
sendNewPhotoEmail(user.email, user.name, photo.id, photo.uploader.name).catch(
|
||||||
(err) => {
|
(err) => {
|
||||||
console.error("Failed to send email to:", user.email, err)
|
logger.error("Failed to send email", {
|
||||||
|
email: user.email,
|
||||||
|
photoId: photo.id,
|
||||||
|
error: err instanceof Error ? err : new Error(String(err)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -189,7 +199,9 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
return NextResponse.json({ photo }, { status: 201 })
|
return NextResponse.json({ photo }, { status: 201 })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error uploading photo:", error)
|
logger.error("Error uploading photo", {
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
})
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Internal server error" },
|
{ error: "Internal server error" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server"
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
import { auth } from "@/lib/auth"
|
import { auth } from "@/lib/auth"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
|
import { logger } from "@/lib/logger"
|
||||||
import bcrypt from "bcryptjs"
|
import bcrypt from "bcryptjs"
|
||||||
import { hashPassword } from "@/lib/utils"
|
import { hashPassword } from "@/lib/utils"
|
||||||
|
|
||||||
@ -44,7 +45,9 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
return NextResponse.json({ success: true })
|
return NextResponse.json({ success: true })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error changing password:", error)
|
logger.error("Error changing password", {
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
})
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Internal server error" },
|
{ error: "Internal server error" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server"
|
import { NextRequest, NextResponse } from "next/server"
|
||||||
|
import { logger } from "@/lib/logger"
|
||||||
import { readFile } from "fs/promises"
|
import { readFile } from "fs/promises"
|
||||||
import { join } from "path"
|
import { join } from "path"
|
||||||
import { existsSync } from "fs"
|
import { existsSync } from "fs"
|
||||||
@ -7,8 +8,9 @@ export async function GET(
|
|||||||
request: NextRequest,
|
request: NextRequest,
|
||||||
{ params }: { params: Promise<{ filename: string }> }
|
{ params }: { params: Promise<{ filename: string }> }
|
||||||
) {
|
) {
|
||||||
|
let filename: string | undefined
|
||||||
try {
|
try {
|
||||||
const { filename } = await params
|
filename = (await params).filename
|
||||||
|
|
||||||
// Sanitize filename - only allow alphanumeric, dots, hyphens
|
// Sanitize filename - only allow alphanumeric, dots, hyphens
|
||||||
if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
|
if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
|
||||||
@ -26,7 +28,11 @@ export async function GET(
|
|||||||
|
|
||||||
// Check if file exists
|
// Check if file exists
|
||||||
if (!existsSync(filepath)) {
|
if (!existsSync(filepath)) {
|
||||||
console.error(`[UPLOAD] File not found: ${filepath} (cwd: ${process.cwd()})`)
|
logger.warn("File not found", {
|
||||||
|
filepath,
|
||||||
|
filename,
|
||||||
|
cwd: process.cwd(),
|
||||||
|
})
|
||||||
return NextResponse.json({ error: "File not found" }, { status: 404 })
|
return NextResponse.json({ error: "File not found" }, { status: 404 })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +55,10 @@ export async function GET(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[UPLOAD] Error serving file:", error)
|
logger.error("Error serving file", {
|
||||||
|
filename: filename || "unknown",
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
})
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Internal server error" },
|
{ error: "Internal server error" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
@ -4,33 +4,34 @@ import { prisma } from "@/lib/prisma"
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import PhotoThumbnail from "@/components/PhotoThumbnail"
|
import PhotoThumbnail from "@/components/PhotoThumbnail"
|
||||||
import DeletePhotoButton from "@/components/DeletePhotoButton"
|
import DeletePhotoButton from "@/components/DeletePhotoButton"
|
||||||
|
import { logger } from "@/lib/logger"
|
||||||
|
|
||||||
// Enable caching for this page
|
// Enable caching for this page
|
||||||
export const revalidate = 60 // Revalidate every 60 seconds
|
export const revalidate = 60 // Revalidate every 60 seconds
|
||||||
|
|
||||||
export default async function PhotosPage() {
|
export default async function PhotosPage() {
|
||||||
console.log("PhotosPage: Starting, calling auth()...")
|
// DEBUG level: only logs in development or when LOG_LEVEL=DEBUG
|
||||||
|
logger.debug("PhotosPage: Starting, calling auth()")
|
||||||
const session = await auth()
|
const session = await auth()
|
||||||
|
|
||||||
console.log("PhotosPage: auth() returned", {
|
|
||||||
hasSession: !!session,
|
|
||||||
sessionType: typeof session,
|
|
||||||
sessionUser: session?.user,
|
|
||||||
sessionKeys: session ? Object.keys(session) : [],
|
|
||||||
sessionString: JSON.stringify(session, null, 2)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
console.log("PhotosPage: No session, redirecting to login")
|
logger.debug("PhotosPage: No session, redirecting to login")
|
||||||
redirect("/login")
|
redirect("/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!session.user) {
|
if (!session.user) {
|
||||||
console.log("PhotosPage: Session exists but no user, redirecting to login")
|
// WARN level: session exists but no user is a warning condition
|
||||||
|
logger.warn("PhotosPage: Session exists but no user, redirecting to login", {
|
||||||
|
hasSession: !!session,
|
||||||
|
sessionKeys: session ? Object.keys(session) : [],
|
||||||
|
})
|
||||||
redirect("/login")
|
redirect("/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("PhotosPage: Session valid, rendering page")
|
logger.debug("PhotosPage: Session valid, rendering page", {
|
||||||
|
userId: session.user.id,
|
||||||
|
userEmail: session.user.email,
|
||||||
|
})
|
||||||
|
|
||||||
// Limit to 50 photos per page for performance
|
// Limit to 50 photos per page for performance
|
||||||
const photos = await prisma.photo.findMany({
|
const photos = await prisma.photo.findMany({
|
||||||
|
|||||||
@ -15,3 +15,9 @@ SMTP_FROM="MirrorMatch <noreply@mirrormatch.com>"
|
|||||||
|
|
||||||
# In development, emails will be logged to console or use Ethereal
|
# In development, emails will be logged to console or use Ethereal
|
||||||
# No SMTP config needed for dev mode
|
# No SMTP config needed for dev mode
|
||||||
|
|
||||||
|
# Logging Configuration
|
||||||
|
# LOG_LEVEL: DEBUG, INFO, WARN, ERROR, or NONE (default: DEBUG in dev, INFO in production)
|
||||||
|
# LOG_FORMAT: "json" for structured JSON logs, or omit for human-readable format
|
||||||
|
# LOG_LEVEL=INFO
|
||||||
|
# LOG_FORMAT=json
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Activity logging utility for tracking user actions
|
* Activity logging utility for tracking user actions
|
||||||
|
* Uses structured logging with log levels for better production control
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { logger } from './logger'
|
||||||
|
|
||||||
export interface ActivityLog {
|
export interface ActivityLog {
|
||||||
timestamp: string
|
timestamp: string
|
||||||
userId?: string
|
userId?: string
|
||||||
@ -14,6 +17,18 @@ export interface ActivityLog {
|
|||||||
details?: Record<string, unknown>
|
details?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log user activity with structured data
|
||||||
|
* Uses INFO level logging - can be filtered via LOG_LEVEL environment variable
|
||||||
|
*
|
||||||
|
* @param action - The action being performed (e.g., "PHOTO_UPLOAD", "GUESS_SUBMITTED")
|
||||||
|
* @param path - The request path
|
||||||
|
* @param method - The HTTP method
|
||||||
|
* @param user - Optional user object (id, email, role)
|
||||||
|
* @param details - Optional additional context data
|
||||||
|
* @param request - Optional Request object for extracting IP address
|
||||||
|
* @returns Structured activity log object
|
||||||
|
*/
|
||||||
export function logActivity(
|
export function logActivity(
|
||||||
action: string,
|
action: string,
|
||||||
path: string,
|
path: string,
|
||||||
@ -21,7 +36,7 @@ export function logActivity(
|
|||||||
user?: { id: string; email: string; role: string } | null,
|
user?: { id: string; email: string; role: string } | null,
|
||||||
details?: Record<string, unknown>,
|
details?: Record<string, unknown>,
|
||||||
request?: Request
|
request?: Request
|
||||||
) {
|
): ActivityLog {
|
||||||
const timestamp = new Date().toISOString()
|
const timestamp = new Date().toISOString()
|
||||||
const ip = request?.headers.get("x-forwarded-for") ||
|
const ip = request?.headers.get("x-forwarded-for") ||
|
||||||
request?.headers.get("x-real-ip") ||
|
request?.headers.get("x-real-ip") ||
|
||||||
@ -39,14 +54,17 @@ export function logActivity(
|
|||||||
details
|
details
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format: [ACTION] timestamp | method path | User: email (role) | IP: ip | Details: {...}
|
// Use structured logging with INFO level
|
||||||
const userInfo = user
|
// This allows filtering via LOG_LEVEL environment variable
|
||||||
? `${user.email} (${user.role})`
|
logger.info(`Activity: ${action}`, {
|
||||||
: "UNAUTHENTICATED"
|
method,
|
||||||
|
path,
|
||||||
const detailsStr = details ? ` | Details: ${JSON.stringify(details)}` : ""
|
userId: user?.id,
|
||||||
|
userEmail: user?.email,
|
||||||
console.log(`[${action}] ${timestamp} | ${method} ${path} | User: ${userInfo} | IP: ${ip.split(",")[0].trim()}${detailsStr}`)
|
userRole: user?.role,
|
||||||
|
ip: ip.split(",")[0].trim(),
|
||||||
|
...(details && { details }),
|
||||||
|
})
|
||||||
|
|
||||||
return log
|
return log
|
||||||
}
|
}
|
||||||
|
|||||||
39
lib/auth.ts
39
lib/auth.ts
@ -2,6 +2,8 @@ import NextAuth from "next-auth"
|
|||||||
import Credentials from "next-auth/providers/credentials"
|
import Credentials from "next-auth/providers/credentials"
|
||||||
import { prisma } from "./prisma"
|
import { prisma } from "./prisma"
|
||||||
import bcrypt from "bcryptjs"
|
import bcrypt from "bcryptjs"
|
||||||
|
import { logger } from "./logger"
|
||||||
|
import { SESSION_COOKIE_NAME } from "./constants"
|
||||||
|
|
||||||
const nextAuthSecret = process.env.NEXTAUTH_SECRET
|
const nextAuthSecret = process.env.NEXTAUTH_SECRET
|
||||||
if (!nextAuthSecret) {
|
if (!nextAuthSecret) {
|
||||||
@ -48,7 +50,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
role: user.role,
|
role: user.role,
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Auth authorize error:", err)
|
logger.error("Auth authorize error", err instanceof Error ? err : new Error(String(err)))
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,27 +63,22 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
token.role = (user as { role: string }).role
|
token.role = (user as { role: string }).role
|
||||||
token.email = user.email
|
token.email = user.email
|
||||||
token.name = user.name
|
token.name = user.name
|
||||||
console.log("JWT callback: user added to token", { userId: user.id, email: user.email })
|
// DEBUG level: only logs in development or when LOG_LEVEL=DEBUG
|
||||||
|
logger.debug("JWT callback: user added to token", {
|
||||||
|
userId: user.id,
|
||||||
|
email: user.email
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
console.log("JWT callback: no user, token exists", {
|
// DEBUG level: token refresh (normal operation, only log in debug mode)
|
||||||
|
logger.debug("JWT callback: token refresh", {
|
||||||
hasToken: !!token,
|
hasToken: !!token,
|
||||||
tokenKeys: token ? Object.keys(token) : [],
|
|
||||||
tokenId: token?.id,
|
tokenId: token?.id,
|
||||||
tokenEmail: token?.email,
|
tokenEmail: token?.email,
|
||||||
tokenName: token?.name,
|
|
||||||
tokenRole: token?.role
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return token
|
return token
|
||||||
},
|
},
|
||||||
async session({ session, token }) {
|
async session({ session, token }) {
|
||||||
console.log("Session callback: called", {
|
|
||||||
hasToken: !!token,
|
|
||||||
hasSession: !!session,
|
|
||||||
tokenId: token?.id,
|
|
||||||
tokenEmail: token?.email,
|
|
||||||
stackTrace: new Error().stack?.split('\n').slice(1, 4).join('\n')
|
|
||||||
})
|
|
||||||
// Always ensure session.user exists when token exists
|
// Always ensure session.user exists when token exists
|
||||||
if (token && (token.id || token.email)) {
|
if (token && (token.id || token.email)) {
|
||||||
session.user = {
|
session.user = {
|
||||||
@ -91,23 +88,17 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
name: (token.name as string) || session.user?.name || "",
|
name: (token.name as string) || session.user?.name || "",
|
||||||
role: token.role as string,
|
role: token.role as string,
|
||||||
}
|
}
|
||||||
console.log("Session callback: session created", {
|
// DEBUG level: session creation is normal operation, only log in debug mode
|
||||||
|
logger.debug("Session callback: session created", {
|
||||||
userId: token.id,
|
userId: token.id,
|
||||||
email: token.email,
|
email: token.email,
|
||||||
hasUser: !!session.user,
|
|
||||||
userKeys: session.user ? Object.keys(session.user) : [],
|
|
||||||
userRole: token.role,
|
userRole: token.role,
|
||||||
sessionUser: session.user,
|
|
||||||
sessionExpires: session.expires,
|
|
||||||
fullSession: JSON.stringify(session, null, 2)
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
console.warn("Session callback: token missing or invalid", {
|
// WARN level: token missing/invalid is a warning condition
|
||||||
|
logger.warn("Session callback: token missing or invalid", {
|
||||||
hasToken: !!token,
|
hasToken: !!token,
|
||||||
tokenKeys: token ? Object.keys(token) : [],
|
|
||||||
hasSession: !!session,
|
hasSession: !!session,
|
||||||
sessionKeys: session ? Object.keys(session) : [],
|
|
||||||
sessionUser: session?.user,
|
|
||||||
tokenId: token?.id,
|
tokenId: token?.id,
|
||||||
tokenEmail: token?.email
|
tokenEmail: token?.email
|
||||||
})
|
})
|
||||||
@ -126,7 +117,7 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
|
|||||||
},
|
},
|
||||||
cookies: {
|
cookies: {
|
||||||
sessionToken: {
|
sessionToken: {
|
||||||
name: `__Secure-authjs.session-token`,
|
name: SESSION_COOKIE_NAME,
|
||||||
options: {
|
options: {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: "lax",
|
sameSite: "lax",
|
||||||
|
|||||||
9
lib/constants.ts
Normal file
9
lib/constants.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
/**
|
||||||
|
* Application-wide constants
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NextAuth session cookie name
|
||||||
|
* Must match the cookie name defined in lib/auth.ts
|
||||||
|
*/
|
||||||
|
export const SESSION_COOKIE_NAME = "__Secure-authjs.session-token"
|
||||||
156
lib/logger.ts
Normal file
156
lib/logger.ts
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
/**
|
||||||
|
* Structured logging utility with log levels and environment-based filtering
|
||||||
|
*
|
||||||
|
* Log levels (in order of severity):
|
||||||
|
* - DEBUG: Detailed information for debugging (only in development)
|
||||||
|
* - INFO: General informational messages
|
||||||
|
* - WARN: Warning messages for potentially harmful situations
|
||||||
|
* - ERROR: Error messages for error events
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { logger } from '@/lib/logger'
|
||||||
|
* logger.debug('Debug message', { data })
|
||||||
|
* logger.info('Info message', { data })
|
||||||
|
* logger.warn('Warning message', { data })
|
||||||
|
* logger.error('Error message', { error })
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum LogLevel {
|
||||||
|
DEBUG = 0,
|
||||||
|
INFO = 1,
|
||||||
|
WARN = 2,
|
||||||
|
ERROR = 3,
|
||||||
|
NONE = 4, // Disable all logging
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LogContext {
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Logger {
|
||||||
|
debug(message: string, context?: LogContext): void;
|
||||||
|
info(message: string, context?: LogContext): void;
|
||||||
|
warn(message: string, context?: LogContext): void;
|
||||||
|
error(message: string, context?: LogContext | Error): void;
|
||||||
|
isLevelEnabled(level: LogLevel): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse log level from environment variable or default based on NODE_ENV
|
||||||
|
*/
|
||||||
|
function getLogLevel(): LogLevel {
|
||||||
|
const envLogLevel = process.env.LOG_LEVEL?.toUpperCase();
|
||||||
|
|
||||||
|
// If explicitly set, use that
|
||||||
|
if (envLogLevel) {
|
||||||
|
switch (envLogLevel) {
|
||||||
|
case 'DEBUG':
|
||||||
|
return LogLevel.DEBUG;
|
||||||
|
case 'INFO':
|
||||||
|
return LogLevel.INFO;
|
||||||
|
case 'WARN':
|
||||||
|
return LogLevel.WARN;
|
||||||
|
case 'ERROR':
|
||||||
|
return LogLevel.ERROR;
|
||||||
|
case 'NONE':
|
||||||
|
return LogLevel.NONE;
|
||||||
|
default:
|
||||||
|
// Invalid value, fall through to default behavior
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default behavior: DEBUG in development, INFO in production
|
||||||
|
return process.env.NODE_ENV === 'production' ? LogLevel.INFO : LogLevel.DEBUG;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format log entry as structured JSON or human-readable string
|
||||||
|
*/
|
||||||
|
function formatLog(
|
||||||
|
level: LogLevel,
|
||||||
|
message: string,
|
||||||
|
context?: LogContext | Error
|
||||||
|
): string {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const levelName = LogLevel[level];
|
||||||
|
|
||||||
|
// If structured logging is enabled, output JSON
|
||||||
|
if (process.env.LOG_FORMAT === 'json') {
|
||||||
|
const logEntry: Record<string, unknown> = {
|
||||||
|
timestamp,
|
||||||
|
level: levelName,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (context) {
|
||||||
|
if (context instanceof Error) {
|
||||||
|
logEntry.error = {
|
||||||
|
name: context.name,
|
||||||
|
message: context.message,
|
||||||
|
stack: context.stack,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
Object.assign(logEntry, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(logEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Human-readable format
|
||||||
|
const contextStr = context
|
||||||
|
? context instanceof Error
|
||||||
|
? ` | Error: ${context.name}: ${context.message}`
|
||||||
|
: ` | ${JSON.stringify(context)}`
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `[${levelName}] ${timestamp} | ${message}${contextStr}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a logger instance with the configured log level
|
||||||
|
*/
|
||||||
|
function createLogger(): Logger {
|
||||||
|
const currentLevel = getLogLevel();
|
||||||
|
|
||||||
|
const shouldLog = (level: LogLevel): boolean => {
|
||||||
|
return level >= currentLevel;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
debug(message: string, context?: LogContext): void {
|
||||||
|
if (shouldLog(LogLevel.DEBUG)) {
|
||||||
|
console.log(formatLog(LogLevel.DEBUG, message, context));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
info(message: string, context?: LogContext): void {
|
||||||
|
if (shouldLog(LogLevel.INFO)) {
|
||||||
|
console.log(formatLog(LogLevel.INFO, message, context));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
warn(message: string, context?: LogContext): void {
|
||||||
|
if (shouldLog(LogLevel.WARN)) {
|
||||||
|
console.warn(formatLog(LogLevel.WARN, message, context));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
error(message: string, context?: LogContext | Error): void {
|
||||||
|
if (shouldLog(LogLevel.ERROR)) {
|
||||||
|
console.error(formatLog(LogLevel.ERROR, message, context));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isLevelEnabled(level: LogLevel): boolean {
|
||||||
|
return shouldLog(level);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export singleton logger instance
|
||||||
|
export const logger = createLogger();
|
||||||
|
|
||||||
|
// Export for testing
|
||||||
|
export { getLogLevel, formatLog, createLogger };
|
||||||
@ -1,5 +1,31 @@
|
|||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Next.js Configuration
|
||||||
|
*
|
||||||
|
* Configuration decisions:
|
||||||
|
*
|
||||||
|
* 1. Image Optimization:
|
||||||
|
* - Enabled (unoptimized: false) for better performance and bandwidth usage
|
||||||
|
* - Remote patterns allow all hosts (http/https) - this is permissive but necessary
|
||||||
|
* for the photo guessing game where users may upload images from any URL.
|
||||||
|
* - Consider restricting in production if security is a concern, but this would
|
||||||
|
* limit functionality for URL-based photo uploads.
|
||||||
|
*
|
||||||
|
* 2. Turbopack Configuration:
|
||||||
|
* - Currently configured but not actively used (dev script uses --webpack flag)
|
||||||
|
* - Kept for future migration to Turbopack when stable
|
||||||
|
* - Can be removed if not planning to use Turbopack
|
||||||
|
*
|
||||||
|
* 3. Webpack Configuration:
|
||||||
|
* - Prisma client is externalized on server-side to prevent bundling issues
|
||||||
|
* - This is necessary because Prisma generates client code that shouldn't be bundled
|
||||||
|
* - Required for proper Prisma functionality in Next.js
|
||||||
|
*
|
||||||
|
* 4. Page Extensions:
|
||||||
|
* - Only processes TypeScript/JavaScript files (ts, tsx, js, jsx)
|
||||||
|
* - Prevents processing of other file types as pages
|
||||||
|
*/
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
// Only process specific file extensions
|
// Only process specific file extensions
|
||||||
pageExtensions: ["ts", "tsx", "js", "jsx"],
|
pageExtensions: ["ts", "tsx", "js", "jsx"],
|
||||||
@ -16,10 +42,15 @@ const nextConfig: NextConfig = {
|
|||||||
hostname: "**",
|
hostname: "**",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
unoptimized: false, // Enable optimization for better performance
|
// Enable optimization for better performance and bandwidth usage
|
||||||
|
// Note: Remote patterns are permissive to allow URL-based photo uploads
|
||||||
|
// Consider restricting in production if security is a concern
|
||||||
|
unoptimized: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Configure Turbopack
|
// Configure Turbopack (currently not used - dev script uses --webpack)
|
||||||
|
// Kept for future migration when Turbopack is stable
|
||||||
|
// Can be removed if not planning to use Turbopack
|
||||||
turbopack: {
|
turbopack: {
|
||||||
resolveExtensions: [
|
resolveExtensions: [
|
||||||
".tsx",
|
".tsx",
|
||||||
@ -38,6 +69,7 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Webpack configuration to externalize Prisma
|
// Webpack configuration to externalize Prisma
|
||||||
|
// Required: Prisma client must be externalized on server-side to prevent bundling issues
|
||||||
webpack: (config, { isServer }) => {
|
webpack: (config, { isServer }) => {
|
||||||
if (isServer) {
|
if (isServer) {
|
||||||
config.externals = config.externals || [];
|
config.externals = config.externals || [];
|
||||||
|
|||||||
41
proxy.ts
41
proxy.ts
@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
import type { NextRequest } from "next/server"
|
import type { NextRequest } from "next/server"
|
||||||
import { getToken } from "next-auth/jwt"
|
import { getToken } from "next-auth/jwt"
|
||||||
|
import { SESSION_COOKIE_NAME } from "./lib/constants"
|
||||||
|
import { logActivity } from "./lib/activity-log"
|
||||||
|
|
||||||
export async function proxy(request: NextRequest) {
|
export async function proxy(request: NextRequest) {
|
||||||
const pathname = request.nextUrl.pathname
|
const pathname = request.nextUrl.pathname
|
||||||
@ -11,30 +13,35 @@ export async function proxy(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get token (works in Edge runtime)
|
// Get token (works in Edge runtime)
|
||||||
// Explicitly specify the cookie name to match NextAuth config
|
// Use constant for cookie name to match NextAuth config
|
||||||
const cookieName = "__Secure-authjs.session-token"
|
|
||||||
const token = await getToken({
|
const token = await getToken({
|
||||||
req: request,
|
req: request,
|
||||||
secret: process.env.NEXTAUTH_SECRET,
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
cookieName: cookieName
|
cookieName: SESSION_COOKIE_NAME
|
||||||
})
|
})
|
||||||
|
|
||||||
// User activity logging - track all page visits and API calls
|
// User activity logging - track all page visits and API calls
|
||||||
const timestamp = new Date().toISOString()
|
// Uses structured logging with log levels (INFO level, can be filtered)
|
||||||
const userAgent = request.headers.get("user-agent") || "unknown"
|
const user = token ? {
|
||||||
const ip = request.headers.get("x-forwarded-for") ||
|
id: token.id as string,
|
||||||
request.headers.get("x-real-ip") ||
|
email: token.email as string,
|
||||||
"unknown"
|
role: token.role as string,
|
||||||
const referer = request.headers.get("referer") || "direct"
|
} : null
|
||||||
const method = request.method
|
|
||||||
|
|
||||||
if (token) {
|
const referer = request.headers.get("referer") || "direct"
|
||||||
// Log authenticated user activity
|
const userAgent = request.headers.get("user-agent") || "unknown"
|
||||||
console.log(`[ACTIVITY] ${timestamp} | ${method} ${pathname} | User: ${token.email} (${token.role}) | IP: ${ip} | Referer: ${referer}`)
|
|
||||||
} else {
|
logActivity(
|
||||||
// Log unauthenticated access attempts
|
token ? "PAGE_VIEW" : "UNAUTHENTICATED_ACCESS",
|
||||||
console.log(`[ACTIVITY] ${timestamp} | ${method} ${pathname} | User: UNAUTHENTICATED | IP: ${ip} | Referer: ${referer} | UA: ${userAgent.substring(0, 100)}`)
|
pathname,
|
||||||
}
|
request.method,
|
||||||
|
user,
|
||||||
|
{
|
||||||
|
referer,
|
||||||
|
userAgent: userAgent.substring(0, 100), // Limit length
|
||||||
|
},
|
||||||
|
request
|
||||||
|
)
|
||||||
|
|
||||||
// Protected routes - require authentication
|
// Protected routes - require authentication
|
||||||
if (!token) {
|
if (!token) {
|
||||||
|
|||||||
@ -1,10 +1,29 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Watch user activity logs in real-time
|
# Watch user activity logs in real-time
|
||||||
|
#
|
||||||
|
# This script monitors systemd journal logs for MirrorMatch activity.
|
||||||
|
# It filters for activity-related log entries including page visits, photo uploads, and guess submissions.
|
||||||
|
#
|
||||||
# Usage: ./watch-activity.sh
|
# Usage: ./watch-activity.sh
|
||||||
|
#
|
||||||
|
# Requirements:
|
||||||
|
# - Systemd/journalctl (Linux systems with systemd)
|
||||||
|
# - Appropriate permissions (may require sudo)
|
||||||
|
# - Application running as a systemd service named "app-backend"
|
||||||
|
#
|
||||||
|
# For local development without systemd:
|
||||||
|
# - Check application console output directly
|
||||||
|
# - Use `npm run dev` and monitor terminal output
|
||||||
|
# - Or redirect logs to a file and tail it: `npm run dev > app.log 2>&1 && tail -f app.log`
|
||||||
|
#
|
||||||
|
# Note: With the new structured logging system, you can also filter by log level:
|
||||||
|
# - Set LOG_LEVEL=DEBUG to see all activity logs
|
||||||
|
# - Set LOG_FORMAT=json for structured JSON logs
|
||||||
|
|
||||||
echo "Watching user activity logs..."
|
echo "Watching user activity logs..."
|
||||||
echo "Press Ctrl+C to stop"
|
echo "Press Ctrl+C to stop"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Watch for activity logs (ACTIVITY, PHOTO_UPLOAD, GUESS_SUBMIT)
|
# Watch for activity logs (ACTIVITY, PHOTO_UPLOAD, GUESS_SUBMIT)
|
||||||
sudo journalctl -u app-backend -f | grep -E "\[ACTIVITY\]|\[PHOTO_UPLOAD\]|\[GUESS_SUBMIT\]"
|
# These patterns match the activity log format from lib/activity-log.ts
|
||||||
|
sudo journalctl -u app-backend -f | grep -E "\[ACTIVITY\]|\[PHOTO_UPLOAD\]|\[GUESS_SUBMIT\]|Activity:"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user