The Problem
A 50-person law firm shares case files with clients, opposing counsel, and courts via Dropbox. But their malpractice insurer flagged it as a compliance risk: Dropbox stores data on shared infrastructure, there's no audit trail of who accessed what, and a paralegal accidentally shared an entire case folder with the wrong client last quarter — a potential ethics violation. The firm needs a self-hosted platform where every file access is logged, links expire automatically, and documents can be watermarked per-viewer.
Step 1: File Storage with Encryption at Rest
typescript
// src/storage/encrypted-store.ts
import { createCipheriv, createDecipheriv, randomBytes, createHash } from 'crypto';
import { createReadStream, createWriteStream } from 'fs';
import { pipeline } from 'stream/promises';
import { join } from 'path';
const STORAGE_PATH = process.env.FILE_STORAGE_PATH ?? '/data/files';
const ENCRYPTION_KEY = Buffer.from(process.env.FILE_ENCRYPTION_KEY!, 'hex'); // 32 bytes
export async function storeFile(
fileStream: NodeJS.ReadableStream,
fileId: string,
metadata: { filename: string; mimeType: string; uploadedBy: string }
): Promise<{ encryptedPath: string; iv: string; checksum: string; sizeBytes: number }> {
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-cbc', ENCRYPTION_KEY, iv);
const hash = createHash('sha256');
const encryptedPath = join(STORAGE_PATH, `${fileId}.enc`);
const writeStream = createWriteStream(encryptedPath);
let sizeBytes = 0;
const transform = new (await import('stream')).Transform({
transform(chunk, _, cb) {
sizeBytes += chunk.length;
hash.update(chunk);
cb(null, chunk);
},
});
await pipeline(fileStream, transform, cipher, writeStream);
return {
encryptedPath,
iv: iv.toString('hex'),
checksum: hash.digest('hex'),
sizeBytes,
};
}
export async function retrieveFile(
fileId: string,
iv: string
): Promise<NodeJS.ReadableStream> {
const encryptedPath = join(STORAGE_PATH, `${fileId}.enc`);
const decipher = createDecipheriv('aes-256-cbc', ENCRYPTION_KEY, Buffer.from(iv, 'hex'));
const readStream = createReadStream(encryptedPath);
return readStream.pipe(decipher);
}
Step 2: Granular Permissions and Expiring Links
typescript
// src/sharing/permissions.ts
import { z } from 'zod';
import { PrismaClient } from '@prisma/client';
import { randomBytes } from 'crypto';
const prisma = new PrismaClient();
export const SharePermission = z.object({
fileId: z.string().uuid(),
sharedWith: z.string().email(),
permission: z.enum(['view', 'download', 'edit']),
expiresAt: z.string().datetime().optional(),
maxDownloads: z.number().int().positive().optional(),
requirePassword: z.boolean().default(false),
password: z.string().optional(),
watermark: z.boolean().default(true),
ipWhitelist: z.array(z.string().ip()).optional(),
});
export async function createShareLink(input: z.infer<typeof SharePermission>): Promise<{
shareId: string;
url: string;
}> {
const token = randomBytes(32).toString('base64url');
const passwordHash = input.password
? (await import('crypto')).createHash('sha256').update(input.password).digest('hex')
: null;
const share = await prisma.fileShare.create({
data: {
fileId: input.fileId,
sharedWith: input.sharedWith,
permission: input.permission,
token,
expiresAt: input.expiresAt ? new Date(input.expiresAt) : null,
maxDownloads: input.maxDownloads,
downloadCount: 0,
passwordHash,
watermark: input.watermark,
ipWhitelist: input.ipWhitelist ?? [],
active: true,
},
});
return {
shareId: share.id,
url: `${process.env.BASE_URL}/share/${token}`,
};
}
export async function validateAccess(
token: string,
clientIp: string,
password?: string
): Promise<{ valid: boolean; share?: any; reason?: string }> {
const share = await prisma.fileShare.findUnique({ where: { token } });
if (!share || !share.active) return { valid: false, reason: 'Link not found or deactivated' };
if (share.expiresAt && share.expiresAt < new Date()) return { valid: false, reason: 'Link expired' };
if (share.maxDownloads && share.downloadCount >= share.maxDownloads)
return { valid: false, reason: 'Download limit reached' };
if (share.ipWhitelist.length > 0 && !share.ipWhitelist.includes(clientIp))
return { valid: false, reason: 'IP not allowed' };
if (share.passwordHash) {
const hash = (await import('crypto')).createHash('sha256').update(password ?? '').digest('hex');
if (hash !== share.passwordHash) return { valid: false, reason: 'Incorrect password' };
}
return { valid: true, share };
}
Step 3: Comprehensive Audit Trail
typescript
// src/audit/logger.ts
import { Pool } from 'pg';
const db = new Pool({ connectionString: process.env.DATABASE_URL });
export type AuditAction = 'upload' | 'download' | 'view' | 'share' | 'revoke'
| 'delete' | 'rename' | 'move' | 'permission_change' | 'failed_access';
export async function logAuditEvent(event: {
action: AuditAction;
fileId: string;
userId: string;
clientIp: string;
userAgent: string;
metadata?: Record<string, unknown>;
}): Promise<void> {
await db.query(`
INSERT INTO audit_log (action, file_id, user_id, client_ip, user_agent, metadata, occurred_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW())
`, [event.action, event.fileId, event.userId, event.clientIp, event.userAgent,
JSON.stringify(event.metadata ?? {})]);
}
export async function getFileAuditTrail(fileId: string): Promise<any[]> {
const { rows } = await db.query(`
SELECT action, user_id, client_ip, metadata, occurred_at
FROM audit_log WHERE file_id = $1
ORDER BY occurred_at DESC LIMIT 500
`, [fileId]);
return rows;
}
export async function generateComplianceReport(dateRange: { from: Date; to: Date }): Promise<{
totalEvents: number;
byAction: Record<string, number>;
failedAccess: number;
uniqueUsers: number;
externalAccess: number;
}> {
const { rows } = await db.query(`
SELECT action, COUNT(*) as cnt, COUNT(DISTINCT user_id) as users
FROM audit_log WHERE occurred_at BETWEEN $1 AND $2
GROUP BY action
`, [dateRange.from, dateRange.to]);
const byAction: Record<string, number> = {};
let total = 0, users = 0, failed = 0;
for (const r of rows) {
byAction[r.action] = parseInt(r.cnt);
total += parseInt(r.cnt);
users = Math.max(users, parseInt(r.users));
if (r.action === 'failed_access') failed = parseInt(r.cnt);
}
return { totalEvents: total, byAction, failedAccess: failed, uniqueUsers: users, externalAccess: 0 };
}
Results
- Compliance audit passed — malpractice insurer approved the platform
- Accidental sharing incidents: zero (was 2-3/quarter with Dropbox)
- Audit trail: every file access logged with IP, timestamp, user agent
- Expiring links: 100% of external shares auto-expire (default 7 days)
- Access attempts blocked: 23 expired link attempts, 8 wrong-password attempts in first month
- Watermarked downloads: every PDF downloaded by external parties has viewer-specific watermark
- Self-hosted: all data on firm's own servers, zero third-party cloud exposure