Set up backend API server with Express and Prisma

- Initialize Node.js/Express backend project structure
- Configure TypeScript for backend development
- Set up Prisma ORM with MySQL database schema
- Implement authentication middleware with Clerk
- Create photo upload endpoint with EXIF extraction
- Build report CRUD endpoints (create, read, delete)
- Add file upload handling with Multer
- Configure CORS and security middleware (helmet)
- Set up error handling and validation
- Create development environment configuration

Task 21 complete: Backend foundation ready for report submissions
This commit is contained in:
2025-11-16 17:29:26 +00:00
parent 7dc1ff740b
commit c0ec28c5c9
15 changed files with 3191 additions and 0 deletions

View File

@@ -0,0 +1,56 @@
import { Request, Response, NextFunction } from 'express';
import { clerkClient } from '@clerk/clerk-sdk-node';
export interface AuthRequest extends Request {
userId?: string;
user?: any;
}
export const requireAuth = async (
req: AuthRequest,
res: Response,
next: NextFunction
) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'No authorization token provided',
timestamp: new Date().toISOString(),
},
});
}
const token = authHeader.substring(7);
// Verify the session token with Clerk
const session = await clerkClient.sessions.verifySession(token, token);
if (!session) {
return res.status(401).json({
error: {
code: 'INVALID_TOKEN',
message: 'Invalid or expired token',
timestamp: new Date().toISOString(),
},
});
}
// Attach user ID to request
req.userId = session.userId;
next();
} catch (error) {
console.error('Auth middleware error:', error);
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Authentication failed',
timestamp: new Date().toISOString(),
},
});
}
};

View File

@@ -0,0 +1,40 @@
import multer from 'multer';
import path from 'path';
import fs from 'fs';
// Ensure upload directory exists
const uploadDir = process.env.UPLOAD_DIR || './uploads';
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
// Configure storage
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
cb(null, file.fieldname + '-' + uniqueSuffix + path.extname(file.originalname));
},
});
// File filter
const fileFilter = (req: any, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only JPEG, PNG, and WebP images are allowed.'));
}
};
// Configure multer
export const upload = multer({
storage,
fileFilter,
limits: {
fileSize: parseInt(process.env.MAX_FILE_SIZE || '5242880'), // 5MB default
},
});