feat: implement report submission with photo upload and map display

- Fix Prisma client generation and database schema sync issues
- Implement user auto-creation from Clerk authentication
- Add raw SQL queries to handle MySQL POINT spatial fields
- Make password_hash optional for Clerk-only authentication
- Update MapPage to fetch real reports from API
- Add photo display in map popups with full-size modal view
- Configure CORS and Helmet for cross-origin image loading
- Add cleanup tracking task to implementation plan

Fixes report submission 500 errors and enables end-to-end workflow from submission to map display.
This commit is contained in:
2025-11-16 20:01:26 +00:00
parent a58e8da0e1
commit 64fe312f01
7 changed files with 180 additions and 39 deletions

View File

@@ -2,9 +2,44 @@ import { Response } from 'express';
import { AuthRequest } from '../middleware/auth';
import { reportService } from '../services/reportService';
import { extractExifData } from '../utils/exif';
import path from 'path';
import prisma from '../config/database';
import { clerkClient } from '@clerk/clerk-sdk-node';
export class ReportController {
// Helper to find or create user from Clerk ID
private async findOrCreateUser(clerkId: string): Promise<string> {
// Check if user exists
let user = await prisma.user.findUnique({
where: { clerkId },
select: { id: true },
});
if (user) {
return user.id;
}
// User doesn't exist, fetch from Clerk and create
try {
const clerkUser = await clerkClient.users.getUser(clerkId);
user = await prisma.user.create({
data: {
clerkId,
email: clerkUser.emailAddresses[0]?.emailAddress || `${clerkId}@temp.com`,
username: clerkUser.username || `user_${clerkId.substring(0, 8)}`,
firstName: clerkUser.firstName,
lastName: clerkUser.lastName,
avatarUrl: clerkUser.imageUrl,
},
select: { id: true },
});
return user.id;
} catch (error) {
console.error('Error creating user from Clerk:', error);
throw new Error('Failed to create user');
}
}
async uploadPhoto(req: AuthRequest, res: Response) {
try {
if (!req.file) {
@@ -88,8 +123,11 @@ export class ReportController {
});
}
// Find or create user in database
const dbUserId = await this.findOrCreateUser(req.userId);
const report = await reportService.createReport({
userId: req.userId,
userId: dbUserId,
latitude: parseFloat(latitude),
longitude: parseFloat(longitude),
locationSource,

View File

@@ -12,7 +12,10 @@ const app = express();
const PORT = process.env.PORT || 3005;
// Middleware
app.use(helmet());
app.use(helmet({
crossOriginResourcePolicy: { policy: "cross-origin" },
contentSecurityPolicy: false, // Disable CSP for development
}));
app.use(cors({
origin: process.env.CORS_ORIGIN || 'http://localhost:5173',
credentials: true,

View File

@@ -20,27 +20,43 @@ export interface CreateReportData {
export class ReportService {
async createReport(data: CreateReportData) {
const report = await prisma.report.create({
data: {
userId: data.userId,
latitude: new Prisma.Decimal(data.latitude),
longitude: new Prisma.Decimal(data.longitude),
locationSource: data.locationSource,
photoUrls: data.photoUrls,
photoTimestamp: data.photoTimestamp,
litterType: data.litterType,
quantity: data.quantity,
description: data.description,
environmentalConcerns: data.environmentalConcerns
? {
create: data.environmentalConcerns.map((concern) => ({
concernType: concern.type,
severity: concern.severity,
description: concern.description,
})),
}
: undefined,
},
// Create report using raw SQL to handle the POINT field
const reportId = await prisma.$queryRaw<Array<{ id: string }>>`
INSERT INTO reports (
user_id, location, latitude, longitude, location_source,
photo_urls, photo_timestamp, litter_type, quantity, description
) VALUES (
${data.userId},
ST_GeomFromText(${`POINT(${data.longitude} ${data.latitude})`}, 4326),
${data.latitude},
${data.longitude},
${data.locationSource},
${JSON.stringify(data.photoUrls)},
${data.photoTimestamp || null},
${data.litterType},
${data.quantity},
${data.description || null}
)
`;
// Get the created report ID
const createdId = await prisma.$queryRaw<Array<{ id: string }>>`
SELECT id FROM reports ORDER BY created_at DESC LIMIT 1
`;
// Create environmental concerns if provided
if (data.environmentalConcerns && data.environmentalConcerns.length > 0) {
for (const concern of data.environmentalConcerns) {
await prisma.$queryRaw`
INSERT INTO environmental_concerns (report_id, concern_type, severity, description)
VALUES (${createdId[0].id}, ${concern.type}, ${concern.severity}, ${concern.description})
`;
}
}
// Fetch and return the complete report
const report = await prisma.report.findUnique({
where: { id: createdId[0].id },
include: {
user: {
select: {