Compare commits

...
10 Commits
Author SHA1 Message Date
arsam 89020fb528 Import documentation: README.md
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:15:49 +03:30
arsam 30a095ce41 Import documentation: PRODUCTION.md
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:15:47 +03:30
arsam 6f32ea43d9 Import documentation: docs/superpowers/specs/2026-05-30-dashboard-polish-design.md
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:15:45 +03:30
arsam 1a494896a5 Import documentation: cshare-app/README.md
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:15:42 +03:30
arsam a2432de7db Import documentation: CONTINUE.md
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:15:39 +03:30
arsam 64ae9731e7 Import documentation: CLAUDE.md
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:15:37 +03:30
arsam 0d17401286 Import documentation: backend/README.md
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:15:34 +03:30
arsam 84e561f2c7 Import documentation: AGENTS.md
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:15:32 +03:30
arsam dc6c010b0b Import cshare-app/src/assets visual and font assets
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:15:30 +03:30
arsam 0713e9004e Import cshare-app/public/fonts visual and font assets
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:15:27 +03:30
19 changed files with 1430 additions and 0 deletions
+61
View File
@@ -0,0 +1,61 @@
# Repository Guidelines
## Commands
Run from `cshare-app/`:
```sh
npm run dev # Vite dev server
npm run build # Build + precompress (creates .gz/.br)
npm run check # lint + build (run before committing)
npm run lint # ESLint
```
Run from `backend/`:
```sh
npm run dev # tsx watch src/server.ts
npm test # tsx --test test/*.test.ts
npm run build # tsc
npm run check # test + build (run before committing)
```
## Architecture
**Two packages:** `cshare-app/` (React 19 + Vite 8, JavaScript/JSX, no TypeScript) and `backend/` (Express 5 + TypeScript strict mode).
**Backend storage:** Currently uses local JSON (`backend/data/db.json`) and disk storage. PostgreSQL/Redis/R2 adapters are planned in `CONTINUE.md` but not yet implemented.
**Frontend routing:** React Router v7 with lazy-loaded routes. All routes defined in `src/App.jsx`.
**Styling:** Tailwind CSS 4 via `@tailwindcss/vite` plugin (not PostCSS). Custom design system in `src/index.css` with CSS variables and component classes (`.btn`, `.card`, `.input`).
**Toast system:** `ToastProvider` wraps app in `App.jsx`. Use `useToast()` hook → `addToast(message, type)` where type is `'success' | 'info' | 'error'`.
**API client:** `src/lib/api.js` contains `uploadFile` (XHR with progress), `listFiles`, `deleteFile`, `getDashboardStats`, `resolveLink`, `downloadUrl`, `previewUrl`.
## Environment
**Frontend:** Set `VITE_API_BASE_URL=http://localhost:4001` only when API runs on different port. Omit in production if using Nginx reverse proxy (frontend calls `window.location.origin`).
**Backend:** Requires `.env` with `JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`, `ADMIN_API_KEY` (64+ char random strings). See `backend/.env.example`.
**Build gotcha:** `dist/` may be owned by root. If `npm run build` fails with `EACCES`, run `sudo chown -R $(whoami) cshare-app/dist/`.
## Key Files
- `CONTINUE.md` — Full production roadmap (Postgres, Redis, R2, auth, Stripe implementation plan)
- `CLAUDE.md` — Detailed architecture reference
- `PRODUCTION.md` — Deployment and Nginx configuration
- `cshare-app/src/App.jsx` — All route definitions
- `cshare-app/src/lib/api.js` — API client
- `backend/src/server.ts` — Express app bootstrap
- `backend/ecosystem.config.cjs` — PM2 configuration (reads `.env` file)
## Style
**Frontend:** PascalCase for components (`UploadPage.jsx`), camelCase for hooks (`useLenis.js`) and data modules (`mockData.js`). Two-space indentation, single quotes, semicolons.
**Backend:** TypeScript strict mode, named exports. Route modules named by resource (`files.ts`, `links.ts`, `auth.ts`).
ESLint configured in `cshare-app/eslint.config.js` (flat config, React hooks + refresh plugins).
+68
View File
@@ -0,0 +1,68 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
CShare is a file-sharing web application with a React frontend and an Express/TypeScript backend. The frontend is feature-rich with 12+ pages; the backend is live and working with local JSON storage. The backend (Postgres, R2 storage, auth, Stripe) is the next major work item (see `CONTINUE.md`).
## Commands
All commands run from `/srv/CShare/cshare-app/`:
```bash
npm run dev # Start Vite dev server with HMR
npm run build # Production build to dist/
npm run lint # ESLint (flat config, React hooks + refresh plugins)
npm run preview # Serve production build locally
```
No test framework is configured yet.
Backend commands run from `/srv/CShare/backend/`:
```bash
npm run dev # Start API with tsx watch
npm test # Run test suite (tsx --test)
npm run build # Compile TypeScript to dist/
```
## Architecture
**Stack:** React 19 + Vite 8, Tailwind CSS 4 (via `@tailwindcss/vite` plugin), no TypeScript. Backend: Express 5 + TypeScript + tsx.
**Routing:** React Router v7 with lazy-loaded routes and real URLs. All routes defined in `App.jsx`. Page keys: `/`, `/upload`, `/dashboard`, `/preview/:linkId`, `/pricing`, `/docs`, `/about`, `/blog`, `/careers`, `/contact`, `/privacy`, `/terms`, `/security`, `/gdpr`. A catch-all `*` route renders `NotFoundPage`.
**Directory layout (inside `cshare-app/src/`):**
- `pages/` — One component per view. Pages receive route props; `LandingPage` and `UploadPage` also accept external data props.
- `components/` — `Navbar` (sticky, mobile hamburger), `Footer` (hidden on landing/fileview), `Icons` (17 hand-drawn SVG icons), `Toast` (`ToastProvider` context + `useToast` hook), `PageTransition` (route-level fade+slide wrapper), `SkeletonLoader` (shimmer skeleton components), `StorageBar` (segmented storage usage bar), `ActivityFeed` (recent activity feed), `FileDetailSlideOver` (slide-over panel for file details/analytics).
- `hooks/` — `useLenis` (smooth scroll), `useScrollReveal` (intersection observer for below-fold animations).
- `data/mockData.js` — 6 mock files + 1 `sharedFile` object, mock activity data, storage breakdown, and helper functions (`formatFileSize`, `formatDate`, `formatDateRelative`, `formatActivityTime`, `getFileIcon`, `getFileIconColor`).
- `lib/api.js` — API client with `uploadFile` (XHR with progress), `listFiles`, `deleteFile`, `getDashboardStats`, `resolveLink`, `downloadUrl`, `previewUrl`.
- `assets/` — Static images (hero.png, framework SVGs).
**Backend layout (inside `backend/src/`):**
- `routes/` — `files.ts`, `auth.ts`, `links.ts`, `dashboard.ts`, `admin.ts`
- `middleware/` — `session.ts`, `errorHandler.ts`, `rateLimit.ts`, `adminAuth.ts`
- `services/` — `store.ts` (JSON file storage)
- `utils/` — `crypto.ts`, `files.ts`, `gdrive.ts`, `preview.ts`
- `server.ts` — Express app bootstrap
- `worker.ts` — Background worker entry
**Toast system:** `ToastProvider` wraps the app in `App.jsx`. Components call `useToast()` to get `addToast(message, type)` — types are `'success'`, `'info'`, `'error'`. Auto-dismisses after 3s.
**Styling:** `index.css` defines the full design system via CSS custom properties and component classes (`.btn`, `.btn-primary`, `.btn-secondary`, `.btn-ghost`, `.btn-danger`, `.card`, `.input`, `.heading-*`, `.text-*`). Tailwind utilities are used alongside these custom classes. Dark charcoal background (`#0D0D0F`) with electric blue accent (`#4A90FF`) — zero purple anywhere. Keyframe animations available: `fadeIn`, `fadeInUp`, `slideDown`, `scaleIn`, `shimmer`, `progressPulse`, `gradientMove`, `float`, `spin`, `slideInRight`, `staggerIn`, `pageFadeIn`, `copyPop`, `iconBounce`, `orbFloat1-3`, `dashMarch`, `shimmerBar`, `pulseGlow`, `countPop`, `revealLine`, `borderGlow`, `gentlePulse`.
**Fonts:** Three families loaded via `@font-face` from `/public/fonts/` — Syne (headings), Outfit (body), Playfair (decorative).
**Data flow:** Frontend calls real API endpoints via `lib/api.js`. Dashboard loads files and stats from the backend; falls back to mock data when API is unavailable. Upload uses real XHR with progress tracking. The backend stores data in `data/db.json`.
## Key Files
- `CONTINUE.md` — Full production roadmap: what's built and what's next (Postgres, Redis, R2, auth, Stripe, workers, hardening).
- `AGENTS.md` — Repository guidelines: coding style, build commands, commit conventions.
- `docs/superpowers/specs/2026-05-30-dashboard-polish-design.md` — Design spec for dashboard enhancement and app polish (frontend + backend analytics API).
## Build Note
The `dist/` directory may be owned by root (from a previous build). If `npm run build` fails with `EACCES`, fix ownership first: `sudo chown -R $(whoami) cshare-app/dist/`.
+972
View File
@@ -0,0 +1,972 @@
# CShare — Full Production Roadmap
This document covers everything built so far and everything that needs to be implemented to take CShare from a frontend prototype to a production-grade file-sharing service. Read it top to bottom before starting any backend work.
---
## What Is Already Built (Frontend)
### Pages
- `/` — Landing page (Playfair italic hero, split layout, live upload preview, features grid, how-it-works)
- `/upload` — Drag-and-drop upload zone with simulated progress, share link generation, "Share All"
- `/dashboard` — File table + mobile card layout, search, stat cards, delete with confirm, stats modal
- `/preview` — File view/download page (recipient side), countdown expiry timer
- `/pricing` — 3-tier pricing page (Free / Pro / Team)
- `/docs` — Documentation with sticky sidebar and API reference
- `/about`, `/blog`, `/careers`, `/contact` — Company pages
- `/privacy`, `/terms`, `/security`, `/gdpr` — Legal pages
### Components & System
- `Navbar` — sticky, mobile hamburger, React Router wired
- `Footer` — all links routed
- `Toast` — context-based success/info/error notifications
- `Icons` — 17 SVG icons
- Design system in `index.css` — CSS variables, button/card/input classes, keyframes
- Lenis smooth scrolling
- React Router v7 with real URLs
### Stack
- React 19, Vite 8, Tailwind CSS 4
- Outfit (body) + Syne (headings) + Playfair Display (hero) — self-hosted TTFs
- Electric blue accent (`#4A90FF`), dark charcoal background (`#0D0D0F`)
---
## What Needs to Be Built
---
## 1. Backend — Node.js / Express API
### Stack Decision
```
Runtime: Node.js 20 LTS
Framework: Express 5 (or Fastify for higher throughput)
Language: TypeScript (strict mode)
Process mgr: PM2 (production) / tsx watch (development)
```
### Directory Structure
```
/backend
/src
/config # env, constants, feature flags
/db # Drizzle ORM schema + migrations
/middleware # auth, rate-limit, error handler, logger
/routes
files.ts # upload, download, delete, list
auth.ts # register, login, logout, refresh
links.ts # generate, resolve, validate
analytics.ts # view/download events, stats
admin.ts # internal admin endpoints
/services
storage.ts # S3/R2 abstraction
expiry.ts # cron-based file expiry worker
email.ts # Nodemailer / Resend wrapper
analytics.ts # event recording + aggregation
/workers
expiry.worker.ts # scheduled deletion
cleanup.worker.ts # orphaned object cleanup
/utils
crypto.ts # link ID generation, hash helpers
validate.ts # Zod schemas for all request bodies
mime.ts # content-type detection + sanitization
server.ts # Express app bootstrap
worker.ts # standalone worker entry (BullMQ)
/migrations # SQL migration files (Drizzle Kit)
package.json
tsconfig.json
.env.example
```
### Environment Variables
```bash
# Server
PORT=3001
NODE_ENV=production
# Database
DATABASE_URL=postgresql://user:pass@localhost:5432/cshare
# Redis
REDIS_URL=redis://localhost:6379
# Storage (Cloudflare R2 or AWS S3)
STORAGE_ENDPOINT=https://<account>.r2.cloudflarestorage.com
STORAGE_BUCKET=cshare-files
STORAGE_ACCESS_KEY=
STORAGE_SECRET_KEY=
STORAGE_PUBLIC_URL=https://files.asabbagh.com
# Auth
JWT_ACCESS_SECRET=<64-char random hex>
JWT_REFRESH_SECRET=<64-char random hex>
ACCESS_TOKEN_TTL=15m
REFRESH_TOKEN_TTL=30d
# Email
EMAIL_FROM=noreply@cshare.app
RESEND_API_KEY=
# Security
BCRYPT_ROUNDS=12
MAX_FILE_SIZE_FREE=104857600 # 100 MB
MAX_FILE_SIZE_PRO=524288000 # 500 MB
FREE_UPLOAD_MONTHLY_LIMIT=5
# Rate limiting
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX_UPLOAD=10
RATE_LIMIT_MAX_DOWNLOAD=60
# Frontend URL (CORS)
FRONTEND_URL=https://cshare.app
```
---
## 2. Database — PostgreSQL
### Why PostgreSQL
- Transactional integrity for file + link creation (atomically or not at all)
- Row-level expiry queries with proper index support
- JSONB for extensible metadata without schema churn
- Native UUID support
- Strong enum types for plan, status
### Schema (Drizzle ORM)
```typescript
// db/schema.ts
import { pgTable, uuid, text, integer, bigint, boolean, timestamp, pgEnum, jsonb, index } from 'drizzle-orm/pg-core';
// ── Enums ──────────────────────────────────────────────────────────
export const planEnum = pgEnum('plan', ['free', 'pro', 'team']);
export const fileStatusEnum = pgEnum('file_status', ['pending', 'uploaded', 'deleted', 'expired']);
// ── Users ──────────────────────────────────────────────────────────
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
passwordHash: text('password_hash').notNull(),
plan: planEnum('plan').notNull().default('free'),
verified: boolean('verified').notNull().default(false),
uploadCount: integer('upload_count').notNull().default(0), // rolling monthly counter
uploadResetAt: timestamp('upload_reset_at').notNull().defaultNow(),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow(),
}, (t) => ({
emailIdx: index('users_email_idx').on(t.email),
}));
// ── Refresh tokens ─────────────────────────────────────────────────
export const refreshTokens = pgTable('refresh_tokens', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
tokenHash: text('token_hash').notNull().unique(), // SHA-256 of the raw token
expiresAt: timestamp('expires_at').notNull(),
revokedAt: timestamp('revoked_at'),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
userIdx: index('refresh_tokens_user_idx').on(t.userId),
hashIdx: index('refresh_tokens_hash_idx').on(t.tokenHash),
}));
// ── Files ──────────────────────────────────────────────────────────
export const files = pgTable('files', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').references(() => users.id, { onDelete: 'set null' }), // null = anonymous
storageKey: text('storage_key').notNull().unique(), // e.g. "uploads/2026/05/abc123.pdf"
originalName: text('original_name').notNull(),
mimeType: text('mime_type').notNull(),
sizeBytes: bigint('size_bytes', { mode: 'number' }).notNull(),
status: fileStatusEnum('file_status').notNull().default('pending'),
passwordHash: text('password_hash'), // null = no password
expiresAt: timestamp('expires_at').notNull(),
deletedAt: timestamp('deleted_at'),
metadata: jsonb('metadata').default({}), // extensible bag
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
userIdx: index('files_user_idx').on(t.userId),
statusIdx: index('files_status_idx').on(t.status),
expiryIdx: index('files_expires_at_idx').on(t.expiresAt), // for expiry worker queries
}));
// ── Share links ────────────────────────────────────────────────────
export const links = pgTable('links', {
id: text('id').primaryKey(), // short random ID, e.g. "x7k2m9"
fileId: uuid('file_id').notNull().references(() => files.id, { onDelete: 'cascade' }),
active: boolean('active').notNull().default(true),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
fileIdx: index('links_file_idx').on(t.fileId),
}));
// ── Analytics events ───────────────────────────────────────────────
export const events = pgTable('events', {
id: uuid('id').primaryKey().defaultRandom(),
linkId: text('link_id').notNull().references(() => links.id, { onDelete: 'cascade' }),
type: text('type').notNull(), // 'view' | 'download'
ip: text('ip'), // hashed, not raw
userAgent: text('user_agent'),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
linkIdx: index('events_link_idx').on(t.linkId),
typeIdx: index('events_type_idx').on(t.type),
timeIdx: index('events_created_at_idx').on(t.createdAt),
}));
// ── Email verification tokens ──────────────────────────────────────
export const verificationTokens = pgTable('verification_tokens', {
token: text('token').primaryKey(), // random UUID stored as-is (not sensitive long-term)
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
expiresAt: timestamp('expires_at').notNull(),
usedAt: timestamp('used_at'),
});
// ── Password reset tokens ──────────────────────────────────────────
export const passwordResetTokens = pgTable('password_reset_tokens', {
tokenHash: text('token_hash').primaryKey(), // SHA-256
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
expiresAt: timestamp('expires_at').notNull(),
usedAt: timestamp('used_at'),
});
// ── API keys (Pro/Team) ────────────────────────────────────────────
export const apiKeys = pgTable('api_keys', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
keyHash: text('key_hash').notNull().unique(), // SHA-256 of raw key
prefix: text('prefix').notNull(), // first 8 chars for display, e.g. "csh_abc1"
lastUsedAt: timestamp('last_used_at'),
expiresAt: timestamp('expires_at'),
revokedAt: timestamp('revoked_at'),
createdAt: timestamp('created_at').notNull().defaultNow(),
}, (t) => ({
userIdx: index('api_keys_user_idx').on(t.userId),
hashIdx: index('api_keys_hash_idx').on(t.keyHash),
}));
```
### Migrations Strategy
- Use **Drizzle Kit** (`drizzle-kit generate`, `drizzle-kit migrate`)
- Migration files committed to git and run in CI before deploy
- Never edit past migrations — always add new ones
- Production: run migrations as a pre-start step, not in the application boot path
```bash
# Generate migration after schema change
npx drizzle-kit generate
# Run pending migrations
npx drizzle-kit migrate
# Inspect current DB state
npx drizzle-kit studio
```
### Key Indexes to Add Post-Launch (monitor query plans first)
- Partial index on `files` where `status = 'uploaded'` and `expires_at < now()` — used by the expiry worker every minute
- Partial index on `events` for last-30-days window queries — used by analytics
---
## 3. Redis
### Why Redis
- Rate limiting (sliding window counters, per-IP and per-user)
- Upload session state (presigned URL → file ID mapping while upload is in flight)
- Short-lived caches (file metadata for hot links, avoids DB hit on every download)
- BullMQ job queue (expiry worker, email queue, cleanup worker)
- Session invalidation list (jti blocklist for logged-out JWTs before they expire)
### Key Namespaces
```
# Rate limiting
rl:upload:{ip} # sliding window, 10 uploads/min per IP
rl:download:{ip} # sliding window, 60 downloads/min per IP
rl:auth:{ip} # 10 attempts/15 min, then lockout
# Upload sessions (TTL: 30 min)
upload_session:{sessionId} -> { fileId, userId, expectedSize, mimeType }
# File metadata cache (TTL: 5 min)
file_meta:{fileId} -> JSON of file row (invalidated on delete)
link_meta:{linkId} -> JSON of link + file join
# JWT blocklist (jti → expiry timestamp)
jwt_blocklist:{jti} -> "1" # TTL = token's remaining lifetime
# Monthly upload counter per free user (TTL: rolling 31 days)
upload_count:{userId}:{YYYY-MM} -> integer
# BullMQ queues (managed by BullMQ, don't write to these directly)
bull:expiry # file expiry jobs
bull:email # transactional email jobs
bull:cleanup # orphan storage cleanup jobs
```
### Rate Limiting Implementation
```typescript
// middleware/rateLimit.ts
import { RateLimiterRedis } from 'rate-limiter-flexible';
export const uploadLimiter = new RateLimiterRedis({
storeClient: redis,
keyPrefix: 'rl:upload',
points: 10, // 10 uploads
duration: 60, // per 60 seconds
blockDuration: 300, // block for 5 min if exceeded
});
export const downloadLimiter = new RateLimiterRedis({
storeClient: redis,
keyPrefix: 'rl:download',
points: 60,
duration: 60,
});
export const authLimiter = new RateLimiterRedis({
storeClient: redis,
keyPrefix: 'rl:auth',
points: 10,
duration: 900, // 15 minutes
blockDuration: 3600, // 1 hour lockout
});
```
---
## 4. File Storage — Cloudflare R2
### Why R2 over S3
- Zero egress fees (biggest cost driver for a file-sharing service)
- S3-compatible API — drop-in replacement
- Built-in CDN via Cloudflare network
- Cheaper storage per GB
### Upload Flow
```
Client Backend R2
| | |
|-- POST /api/upload -->| |
| (file stream) | |
| |-- PutObject ----->|
| | (stream body) |
| |<-- ETag ----------|
| | |
| |-- INSERT file row |
| |-- INSERT link row |
|<-- { linkId, url } --| |
```
**Alternative for large files — presigned URLs:**
```
Client Backend R2
| | |
|-- POST /api/upload/init -->| |
| |-- createPresign ->|
| |<-- presignedUrl --|
|<-- { uploadUrl, sessionId } --| |
| | |
|---- PUT presignedUrl ------------------>|
|<---- 200 --------------------------------|
| | |
|-- POST /api/upload/complete -->| |
| { sessionId } | |
| |-- HeadObject ---->|
| |<-- metadata ------|
| |-- INSERT file/link|
|<-- { linkId, url } --| |
```
### Storage Key Format
```
uploads/{year}/{month}/{uuidv4}/{sanitized-filename}
Example:
uploads/2026/05/f47ac10b-58cc-4372-a567-0e02b2c3d479/Q4-report.pdf
```
- UUID in path prevents enumeration
- Year/month prefix enables efficient lifecycle rules
- Sanitized filename strips path traversal characters
### File Deletion
```typescript
// services/storage.ts
async function deleteFile(storageKey: string): Promise<void> {
await s3.send(new DeleteObjectCommand({
Bucket: process.env.STORAGE_BUCKET,
Key: storageKey,
}));
}
```
Always delete from storage AND update DB status in the same operation. If storage delete fails, mark the file as `pending_deletion` and retry via the cleanup worker.
### Content-Type Handling
- Extract MIME type from actual file bytes (use `file-type` library), not from the browser's reported Content-Type
- Serve files with `Content-Disposition: attachment` to prevent browser execution of uploaded HTML/JS
- Never serve with `Content-Type: text/html` regardless of what was uploaded
---
## 5. Authentication & Authorization
### Strategy
- **JWT access tokens** (short-lived, 15 min) — stored in memory on the client, never localStorage
- **HTTP-only refresh tokens** (30 days) — cookie, not accessible to JavaScript
- **API keys** for programmatic access (Pro/Team) — hashed in DB, `csh_` prefix
### Auth Endpoints
```
POST /api/auth/register { email, password }
POST /api/auth/login { email, password } -> sets refresh cookie
POST /api/auth/refresh (reads refresh cookie) -> new access token
POST /api/auth/logout revokes refresh token, clears cookie
POST /api/auth/verify-email { token }
POST /api/auth/forgot-password { email }
POST /api/auth/reset-password { token, newPassword }
GET /api/auth/me returns current user
```
### Token Lifecycle
```typescript
// Access token payload
{
sub: userId, // UUID
email: user.email,
plan: 'pro',
jti: randomUUID(), // for blocklist
iat: now,
exp: now + 15min,
}
// Refresh token (stored in DB as SHA-256 hash)
// Raw token sent as HTTP-only cookie, never logged
```
### Password Hashing
```typescript
import bcrypt from 'bcrypt';
const ROUNDS = parseInt(process.env.BCRYPT_ROUNDS); // 12
const hash = await bcrypt.hash(password, ROUNDS);
const valid = await bcrypt.compare(password, hash);
```
### Plan-based Authorization Middleware
```typescript
// middleware/auth.ts
export const requirePlan = (minPlan: 'free' | 'pro' | 'team') =>
(req, res, next) => {
const PLAN_RANK = { free: 0, pro: 1, team: 2 };
if (PLAN_RANK[req.user.plan] < PLAN_RANK[minPlan]) {
return res.status(403).json({ error: 'Plan upgrade required' });
}
next();
};
// Usage
router.post('/upload', requireAuth, requirePlan('pro'), uploadHandler);
```
---
## 6. API Routes
### Files
```
POST /api/v1/files/upload Multipart upload (stream to R2)
POST /api/v1/files/upload/init Init presigned upload (returns URL + sessionId)
POST /api/v1/files/upload/complete Finalize presigned upload
GET /api/v1/files List user's files (paginated)
GET /api/v1/files/:id Get file metadata
DELETE /api/v1/files/:id Delete file + storage object + invalidate links
PATCH /api/v1/files/:id Update expiry, password, name
```
### Links
```
GET /api/v1/links/:linkId Resolve link -> file metadata (recipient endpoint)
GET /api/v1/links/:linkId/download Redirect to signed R2 URL (or stream)
POST /api/v1/links/:linkId/verify Verify password for password-protected links
```
### Analytics
```
POST /api/v1/events Record view/download event (called from link resolution)
GET /api/v1/files/:id/analytics View + download counts, timeline
GET /api/v1/dashboard/stats Aggregate stats for the current user
```
### Auth (see above)
### Response Format
```typescript
// Success
{ data: T, meta?: { page, limit, total } }
// Error
{ error: string, code: string, details?: Record<string, string> }
// HTTP status codes used consistently:
// 200 — OK
// 201 — Created
// 204 — No Content (delete success)
// 400 — Validation error
// 401 — Unauthenticated
// 403 — Forbidden (plan limit or wrong owner)
// 404 — Not found (or intentionally opaque for security)
// 409 — Conflict (e.g., email already registered)
// 413 — Payload too large
// 422 — Unprocessable entity
// 429 — Rate limited
// 500 — Internal server error (never expose stack traces)
```
### Request Validation (Zod)
```typescript
// routes/files.ts
const uploadSchema = z.object({
expiresIn: z.number().int().min(3600).max(7776000).default(604800), // 1h to 90d
password: z.string().min(4).max(128).optional(),
});
router.post('/upload', requireAuth, upload.single('file'), async (req, res) => {
const body = uploadSchema.safeParse(req.body);
if (!body.success) return res.status(400).json({ error: 'Validation failed', details: body.error.flatten() });
// ...
});
```
---
## 7. Background Workers
### Expiry Worker (runs every 60 seconds)
```typescript
// workers/expiry.worker.ts
import { CronJob } from 'cron';
async function runExpiry() {
const expired = await db
.select({ id: files.id, storageKey: files.storageKey })
.from(files)
.where(
and(
eq(files.status, 'uploaded'),
lt(files.expiresAt, new Date()),
)
)
.limit(100); // process in batches
for (const file of expired) {
try {
await deleteFromStorage(file.storageKey);
await db.update(files)
.set({ status: 'expired', deletedAt: new Date() })
.where(eq(files.id, file.id));
await db.update(links).set({ active: false }).where(eq(links.fileId, file.id));
await redis.del(`file_meta:${file.id}`);
} catch (err) {
logger.error({ fileId: file.id, err }, 'Expiry failed — will retry next tick');
}
}
}
new CronJob('* * * * *', runExpiry).start();
```
### Cleanup Worker (runs every 6 hours)
- List all objects in R2 bucket
- Cross-reference with DB — any storage key not in DB is an orphan
- Delete orphans (they're files that failed mid-upload and never got a DB row)
### Email Worker (BullMQ)
```typescript
// Queue jobs for:
// - Email verification on register
// - Password reset
// - File expiry warning (24h before, Pro plan)
// - Usage limit warning (at 80% of monthly Free quota)
const emailQueue = new Queue('email', { connection: redis });
// Producer
await emailQueue.add('verify-email', {
to: user.email,
userId: user.id,
token: verificationToken,
});
// Consumer
const worker = new Worker('email', async (job) => {
if (job.name === 'verify-email') await sendVerificationEmail(job.data);
if (job.name === 'reset-password') await sendResetEmail(job.data);
}, { connection: redis });
```
---
## 8. Infrastructure & Deployment
### Architecture Overview
```
Internet
│
▼
Cloudflare (DNS + DDoS + CDN for static assets)
│
├──> Nginx (reverse proxy, port 443 → 3000 frontend, 3001 backend)
│ SSL termination (Let's Encrypt via Certbot)
│
├──> Node.js API (PM2, port 3001)
│ └── connects to PostgreSQL (port 5432, local socket)
│ └── connects to Redis (port 6379, local socket)
│
├──> PostgreSQL 16 (local, not exposed to internet)
│
├──> Redis 7 (local, not exposed to internet)
│
└──> Cloudflare R2 (file storage, via HTTPS API)
```
### Nginx Config for API
```nginx
# /etc/nginx/sites-enabled/cshare-api
server {
listen 3001;
server_name 188.245.39.229;
# Max upload size (must match backend limit)
client_max_body_size 520M;
client_body_buffer_size 10M;
# Timeouts for large file uploads
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_connect_timeout 10s;
location / {
proxy_pass http://127.0.0.1:4000; # internal port
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
```
### PM2 Ecosystem File
```javascript
// ecosystem.config.js
module.exports = {
apps: [
{
name: 'cshare-api',
script: 'dist/server.js',
instances: 'max', // one per CPU core
exec_mode: 'cluster',
watch: false,
env_production: {
NODE_ENV: 'production',
},
error_file: '/var/log/cshare/api-error.log',
out_file: '/var/log/cshare/api-out.log',
merge_logs: true,
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
},
{
name: 'cshare-worker',
script: 'dist/worker.js',
instances: 1, // single worker instance
exec_mode: 'fork',
watch: false,
env_production: {
NODE_ENV: 'production',
},
},
],
};
```
### PostgreSQL Hardening
```sql
-- Create dedicated app user, never use superuser
CREATE USER cshare WITH PASSWORD 'strong-random-password';
CREATE DATABASE cshare OWNER cshare;
GRANT ALL PRIVILEGES ON DATABASE cshare TO cshare;
-- In postgresql.conf
max_connections = 100
shared_buffers = 256MB -- 25% of RAM on a 1GB server
effective_cache_size = 768MB
work_mem = 4MB
maintenance_work_mem = 64MB
wal_level = replica -- enables point-in-time recovery
archive_mode = on
log_min_duration_statement = 500ms -- log slow queries
```
### Redis Hardening
```conf
# /etc/redis/redis.conf
bind 127.0.0.1 # local only, never expose to internet
requirepass strong-random-password
maxmemory 256mb
maxmemory-policy allkeys-lru
save 900 1 # RDB snapshot
save 300 10
appendonly yes # AOF for durability
appendfsync everysec
```
---
## 9. Security Checklist
### Input/Output
- [ ] All file sizes validated server-side before writing to storage
- [ ] MIME type detected from bytes, not from Content-Type header
- [ ] All file names sanitized (strip `../`, null bytes, control characters)
- [ ] Download served with `Content-Disposition: attachment; filename="..."` (prevents XSS via HTML files)
- [ ] `X-Content-Type-Options: nosniff` on all responses
- [ ] All HTML rendered by the frontend — API is JSON only, no server-side HTML
### Auth
- [ ] Passwords hashed with bcrypt (rounds ≥ 12)
- [ ] Refresh tokens stored as SHA-256 hash in DB (never raw)
- [ ] JWT `jti` blocklist in Redis for immediate invalidation on logout
- [ ] API keys stored as SHA-256 hash (never recoverable after generation)
- [ ] Email verification required before upload on registered accounts
- [ ] Rate limiting on auth endpoints (10 attempts / 15 min per IP)
### Infrastructure
- [ ] PostgreSQL not accessible from internet (bind to `127.0.0.1` or Unix socket)
- [ ] Redis not accessible from internet
- [ ] All secrets in `.env`, never committed to git
- [ ] `.env.example` committed with placeholder values only
- [ ] UFW firewall — only 22, 80, 443, 3000 exposed externally
- [ ] Fail2ban on SSH
- [ ] Non-root user to run Node processes
- [ ] Let's Encrypt TLS certificate auto-renewed via Certbot systemd timer
### Headers (set via Helmet.js)
```typescript
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // Tailwind needs this
imgSrc: ["'self'", "data:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
},
crossOriginEmbedderPolicy: false, // needed for some browser download behaviors
}));
```
---
## 10. Frontend Integration Points
When the backend is ready, each of these frontend mocks needs to be replaced:
### UploadPage.jsx
- Replace `simulateProgress` with a real `XMLHttpRequest` or `fetch` with `ReadableStream` to track upload progress
- Replace `generateShareLink` with `POST /api/v1/files/upload`
- On success, store the real link returned by the API
### DashboardPage.jsx
- Replace `mockFiles` import with `GET /api/v1/files` (paginated)
- Replace `deleteFile` local state mutation with `DELETE /api/v1/files/:id`
- Replace static `totalViews` / `totalDownloads` with `GET /api/v1/dashboard/stats`
### FileViewPage.jsx
- Replace `sharedFile` import with `GET /api/v1/links/:linkId`
- Replace fake download with `GET /api/v1/links/:linkId/download` (redirect to signed R2 URL)
- Handle password-protected links (prompt UI → `POST /api/v1/links/:linkId/verify`)
### Navbar + auth state
- Add a `useAuth` context that holds the decoded access token payload
- Conditionally show "Log in" / "Sign up" vs. user avatar dropdown
- Refresh access token silently using the HTTP-only cookie before it expires
### New pages needed on the frontend
- `/login` — email + password form
- `/register` — email + password + plan selection
- `/account` — manage subscription, change password, generate API keys, delete account
- `/verify-email` — handles the `?token=` query param from the verification email
- `/reset-password` — handles the `?token=` query param
- `404.jsx` — for expired links and unknown routes
---
## 11. Payments — Stripe
### Integration Points
```
POST /api/v1/billing/checkout Create Stripe Checkout session (upgrade to Pro/Team)
POST /api/v1/billing/portal Create Stripe Customer Portal session (manage subscription)
POST /api/v1/billing/webhook Stripe webhook handler (subscription events)
GET /api/v1/billing/status Current subscription status for logged-in user
```
### Webhook Events to Handle
```
checkout.session.completed -> upgrade user plan in DB
customer.subscription.updated -> sync plan changes
customer.subscription.deleted -> downgrade to free, enforce limits immediately
invoice.payment_failed -> email user, flag account
```
### Plan Enforcement
- On upload: check `user.plan` + `user.uploadCount` for Free users
- Monthly counter (`upload_count:{userId}:{YYYY-MM}` in Redis) resets on the first of each month via a cron job
- On downgrade: do not delete existing files, but block new uploads until user is under the Free limit
---
## 12. Observability
### Logging (Pino)
```typescript
import pino from 'pino';
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });
// Log shape for every request
logger.info({ method, path, statusCode, durationMs, userId, fileId });
// Never log: passwords, tokens, file contents, full IP addresses
```
### Metrics (to export to Prometheus or Datadog)
```
cshare_uploads_total counter, labels: plan, status
cshare_upload_bytes_total counter
cshare_downloads_total counter
cshare_active_files_gauge gauge (refreshed every 5 min)
cshare_expired_files_total counter
cshare_api_request_duration_ms histogram, labels: route, method, status
cshare_db_query_duration_ms histogram
cshare_redis_hit_ratio gauge
```
### Error Tracking
- Integrate Sentry (`@sentry/node`) for unhandled exceptions and slow transactions
- Set up a Sentry project, add `SENTRY_DSN` to env
- Sample rate: 100% errors, 10% performance traces in production
### Health Check Endpoint
```typescript
// GET /health
{
status: 'ok',
db: 'connected', // result of `SELECT 1`
redis: 'connected', // result of PING
storage: 'reachable', // result of HeadBucket
uptime: 12345, // process.uptime()
version: '1.0.0',
}
```
---
## 13. Implementation Order
Work in this exact sequence to always have something deployable:
```
Phase 1 — Data Layer (no network, just local)
[ ] PostgreSQL installed + hardened
[ ] Redis installed + hardened
[ ] Drizzle schema written + first migration run
[ ] Test: can create a user, a file row, and query it
Phase 2 — API Core (no auth, no storage)
[ ] Express app with Helmet, CORS, logger, error handler
[ ] POST /api/v1/files/upload (local disk for now, fake link response)
[ ] GET /api/v1/links/:linkId (returns hardcoded metadata)
[ ] Frontend: wire UploadPage to the real endpoint, replace simulateProgress
[ ] Frontend: wire FileViewPage to the real endpoint
[ ] Confirm end-to-end works with local disk storage
Phase 3 — Storage
[ ] Cloudflare R2 bucket created + access keys
[ ] storage.ts service (upload, delete, signed URL generation)
[ ] Replace local disk with R2 in upload handler
[ ] Test: upload → get link → download file → file is actually the right bytes
Phase 4 — Auth
[ ] POST /api/auth/register + bcrypt
[ ] POST /api/auth/login + JWT issuance + refresh cookie
[ ] POST /api/auth/refresh + GET /api/auth/me
[ ] POST /api/auth/logout + blocklist jti in Redis
[ ] requireAuth middleware on upload/list/delete endpoints
[ ] Frontend: /login and /register pages + useAuth context
Phase 5 — Rate Limiting + Plan Enforcement
[ ] rate-limiter-flexible on upload, download, auth routes
[ ] Free monthly upload counter in Redis
[ ] Plan checks in upload handler (size limit, count limit)
Phase 6 — Workers
[ ] Expiry worker (cron every 60s)
[ ] BullMQ email queue + Resend integration
[ ] Email verification flow (register → email → /verify-email)
[ ] Password reset flow
Phase 7 — Dashboard & Analytics
[ ] GET /api/v1/files (list with pagination)
[ ] DELETE /api/v1/files/:id
[ ] POST /api/v1/events (record view/download)
[ ] GET /api/v1/dashboard/stats
[ ] Frontend: wire DashboardPage to real data
Phase 8 — Payments
[ ] Stripe account + products (Pro, Team)
[ ] POST /api/v1/billing/checkout + portal
[ ] POST /api/v1/billing/webhook
[ ] Frontend: /pricing Buy buttons → Stripe Checkout
Phase 9 — Production hardening
[ ] Sentry integration (backend + frontend)
[ ] Pino structured logging
[ ] PM2 cluster mode
[ ] Nginx tuned for large file uploads
[ ] UFW + Fail2ban
[ ] Let's Encrypt TLS
[ ] Cloudflare proxying for cshare.app domain
[ ] Uptime monitoring (UptimeRobot or Better Uptime)
[ ] Weekly DB backups to a separate R2 bucket (pg_dump → upload)
[ ] Runbook: how to roll back a migration, restart workers, rotate secrets
```
---
## 14. Estimated Effort
| Phase | Complexity | Notes |
|---|---|---|
| Data Layer | Low | Schema is defined above — mostly setup |
| API Core | Medium | Express boilerplate + multipart upload |
| Storage | Low | S3 SDK is straightforward |
| Auth | High | Many edge cases: token refresh, blocklist, verification |
| Rate Limiting | Low | `rate-limiter-flexible` handles the hard parts |
| Workers | Medium | Expiry is simple; BullMQ setup takes time |
| Dashboard | Medium | Pagination + analytics aggregation |
| Payments | High | Stripe webhook reliability is tricky |
| Hardening | Medium | Mostly config, but easy to get wrong |
Total realistic estimate for a solo developer: **6–10 weeks** for Phases 1–8 at a sustainable pace.
+66
View File
@@ -0,0 +1,66 @@
# Production Notes
## Build
Frontend:
```sh
cd cshare-app
npm ci
npm run check
```
`npm run build` also creates `.gz` and `.br` siblings for compressible files in `dist/`.
Backend:
```sh
cd backend
npm ci
npm run check
NODE_ENV=production npm start
```
## Required Environment
Set these before starting the backend in production:
```sh
NODE_ENV=production
PORT=4001
FRONTEND_URL=https://cshare.app
PUBLIC_APP_URL=https://cshare.app
JWT_ACCESS_SECRET=<64+ random chars>
JWT_REFRESH_SECRET=<64+ random chars>
ADMIN_API_KEY=<64+ random chars>
PREVIEW_MAX_FILE_SIZE_BYTES=25000000
DATA_DIR=/var/lib/cshare
```
Set `VITE_API_BASE_URL` for the frontend only when the API is not served from the same origin. If omitted in production, the frontend calls `window.location.origin`, which works with an Nginx reverse proxy that forwards `/api` to the backend.
## Runtime
- Serve `cshare-app/dist/` with Nginx or another static host.
- Cache hashed files under `/assets/` aggressively.
- Serve precompressed `.br` first and `.gz` second when the client supports them.
- Do not cache `/index.html`; it must be revalidated so deploys pick up new asset hashes.
- Proxy `/api`, `/health`, and file download routes to the backend.
- Keep `backend/data/` outside the repo and backed up if using the local disk adapter.
- Protect every `/api/admin/*` request with `x-admin-key: $ADMIN_API_KEY`.
- Small safe file previews are served from `/api/v1/links/:linkId/preview`; files over `PREVIEW_MAX_FILE_SIZE_BYTES` remain download-only.
- The admin API is disabled unless `ADMIN_API_KEY` is set.
Example Nginx compression settings:
```nginx
brotli on;
brotli_static on;
gzip on;
gzip_static on;
gzip_types text/css application/javascript application/json image/svg+xml;
```
`brotli_static` requires an Nginx build with the Brotli module. If that module is unavailable, keep `gzip_static on` and serve the generated `.gz` files.
The current backend is production-shaped but still uses local JSON and disk storage. For durable production use, replace it with the PostgreSQL, Redis, and R2 adapters described in `CONTINUE.md`.
+41
View File
@@ -0,0 +1,41 @@
# CShare
CShare is a production-shaped file sharing app with a Vite/React frontend and a TypeScript/Express backend. It supports file upload, expiring share links, recipient previews for small safe files, downloads, dashboard stats, and an admin API.
## Project Layout
- `cshare-app/` - React frontend, Vite build, precompressed static assets.
- `backend/` - Express API, local disk storage adapter, JSON metadata store, auth primitives, admin API.
- `PRODUCTION.md` - deployment notes, required env, Nginx guidance.
- `deploy/` - sample service and reverse-proxy config.
## Local Development
Backend:
```sh
cd backend
npm install
npm run dev
```
Frontend:
```sh
cd cshare-app
npm install
VITE_API_BASE_URL=http://localhost:4001 npm run dev
```
## Final Checks
```sh
cd backend && npm run check
cd ../cshare-app && npm run check
```
Frontend builds create gzip and Brotli assets in `cshare-app/dist/`. Backend production startup requires strong `JWT_ACCESS_SECRET`, `JWT_REFRESH_SECRET`, and `ADMIN_API_KEY` values.
## Publish Notes
Before publishing, configure the environment in `PRODUCTION.md`, serve `cshare-app/dist/` with a static host or Nginx, and proxy `/api` plus `/health` to the backend. The included local JSON/disk persistence is suitable for a single-server launch; move to PostgreSQL/R2 before depending on durable multi-server storage.
+30
View File
@@ -0,0 +1,30 @@
# CShare Backend
This is the local production-path API scaffold from `CONTINUE.md`. It uses Express 5 and TypeScript with local JSON metadata plus disk storage so upload, share, preview, download, dashboard, and auth flows work without external infrastructure.
## Commands
```sh
npm install
npm run dev
npm run check
npm test
npm run build
```
The API listens on `http://localhost:4001` by default. Frontend requests use `VITE_API_BASE_URL`; in production, the frontend falls back to same-origin API calls when the variable is omitted.
## Implemented Locally
- `GET /health`
- `POST /api/auth/register`, `POST /api/auth/login`, `POST /api/auth/refresh`, `POST /api/auth/logout`, `GET /api/auth/me`
- `POST /api/v1/files/upload`
- `GET /api/v1/files`, `GET /api/v1/files/:id`, `DELETE /api/v1/files/:id`
- `GET /api/v1/links/:linkId`, `GET /api/v1/links/:linkId/download`
- `GET /api/v1/links/:linkId/preview`
- `GET /api/v1/dashboard/stats`
- `GET /api/admin/overview`, `/api/admin/files`, `/api/admin/users`, `/api/admin/events` protected by `x-admin-key`
## Production Adapters Still Needed
The remaining roadmap items require provisioned services and credentials: PostgreSQL/Drizzle migrations, Redis/BullMQ, Cloudflare R2, Resend, Stripe, Sentry, PM2, Nginx, TLS, firewall rules, backups, and DNS. The local API keeps storage and persistence isolated so those adapters can replace the JSON/disk implementation without changing frontend routes.
+25
View File
@@ -0,0 +1,25 @@
# CShare Frontend
React/Vite frontend for CShare.
## Commands
```sh
npm install
npm run dev
npm run check
npm run build
npm run preview
```
`npm run build` emits production assets to `dist/` and precompresses them with gzip and Brotli.
## Configuration
Set `VITE_API_BASE_URL` when the API is hosted on a different origin:
```sh
VITE_API_BASE_URL=https://api.example.com
```
If omitted in production, the app uses same-origin API calls, which is the preferred setup behind an Nginx reverse proxy.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

@@ -0,0 +1,167 @@
# CShare Dashboard Enhancement & App Polish
**Date:** 2026-05-30
**Status:** Approved
**Approach:** Parallel — backend analytics API built simultaneously with frontend enhancements
---
## Context
CShare has a working frontend (12+ pages, React Router v7, dark theme design system) and a live backend (Express 5 + TypeScript, local JSON storage). The biggest gaps are in the dashboard experience and overall app polish. This spec covers both tracks.
**Strategy:** Start simple, grow. Focus on the dashboard (where returning users live) and app-wide polish (makes everything feel trustworthy). The backend analytics API is built in parallel, and the frontend starts with mock data then wires to real endpoints.
---
## Track 1: Backend — Analytics & Dashboard API
### New/Expanded Endpoints
| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/api/v1/dashboard/stats` | Aggregate: total files, storage used, total views, total downloads, active links, expiring soon count |
| GET | `/api/v1/dashboard/activity` | Recent activity feed: last 20 events (views, downloads, uploads) with timestamps |
| GET | `/api/v1/files/:id/analytics` | Per-file: view count, download count, daily breakdown (last 30 days), top referrers |
| POST | `/api/v1/events` | Record a view/download event (called when links are resolved) |
| GET | `/api/v1/dashboard/storage` | Storage breakdown by file type, monthly upload volume |
### Event Recording
- `GET /api/v1/links/:linkId` automatically records a `view` event
- `GET /api/v1/links/:linkId/download` records a `download` event
- Events include: timestamp, hashed IP, user agent
- Stored in the existing events table
### Stats Computation
- Dashboard stats computed on-the-fly from DB (no caching initially)
- If performance becomes an issue, add Redis caching with 5-minute TTL
- Per-file analytics queries use a 30-day window by default
### Files to Create/Modify
- `backend/src/routes/analytics.ts` — new file for analytics endpoints
- `backend/src/routes/dashboard.ts` — expand existing file
- `backend/src/services/analytics.ts` — new service for event recording and aggregation
- `backend/src/routes/links.ts` — modify to record events on link resolution
---
## Track 2: Frontend — Dashboard Enhancement
**Principle:** Enhance the existing `DashboardPage.jsx`, do not rewrite.
### Stat Cards Enhancement
- Animated count-up on first render (number rolls from 0 to actual value)
- Subtle gradient backgrounds per metric theme
- Trend indicators comparing to previous period (up/down arrow + percentage)
### File Table Enhancement
- Inline file type icons with color coding (using existing `getFileIcon` and `getFileIconColor`)
- Quick-action buttons on hover: copy link, download, delete
- Click-to-expand row revealing file details: analytics preview, share link, expiry countdown
- Better sort controls: date, name, size, type with visual indicators
### New Components (added within existing dashboard structure)
- **Search + filters bar** — above the table, type filter chips, date range, size range
- **Storage usage bar** — near stats cards, shows used vs. available
- **Recent activity feed** — compact section below stats, last 5-10 events with timestamps and badges
- **File detail slide-over** — triggered by clicking a file row, contains:
- File info (name, size, type, upload date, expiry)
- Share link with copy button
- QR code for the share link
- Analytics mini-chart (views/downloads over last 30 days)
- Delete action with confirmation
- **Batch operations** — checkbox column, bulk delete, bulk copy links
### Polish Details
- Smooth animations on card hover, row expand, filter changes
- Skeleton loading states while data loads
- Better empty state for new users with upload CTA
### Files to Create/Modify
- `cshare-app/src/pages/DashboardPage.jsx` — enhance existing
- `cshare-app/src/components/StatsCards.jsx` — new, extracted stat cards section
- `cshare-app/src/components/FileDetailSlideOver.jsx` — new slide-over panel
- `cshare-app/src/components/ActivityFeed.jsx` — new activity feed component
- `cshare-app/src/components/StorageBar.jsx` — new storage usage indicator
- `cshare-app/src/components/SkeletonLoader.jsx` — new reusable skeleton
---
## Track 3: Frontend — App-Wide Polish & Micro-Interactions
### Animations & Transitions
- Page transitions: subtle fade + slide between route changes
- Staggered entrance animations on card grids and file lists (elements appear sequentially)
- Button press feedback: scale-down on mousedown, spring-back on release
- Card hover: gentle lift with shadow deepening
### Loading States
- Skeleton screens for file lists, stat cards, and page content (no spinners)
- Upload progress: smoother easing curves on the progress bar
- File preview loading: blur-up placeholder technique
### Mobile Refinements
- Swipe gestures on file cards (swipe left to reveal delete)
- Better touch targets: minimum 44px hit areas
- Bottom sheet for file details on mobile (instead of slide-over)
- Pull-to-refresh on dashboard
### Micro-Interactions
- Copy-to-clipboard: checkmark animation + toast
- Delete confirmation: slide-in modal with undo option
- Share link generation: animated link appearance with "Copied!" feedback
- File type icons: subtle bounce on appearance
### Other
- 404 page with CShare branding and "Upload a file" CTA
- Better scroll-to-top on navigation
- Enhance existing `useScrollReveal` for below-fold content
### Files to Create/Modify
- `cshare-app/src/pages/NotFoundPage.jsx` — new 404 page
- `cshare-app/src/components/PageTransition.jsx` — new route transition wrapper
- `cshare-app/src/hooks/useCountUp.js` — new animated number hook
- `cshare-app/src/hooks/useSwipe.js` — new swipe gesture hook
- `cshare-app/src/components/SkeletonLoader.jsx` — reusable skeleton
- `cshare-app/src/index.css` — new animation keyframes and utility classes
- Various page files — apply transitions and polish
---
## Implementation Order
1. **Skeleton + loading states** — immediately improves perceived performance
2. **Dashboard stat cards enhancement** — animated count-up, trend indicators
3. **File table enhancements** — hover actions, expand rows, sort controls
4. **Backend analytics API** — event recording, stats endpoints (parallel with #2-3)
5. **File detail slide-over** — info, share link, QR code, analytics chart
6. **Activity feed + storage bar** — new dashboard sections
7. **Page transitions** — route-level fade + slide
8. **Micro-interactions** — copy feedback, delete undo, icon bounce
9. **Mobile refinements** — swipe, bottom sheet, touch targets
10. **404 page** — standalone task
11. **Wire frontend to real API** — swap mock data for analytics endpoints
---
## Out of Scope
- Authentication pages (login/register/account) — separate effort
- Stripe payments integration
- PostgreSQL migration
- Cloudflare R2 storage migration
- Email verification flows