Skip to content
Merged
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
334 changes: 159 additions & 175 deletions backend/package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export class AuthService {
};
}

// Shared badge catalog must match donations.service.ts checkAndAwardBadges
// Shared badge catalog - must match donations.service.ts checkAndAwardBadges
private static readonly BADGE_RULES = [
{
threshold: 10,
Expand Down
2 changes: 1 addition & 1 deletion backend/src/auth/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class User {
@Column('simple-array', { default: '' })
badges: string[];

// Timestamps only once each ✅
// Timestamps - only once each ✅
@CreateDateColumn()
createdAt: Date;

Expand Down
10 changes: 5 additions & 5 deletions backend/src/auth/tests/jwt-security.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as jwt from 'jsonwebtoken';

// Uses the same signing approach as your JwtStrategy only the secret matters.
// Uses the same signing approach as your JwtStrategy - only the secret matters.
// These tests run standalone: npx jest jwt-security --no-coverage
const SECRET = 'test-jwt-secret-for-security-tests';

Expand Down Expand Up @@ -74,22 +74,22 @@ describe('JWT Security', () => {
expiresIn: '1h',
});
const decoded = jwt.verify(donorToken, SECRET) as any;
// Role must stay DONOR attacker cannot change it without re-signing
// Role must stay DONOR - attacker cannot change it without re-signing
expect(decoded.role).toBe('DONOR');
expect(decoded.role).not.toBe('ADMIN');
});

// ── 7. Token reuse (stateless note) ──────────────────────────────────────
// JWTs are stateless a valid token stays valid until expiry.
// JWTs are stateless - a valid token stays valid until expiry.
// To truly block reuse after logout, a Redis blacklist is required.
// This test documents the known gap:
it('documents that stateless JWT cannot be invalidated before expiry without a blacklist', () => {
const token = jwt.sign({ sub: 'user-abc', role: 'DONOR' }, SECRET, {
expiresIn: '1h',
});
// Simulate logout but the token is still cryptographically valid
// Simulate logout - but the token is still cryptographically valid
const decoded = jwt.verify(token, SECRET) as any;
// Without a blacklist check, this still passes expected behaviour to document
// Without a blacklist check, this still passes - expected behaviour to document
expect(decoded.sub).toBe('user-abc');
// RECOMMENDATION: Add Redis token blacklist in JwtAuthGuard for full logout security
});
Expand Down
2 changes: 1 addition & 1 deletion backend/src/common/cloudinary.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class CloudinaryService {
async uploadImage(file: Express.Multer.File): Promise<string> {
if (this.isMockMode) {
this.logger.warn(
`Mock upload for "${file.originalname}" Cloudinary not configured`,
`Mock upload for "${file.originalname}" - Cloudinary not configured`,
);
return this.getMockUrl(file.originalname);
}
Expand Down
4 changes: 2 additions & 2 deletions backend/src/common/email.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class EmailService {
if (!this.isConfigured || !this.transporter) {
// Explicit log so devs know emails are not being sent
this.logger.warn(
`[EMAIL NOT SENT no SMTP config]\n To: ${to}\n Subject: ${subject}`,
`[EMAIL NOT SENT - no SMTP config]\n To: ${to}\n Subject: ${subject}`,
);
return;
}
Expand All @@ -57,7 +57,7 @@ export class EmailService {
this.logger.log(`✅ Email sent to ${to}: "${subject}"`);
} catch (error) {
this.logger.error(`❌ Failed to send email to ${to}: ${error.message}`);
// Don't throw email failure should never crash the main flow
// Don't throw - email failure should never crash the main flow
}
}

Expand Down
6 changes: 3 additions & 3 deletions backend/src/notifications/notifications.controller.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('NotificationsController', () => {
];
mockNotificationsService.findByUser.mockResolvedValue(notifications);

const req = { user: { sub: 'user-123' } };
const req = { user: { userId: 'user-123' } };
const result = await controller.getMyNotifications(req);

expect(mockNotificationsService.findByUser).toHaveBeenCalledWith(
Expand All @@ -50,7 +50,7 @@ describe('NotificationsController', () => {
it('should mark a single notification as read', async () => {
mockNotificationsService.markRead.mockResolvedValue(undefined);

const req = { user: { sub: 'user-123' } };
const req = { user: { userId: 'user-123' } };
const result = await controller.markRead('n1', req);

expect(mockNotificationsService.markRead).toHaveBeenCalledWith(
Expand All @@ -65,7 +65,7 @@ describe('NotificationsController', () => {
it('should mark all notifications as read', async () => {
mockNotificationsService.markAllRead.mockResolvedValue(undefined);

const req = { user: { sub: 'user-123' } };
const req = { user: { userId: 'user-123' } };
const result = await controller.markAllRead(req);

expect(mockNotificationsService.markAllRead).toHaveBeenCalledWith(
Expand Down
6 changes: 3 additions & 3 deletions backend/src/notifications/notifications.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,20 @@ export class NotificationsController {
@Get()
@ApiOperation({ summary: 'Get current user notifications' })
async getMyNotifications(@Request() req: any) {
return this.notificationsService.findByUser(req.user.sub);
return this.notificationsService.findByUser(req.user.userId);
}

@Patch(':id/read')
@ApiOperation({ summary: 'Mark a notification as read' })
async markRead(@Param('id') id: string, @Request() req: any) {
await this.notificationsService.markRead(id, req.user.sub);
await this.notificationsService.markRead(id, req.user.userId);
return { success: true };
}

@Patch('read-all')
@ApiOperation({ summary: 'Mark all notifications as read' })
async markAllRead(@Request() req: any) {
await this.notificationsService.markAllRead(req.user.sub);
await this.notificationsService.markAllRead(req.user.userId);
return { success: true };
}
}
2 changes: 1 addition & 1 deletion backend/src/scripts/backup_db.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ else
exit 1
fi

# 3. Rotate old backups keep only the last MAX_BACKUPS files
# 3. Rotate old backups - keep only the last MAX_BACKUPS files
BACKUP_COUNT=$(ls -1 "$BACKUP_DIR"/*.sql 2>/dev/null | wc -l)
if [ "$BACKUP_COUNT" -gt "$MAX_BACKUPS" ]; then
echo "🧹 Rotating old backups (keeping last $MAX_BACKUPS)..."
Expand Down
8 changes: 4 additions & 4 deletions backend/test/realtime-events.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ describeIfServer('Real-Time Events – Socket.IO Client Tests', () => {
expect(ngoEvent.data).toHaveProperty('name');
expect(volunteerEvent.data).toHaveProperty('name');

// Latency check event should arrive quickly
// Latency check - event should arrive quickly
// Docker networking adds overhead; allow up to 500 ms
const maxLatency = 500;
expect(ngoEvent.latencyMs).toBeLessThan(maxLatency);
Expand Down Expand Up @@ -257,7 +257,7 @@ describeIfServer('Real-Time Events – Socket.IO Client Tests', () => {
expect(volunteerEvent.data).toHaveProperty('donationId');
expect(volunteerEvent.data.status).toBe('CLAIMED');

// Latency check Docker networking adds overhead; allow up to 500 ms
// Latency check - Docker networking adds overhead; allow up to 500 ms
const maxLatency = 500;
expect(donorEvent.latencyMs).toBeLessThan(maxLatency);
expect(volunteerEvent.latencyMs).toBeLessThan(maxLatency);
Expand Down Expand Up @@ -311,7 +311,7 @@ describeIfServer('Real-Time Events – Socket.IO Client Tests', () => {
const maxLatency = 500;
expect(latencyMs).toBeLessThan(maxLatency);
} catch {
// Auto-assign may not fire if no volunteer is nearby that's acceptable
// Auto-assign may not fire if no volunteer is nearby - that's acceptable
console.warn('volunteer.assigned not received (no eligible volunteer nearby)');
}
}, 15_000);
Expand Down Expand Up @@ -426,7 +426,7 @@ describe('EventsGateway – NestJS Integration', () => {
setTimeout(() => reject(new Error('Connection timeout')), 5_000);
});
} catch {
// AppModule may not compile without database skip gracefully
// AppModule may not compile without database - skip gracefully
console.warn('Skipping NestJS integration tests (AppModule requires database)');
}
}, 30_000);
Expand Down
6 changes: 3 additions & 3 deletions frontend/e2e/donation-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ test.describe('Donation Lifecycle – Full E2E', () => {
await mapContainer.scrollIntoViewIfNeeded();
await page.waitForTimeout(2_000);

// Click the map offset from top-left to avoid zoom controls
// Click the map - offset from top-left to avoid zoom controls
const box = await mapContainer.boundingBox();
if (box) {
await page.mouse.click(box.x + box.width * 0.6, box.y + box.height * 0.5);
Expand Down Expand Up @@ -178,7 +178,7 @@ test.describe('Donation Lifecycle – Full E2E', () => {
if (hook.queue && hook.memoizedState === null && idx > 0) {
const dispatch = hook.queue.dispatch;
if (typeof dispatch === 'function') {
// Try setting it if this is the right hook, it'll set location
// Try setting it - if this is the right hook, it'll set location
dispatch({ lat: 28.6139, lng: 77.2090 });
return true;
}
Expand All @@ -204,7 +204,7 @@ test.describe('Donation Lifecycle – Full E2E', () => {
// Wait a moment for state to update
await page.waitForTimeout(500);

// Submit the form button should now be enabled
// Submit the form - button should now be enabled
const submitBtn = page.getByRole('button', { name: /add food/i });
await expect(submitBtn).toBeEnabled({ timeout: 15_000 });
await submitBtn.click();
Expand Down
2 changes: 1 addition & 1 deletion frontend/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
globalSetup: './e2e/global-setup.ts',
fullyParallel: false, // Run tests sequentially donation lifecycle depends on order
fullyParallel: false, // Run tests sequentially - donation lifecycle depends on order
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: 1,
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/App.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/* App.css intentionally removed using Tailwind for styles */
/* App.css intentionally removed - using Tailwind for styles */

Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ describe('AdminDashboard - Epic 7 User Story 1 & 2', () => {

await waitFor(() => {
expect(adminAPI.verifyNGO).toHaveBeenCalledWith('ngo-123');
expect(toast.success).toHaveBeenCalledWith('NGO verified email sent');
expect(toast.success).toHaveBeenCalledWith('NGO verified - email sent');
});
});
});
Expand Down
40 changes: 20 additions & 20 deletions frontend/src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { initReactI18next } from 'react-i18next';
import hiStrings from './locales/hi';
import taStrings from './locales/ta';

//─ English source strings (the ONLY static dictionary)─
// English source strings (the ONLY static dictionary)
// Every other language is translated dynamically via API and cached in localStorage.

export const enStrings: Record<string, string> = {
Expand Down Expand Up @@ -81,11 +81,11 @@ export const enStrings: Record<string, string> = {
stepDeliverDesc: 'Food reaches the community through NGOs',
threeRolesOneMission: 'Three roles, one mission',
everyoneHasAPart: 'Everyone has a part to play',
donorSubtitle: 'Restaurants · Caterers · Individuals',
donorSubtitle: 'Restaurants · Caterers · Individuals',
donorDesc: 'List surplus food in seconds. Upload photos, set quantities, and our safety engine auto-calculates expiry windows.',
ngoSubtitle: 'Food banks · Shelters · Charities',
ngoSubtitle: 'Food banks · Shelters · Charities',
ngoDesc: 'Discover nearby donations on a live map. Claim food, track pickups, and manage your daily intake capacity.',
volunteerSubtitle: 'Drivers · Students · Community',
volunteerSubtitle: 'Drivers · Students · Community',
volunteerDesc: 'Pick up claimed food from donors and deliver it to NGOs. Track your deliveries and build your impact score.',
avgListingTime: 'Avg listing time',
safetyValidated: 'Safety validated',
Expand All @@ -110,7 +110,7 @@ export const enStrings: Record<string, string> = {
liveStatusTracking: 'Live status tracking',
liveStatusDesc: 'Follow every donation from listing to delivery with real-time status updates.',
impactDashboard: 'Impact dashboard',
impactDashboardDesc: 'Every meal saved is tracked. See your contribution to reducing COâ‚‚ emissions, total meals redistributed, and community impact over time.',
impactDashboardDesc: 'Every meal saved is tracked. See your contribution to reducing CO₂ emissions, total meals redistributed, and community impact over time.',
emissionsTracked: 'Emissions tracked',
perDonation: 'Per donation',
foodSaved: 'Food saved',
Expand Down Expand Up @@ -231,10 +231,10 @@ export const enStrings: Record<string, string> = {
descriptionPlaceholder: 'Any additional details (ingredients, dietary info, special instructions)...',
foodImages: 'Food Images (Optional)',
pickupLocationLabel: 'Pickup Location',
locationSelected: '✓ Selected',
locationSelected: ' Selected',
clickMapToSet: 'Click on the map to set pickup location',
hygieneChecklist: 'Hygiene Checklist',
hygieneComplete: '✓ Complete',
hygieneComplete: ' Complete',
keptCoveredAlways: 'Food was kept covered at all times',
cleanFoodSafe: 'Container is clean and food-safe',
hygieneNote: 'Both hygiene requirements must be met to ensure food safety',
Expand Down Expand Up @@ -298,24 +298,24 @@ export const enStrings: Record<string, string> = {
nextBadge: 'Next: {{name}}',
pointsToGo: '{{points}} pts to go',
allBadgesUnlocked: 'All badges unlocked! You\'re a Superhero!',
communityImpact: 'Community Impact — Platform Wide',
communityImpact: 'Community Impact - Platform Wide',
communityImpactDesc: 'Live totals across all donors, NGOs and volunteers on SurplusSync',
co2Saved: 'COâ‚‚ Saved',
co2Saved: 'CO₂ Saved',
totalDonationsProcessed: '{{count}} total donations processed',
successfullyDelivered: '{{count}} successfully delivered',
currentlyActive: '{{count}} currently active',
ngoGrowthReports: 'NGO Growth Reports',
lastSixMonths: 'Last 6 months',
monthlyIntakeSummaries: 'Monthly food intake summaries — use these for grant and funding applications',
monthlyIntakeSummaries: 'Monthly food intake summaries - use these for grant and funding applications',
totalReceivedMonth: 'Total donations received each month',
deliveryTrend: 'Delivery Trend',
deliveriesPerMonth: 'Successful food deliveries per month',
claimsEachMonth: 'Donations claimed each month',
highDeliveryTip: 'A high delivery rate demonstrates operational efficiency — highlight this in funding applications',
highDeliveryTip: 'A high delivery rate demonstrates operational efficiency - highlight this in funding applications',

// Notifications
stayUpdated: 'Stay updated on your donations and deliveries',
unread: 'Unread {{count}}',
unread: 'Unread',
markAllRead: 'Mark all as read',
loadingNotifications: 'Loading notifications...',
noUnreadNotifications: 'No unread notifications',
Expand All @@ -331,7 +331,7 @@ export const enStrings: Record<string, string> = {
backToLogin: 'Back to Login',
manageAccount: 'Manage your account and view your impact',
download: 'Download',
levelContributor: 'Level {{level}} • {{role}}',
levelContributor: 'Level {{level}} {{role}}',
progressToLevel: 'Progress to Level {{level}}',
pointsToGoProfile: '{{points}} points to go',
trophyCase: 'Trophy Case',
Expand All @@ -343,7 +343,7 @@ export const enStrings: Record<string, string> = {
notProvided: 'Not provided',
badgeGuide: 'Badge Guide',
earnKarma: 'Earn {{points}} karma points',
earned: '✓ Earned',
earned: ' Earned',
howToEarnKarma: 'How to Earn Karma',
createDonation: 'Create a Donation',
createDonationDesc: 'Donor lists new food for redistribution',
Expand Down Expand Up @@ -387,7 +387,7 @@ export const enStrings: Record<string, string> = {
activeTasks: 'Active Tasks',
completedTrips: 'Completed Trips',
noActiveTasks: 'No active tasks',
waitingForAssignment: "You're available waiting for assignment",
waitingForAssignment: "You're available - waiting for assignment",
toggleAvailabilityHint: 'Toggle your availability to receive assignments',
enRoute: 'En Route',
awaitingPickup: 'Awaiting Pickup',
Expand Down Expand Up @@ -473,11 +473,11 @@ export const enStrings: Record<string, string> = {
karmaPointsLevel: '⭐ {{karma}} Karma Points · Level {{level}} Contributor',

// NGOGrowthCharts (alternate key names used by component)
ngoGrowthSubtitle: 'Monthly food intake summaries use these for grant and funding applications',
ngoGrowthSubtitle: 'Monthly food intake summaries - use these for grant and funding applications',
totalDonationsReceivedMonth: 'Total donations received each month',
successfulDeliveriesPerMonth: 'Successful food deliveries per month',
donationsClaimedMonth: 'Donations claimed each month',
highDeliveryRateTip: '💡 A high delivery rate demonstrates operational efficiency highlight this in funding applications',
highDeliveryRateTip: '💡 A high delivery rate demonstrates operational efficiency - highlight this in funding applications',

// Profile karma actions
deliverDonation: 'Deliver Donation',
Expand Down Expand Up @@ -539,7 +539,7 @@ export const enStrings: Record<string, string> = {
// ── Accessibility extras ──
translating: 'Translating...',
translatingProgress: 'Translating... {{done}}/{{total}}',
firstTimeTranslation: 'First-time translation cached for instant loading next time',
firstTimeTranslation: 'First-time translation - cached for instant loading next time',
toggleHighContrastDesc: 'Toggle high-contrast dark mode for improved readability.',
translationCache: 'Translation Cache',
translationCacheDesc: 'Translations are cached locally for instant loading. Clear the cache to re-translate.',
Expand Down Expand Up @@ -599,7 +599,7 @@ export const enStrings: Record<string, string> = {

// ── Near-Expiry Alerts ──
nearExpiryAlerts: 'Near-Expiry Alerts',
nearExpiryDesc: 'Donations expiring within 2 hours act fast to prevent waste.',
nearExpiryDesc: 'Donations expiring within 2 hours - act fast to prevent waste.',
refresh: 'Refresh',
urgencyFilter: 'Urgency Filter',
critical: 'Critical',
Expand Down Expand Up @@ -654,7 +654,7 @@ export const enStrings: Record<string, string> = {
loadingTickets: 'Loading tickets…',
noTicketsYet: 'No tickets yet. Create one to get started.',
advance: 'Advance',
ticketSubmitted: 'Ticket submitted you will receive an email confirmation',
ticketSubmitted: 'Ticket submitted - you will receive an email confirmation',
failedLoadTickets: 'Failed to load tickets',
failedSubmitTicket: 'Failed to submit ticket',
failedUpdateTicket: 'Failed to update ticket',
Expand Down
Loading
Loading