Files
evidencija-rezija/web-app/app/lib/shareChecksum.ts
Knee Cola 57dcebd640 refactor: convert repository to monorepo with npm workspaces
Restructured the repository into a monorepo to better organize application code
and maintenance scripts.

## Workspace Structure
- web-app: Next.js application (all app code moved from root)
- housekeeping: Database backup and maintenance scripts

## Key Changes
- Moved all application code to web-app/ using git mv
- Moved database scripts to housekeeping/ workspace
- Updated Dockerfile for monorepo build process
- Updated docker-compose files (volume paths: ./web-app/etc/hosts/)
- Updated .gitignore for workspace-level node_modules
- Updated documentation (README.md, CLAUDE.md, CHANGELOG.md)

## Migration Impact
- Root package.json now manages workspaces
- Build commands delegate to web-app workspace
- All file history preserved via git mv
- Docker build process updated for workspace structure

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-25 12:13:04 +01:00

87 lines
2.4 KiB
TypeScript

import crypto from 'crypto';
/**
* Checksum length in hex characters (16 chars = 64 bits of entropy)
*/
export const CHECKSUM_LENGTH = 16;
/**
* Generate share link checksum for location
* Uses HMAC-SHA256 for cryptographic integrity
*
* SECURITY: Prevents location ID enumeration while allowing stateless validation
*/
export function generateShareChecksum(locationId: string): string {
const secret = process.env.SHARE_LINK_SECRET;
if (!secret) {
throw new Error('SHARE_LINK_SECRET environment variable not configured');
}
return crypto
.createHmac('sha256', secret)
.update(locationId)
.digest('hex')
.substring(0, CHECKSUM_LENGTH);
}
/**
* Validate share link checksum
* Uses constant-time comparison to prevent timing attacks
*
* @param locationId - The location ID from URL
* @param providedChecksum - The checksum from URL
* @returns true if checksum is valid
*/
export function validateShareChecksum(
locationId: string,
providedChecksum: string
): boolean {
try {
const expectedChecksum = generateShareChecksum(locationId);
// Convert to buffers for timing-safe comparison
const expected = Buffer.from(expectedChecksum);
const provided = Buffer.from(providedChecksum);
// Length check (prevents timing attack on different lengths)
if (expected.length !== provided.length) {
return false;
}
// Constant-time comparison (prevents timing attacks)
return crypto.timingSafeEqual(expected, provided);
} catch {
return false;
}
}
/**
* Generate combined location ID with checksum appended
* @param locationId - The MongoDB location ID (24 chars)
* @returns Combined ID: locationId + checksum (40 chars total)
*/
export function generateShareId(locationId: string): string {
const checksum = generateShareChecksum(locationId);
return locationId + checksum;
}
/**
* Extract location ID and checksum from combined share ID
* @param shareId - Combined ID (locationId + checksum)
* @returns Object with locationId and checksum, or null if invalid format
*/
export function extractShareId(shareId: string): { locationId: string; checksum: string } | null {
// MongoDB ObjectID is 24 chars, checksum is 16 chars = 40 total
const expectedLength = 24 + CHECKSUM_LENGTH;
if (shareId.length !== expectedLength) {
return null;
}
const locationId = shareId.substring(0, 24);
const checksum = shareId.substring(24);
return { locationId, checksum };
}