Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions apps/web/app/api/incidents/route.ts
Original file line number Diff line number Diff line change
@@ -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 }
);
}
}
61 changes: 61 additions & 0 deletions apps/web/lib/validations/incident-report.ts
Original file line number Diff line number Diff line change
@@ -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<typeof IncidentReportSchema>;

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 };
}
160 changes: 160 additions & 0 deletions packages/database/tests/auth.test.ts
Original file line number Diff line number Diff line change
@@ -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: '[email protected]',
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: '[email protected]',
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: '[email protected]',
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: '[email protected]', password: 'WrongPassword'})).rejects.toThrow(
'Invalid credentials'
);
});
});

describe('JWT Token Generation and Validation', () => {
it('should generate valid JWT token', () => {
const tokenPayload = { userId: '1', email: '[email protected]' };
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: '[email protected]',
});

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();
});
});