diff --git a/apps/web/app/api/incidents/route.ts b/apps/web/app/api/incidents/route.ts new file mode 100644 index 0000000..b5bc43f --- /dev/null +++ b/apps/web/app/api/incidents/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { validateIncidentReport } from '@/lib/validations/incident-report'; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const validation = validateIncidentReport(body); + + if (!validation.success) { + return NextResponse.json( + { + success: false, + message: 'Validation failed', + errors: validation.errors, + }, + { status: 400 } + ); + } + + const incidentData = validation.data; + + return NextResponse.json( + { + success: true, + message: 'Incident report submitted successfully', + data: { id: 'incident-' + Date.now(), ...incidentData }, + }, + { status: 201 } + ); + } catch (error) { + if (error instanceof SyntaxError) { + return NextResponse.json( + { success: false, message: 'Invalid JSON format' }, + { status: 400 } + ); + } + + console.error('Incident submission error:', error); + return NextResponse.json( + { success: false, message: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/apps/web/lib/validations/incident-report.ts b/apps/web/lib/validations/incident-report.ts new file mode 100644 index 0000000..57e3114 --- /dev/null +++ b/apps/web/lib/validations/incident-report.ts @@ -0,0 +1,61 @@ +import { z } from 'zod'; + +// Validation schema for incident report form +export const IncidentReportSchema = z.object({ + description: z + .string() + .trim() + .min(10, 'Description must be at least 10 characters') + .max(1000, 'Description must not exceed 1000 characters'), + + location: z.object({ + latitude: z + .number() + .min(-90, 'Latitude must be between -90 and 90') + .max(90, 'Latitude must be between -90 and 90'), + longitude: z + .number() + .min(-180, 'Longitude must be between -180 and 180') + .max(180, 'Longitude must be between -180 and 180'), + }), + + category: z + .enum(['kidnapping', 'suspicious', 'road-block', 'other']), + + contact: z + .object({ + type: z.enum(['phone', 'email']).optional(), + value: z.string().optional(), + }) + .optional() + .refine((val) => { + if (!val || !val.value) return true; + const phoneRegex = /^\+234[0-9]{10}$/; + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return phoneRegex.test(val.value) || emailRegex.test(val.value); + }, 'Invalid phone or email format'), + + photos: z + .array( + z.object({ + name: z.string(), + size: z.number().max(5242880, 'Photo must not exceed 5MB'), + type: z.string().regex(/^image\/(jpeg|png|gif|webp)$/, 'Only JPEG, PNG, GIF, WebP allowed'), + }) + ) + .max(5, 'Maximum 5 photos allowed') + .optional(), +}); + +export type IncidentReport = z.infer; + +export function validateIncidentReport(data: unknown) { + const result = IncidentReportSchema.safeParse(data); + if (!result.success) { + return { + success: false, + error: result.error.flatten(), + }; + } + return { success: true, data: result.data }; +} diff --git a/packages/database/tests/auth.test.ts b/packages/database/tests/auth.test.ts new file mode 100644 index 0000000..9342604 --- /dev/null +++ b/packages/database/tests/auth.test.ts @@ -0,0 +1,160 @@ +import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals'; + +// Mock authentication service module +const mockAuthService = { + registerUser: jest.fn(), + loginUser: jest.fn(), + generateJWT: jest.fn(), + validateJWT: jest.fn(), + hashPassword: jest.fn(), + comparePassword: jest.fn(), + validatePhoneNumber: jest.fn(), +}; + +describe('Authentication Service', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('User Registration', () => { + it('should register user with valid data', async () => { + const userData = { + email: 'test@example.com', + password: 'SecurePass123', + phoneNumber: '+234803000000', + }; + + mockAuthService.registerUser.mockResolvedValue({ + id: '1', + email: userData.email, + phoneNumber: userData.phoneNumber, + }); + + const result = await mockAuthService.registerUser(userData); + + expect(result).toBeDefined(); + expect(result.email).toBe(userData.email); + expect(mockAuthService.registerUser).toHaveBeenCalledWith(userData); + }); + + it('should reject registration with duplicate email', async () => { + const userData = { + email: 'duplicate@example.com', + password: 'SecurePass123', + phoneNumber: '+234803000001', + }; + + mockAuthService.registerUser.mockRejectedValue( + new Error('Email already exists') + ); + + await expect(mockAuthService.registerUser(userData)).rejects.toThrow( + 'Email already exists' + ); + }); + }); + + describe('User Login', () => { + it('should login with correct credentials', async () => { + const loginData = { + email: 'test@example.com', + password: 'SecurePass123', + }; + + mockAuthService.loginUser.mockResolvedValue({ + id: '1', + email: loginData.email, + token: 'jwt_token_here', + }); + + const result = await mockAuthService.loginUser(loginData); + + expect(result).toBeDefined(); + expect(result.email).toBe(loginData.email); + expect(result.token).toBeDefined(); + }); + + it('should reject login with incorrect credentials', async () => { + mockAuthService.loginUser.mockRejectedValue( + new Error('Invalid credentials') + ); + + await expect(mockAuthService.loginUser({email: 'test@example.com', password: 'WrongPassword'})).rejects.toThrow( + 'Invalid credentials' + ); + }); + }); + + describe('JWT Token Generation and Validation', () => { + it('should generate valid JWT token', () => { + const tokenPayload = { userId: '1', email: 'test@example.com' }; + const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxIn0.signature'; + + mockAuthService.generateJWT.mockReturnValue(token); + const result = mockAuthService.generateJWT(tokenPayload); + + expect(result).toBeDefined(); + expect(typeof result).toBe('string'); + }); + + it('should validate correct JWT token', () => { + const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'; + mockAuthService.validateJWT.mockReturnValue({ + userId: '1', + email: 'test@example.com', + }); + + const result = mockAuthService.validateJWT(validToken); + expect(result).toBeDefined(); + expect(result.userId).toBe('1'); + }); + + it('should reject invalid JWT token', () => { + mockAuthService.validateJWT.mockReturnValue(null); + const result = mockAuthService.validateJWT('invalid.token'); + expect(result).toBeNull(); + }); + }); + + describe('Password Management', () => { + it('should hash password securely', () => { + const password = 'SecurePass123'; + const hashedPassword = '$2b$10$hashedPasswordString'; + + mockAuthService.hashPassword.mockReturnValue(hashedPassword); + const result = mockAuthService.hashPassword(password); + + expect(result).toBeDefined(); + expect(result).not.toBe(password); + expect(typeof result).toBe('string'); + }); + + it('should compare password with hash', () => { + mockAuthService.comparePassword.mockReturnValue(true); + const result = mockAuthService.comparePassword('SecurePass123', '$2b$10$hash'); + expect(result).toBe(true); + }); + + it('should reject incorrect password', () => { + mockAuthService.comparePassword.mockReturnValue(false); + const result = mockAuthService.comparePassword('WrongPassword', '$2b$10$hash'); + expect(result).toBe(false); + }); + }); + + describe('Phone Number Validation (Nigerian format)', () => { + it('should validate correct Nigerian phone number', () => { + mockAuthService.validatePhoneNumber.mockReturnValue(true); + expect(mockAuthService.validatePhoneNumber('+234803000000')).toBe(true); + }); + + it('should reject invalid phone number format', () => { + mockAuthService.validatePhoneNumber.mockReturnValue(false); + expect(mockAuthService.validatePhoneNumber('123456')).toBe(false); + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); +});