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
6 changes: 6 additions & 0 deletions .vercelignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ DEPLOYMENT.md
eslint.config.mjs
vitest.config.ts

# Cloudflare Worker backend (separate deployment)
cloudflare-worker/

# Documentation
docs/

# Misc
.idea/
.vscode/
Expand Down
26 changes: 26 additions & 0 deletions cloudflare-worker/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# GEM Enterprise Worker — Environment Variables
# Copy this file to .env and fill in the values for local development.

# ── Cloudflare Account ────────────────────────────────────────────────────────
# These are set as Wrangler secrets in production, not env vars.
CLOUDFLARE_API_TOKEN=your-api-token-here
CLOUDFLARE_ACCOUNT_ID=your-account-id-here
CLOUDFLARE_ZONE_ID=your-zone-id-here

# ── Auth ──────────────────────────────────────────────────────────────────────
# Must match the JWT_SECRET used by the Vercel frontend (src/lib/auth.ts)
JWT_SECRET=your-jwt-secret-at-least-32-characters-long

# ── D1 Database ───────────────────────────────────────────────────────────────
# Managed by wrangler.toml — no env var needed for local dev.
# For remote: set database_id in wrangler.toml

# ── R2 Bucket ─────────────────────────────────────────────────────────────────
# Managed by wrangler.toml — no env var needed for local dev.

# ── KV Namespace ──────────────────────────────────────────────────────────────
# Managed by wrangler.toml — no env var needed for local dev.

# ── Frontend URL (for CORS) ──────────────────────────────────────────────────
FRONTEND_URL=http://localhost:3000
CORS_ORIGINS=http://localhost:3000,http://localhost:8787
5 changes: 5 additions & 0 deletions cloudflare-worker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node_modules/
dist/
.wrangler/
.dev.vars
.env
137 changes: 137 additions & 0 deletions cloudflare-worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# GEM Enterprise — Cloudflare Worker Backend

Operational backend for GEM Enterprise, running on Cloudflare Workers with D1, R2, and KV.

## Architecture

```
┌─────────────────────────────┐ ┌──────────────────────────────┐
│ Vercel (Next.js Frontend) │────▶│ Cloudflare Worker (Backend) │
│ gemcybersecurityassist.com │ │ gem-enterprise-worker │
│ │ │ │
│ • Pages / App Router │ │ • Auth validation (JWT) │
│ • Next.js API routes │ │ • RBAC engine │
│ • Prisma → PostgreSQL │ │ • KYC service hooks │
│ • Server-side rendering │ │ • Document vault (R2) │
└─────────────────────────────┘ │ • Service requests (D1) │
│ • Audit logging (D1) │
│ • Notifications (D1 + KV) │
└──────────────────────────────┘
```

## Quick Start

```bash
# Install dependencies
pnpm install

# Run local D1 migrations
pnpm run db:migrate

# Start dev server (port 8787)
pnpm dev

# Type check
pnpm typecheck
```

## Endpoints

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/health` | No | Service health check |
| GET | `/api/ready` | No | Readiness probe |
| GET | `/api/version` | No | Version info |
| POST | `/api/auth/validate` | No | Validate a JWT |
| GET | `/api/auth/session` | Yes | Current session |
| GET | `/api/rbac/permissions` | Yes | User permissions |
| POST | `/api/rbac/check` | Yes | Check permission |
| GET | `/api/rbac/roles` | Admin | List roles |
| POST | `/api/rbac/assign` | Admin | Assign role |
| POST | `/api/kyc/webhook` | Admin | KYC status webhook |
| GET | `/api/kyc/status/:id` | Yes | KYC status |
| GET | `/api/kyc/pending` | Analyst | Pending KYC list |
| POST | `/api/documents/upload` | Yes | Upload to R2 |
| GET | `/api/documents` | Yes | List documents |
| GET | `/api/documents/:id/download` | Yes | Download document |
| DELETE | `/api/documents/:id` | Admin | Soft-delete document |
| GET | `/api/service-requests` | Yes | List requests |
| POST | `/api/service-requests` | Yes | Create request |
| GET | `/api/service-requests/:id` | Yes | Get request |
| PATCH | `/api/service-requests/:id` | Yes | Update request |
| GET | `/api/audit/logs` | Admin | Query audit logs |
| GET | `/api/audit/logs/:id` | Admin | Get audit entry |
| GET | `/api/audit/summary` | Admin | Audit stats |
| GET | `/api/notifications` | Yes | List notifications |
| POST | `/api/notifications` | Admin | Create notification |
| POST | `/api/notifications/bulk` | Admin | Bulk send |
| PATCH | `/api/notifications/:id/read` | Yes | Mark read |
| PATCH | `/api/notifications/read-all` | Yes | Mark all read |

## Cloudflare Services

| Service | Binding | Purpose |
|---------|---------|---------|
| D1 | `DB` | Audit logs, KYC events, service requests, notifications |
| R2 | `VAULT` | Document storage (encrypted at rest) |
| KV | `CACHE` | Session cache, rate limiting |

## Secrets

Set via `wrangler secret put <NAME>`:

```bash
wrangler secret put JWT_SECRET
wrangler secret put CLOUDFLARE_API_TOKEN
wrangler secret put CLOUDFLARE_ACCOUNT_ID
wrangler secret put CLOUDFLARE_ZONE_ID
```

## Deployment

```bash
# Dry run (validates config, does not deploy)
pnpm run deploy:dry

# Deploy to production
pnpm run deploy

# Deploy to staging
pnpm run deploy -- --env staging

# Apply D1 migrations to remote
pnpm run db:migrate:remote
```

## Project Structure

```
cloudflare-worker/
├── src/
│ ├── index.ts # Hono app entry point
│ ├── routes/
│ │ ├── health.ts # /api/health, /api/ready, /api/version
│ │ ├── auth.ts # /api/auth/*
│ │ ├── rbac.ts # /api/rbac/*
│ │ ├── kyc.ts # /api/kyc/*
│ │ ├── documents.ts # /api/documents/*
│ │ ├── service-requests.ts
│ │ ├── audit.ts # /api/audit/*
│ │ └── notifications.ts # /api/notifications/*
│ ├── middleware/
│ │ ├── auth.ts # JWT verification + RBAC middleware
│ │ ├── cors.ts # CORS with origin whitelist
│ │ └── audit.ts # Audit log helper
│ ├── types/
│ │ ├── env.ts # Env bindings type
│ │ └── api.ts # Response types
│ └── services/ # Business logic (future)
├── migrations/
│ └── 0001_initial_schema.sql
├── openapi/
│ └── spec.yaml
├── wrangler.toml
├── tsconfig.json
├── package.json
└── .env.example
```
93 changes: 93 additions & 0 deletions cloudflare-worker/migrations/0001_initial_schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
-- GEM Enterprise Worker — D1 Initial Schema
-- This schema mirrors key models from the Prisma schema for the Cloudflare Worker backend.
-- The Vercel frontend continues to use PostgreSQL via Prisma; this D1 database
-- serves as the operational data store for the Cloudflare Worker.

-- ── Audit Logs ────────────────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS audit_logs (
id TEXT PRIMARY KEY,
user_id TEXT,
action TEXT NOT NULL,
resource TEXT,
resource_id TEXT,
metadata TEXT, -- JSON
ip_address TEXT,
user_agent TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action);
CREATE INDEX IF NOT EXISTS idx_audit_logs_resource ON audit_logs(resource);
CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at);

-- ── KYC Events ────────────────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS kyc_events (
id TEXT PRIMARY KEY,
application_id TEXT NOT NULL,
user_id TEXT NOT NULL,
event TEXT NOT NULL,
status TEXT NOT NULL,
metadata TEXT, -- JSON
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_kyc_events_application_id ON kyc_events(application_id);
CREATE INDEX IF NOT EXISTS idx_kyc_events_user_id ON kyc_events(user_id);
CREATE INDEX IF NOT EXISTS idx_kyc_events_event ON kyc_events(event);

-- ── Document Vault ────────────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS document_vault (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
r2_key TEXT NOT NULL UNIQUE,
file_name TEXT NOT NULL,
file_type TEXT NOT NULL,
file_size INTEGER NOT NULL,
category TEXT NOT NULL DEFAULT 'general',
deleted_at TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_document_vault_user_id ON document_vault(user_id);
CREATE INDEX IF NOT EXISTS idx_document_vault_category ON document_vault(category);

-- ── Service Requests ──────────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS service_requests (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL,
priority TEXT NOT NULL DEFAULT 'medium',
status TEXT NOT NULL DEFAULT 'open',
assigned_to TEXT,
resolution TEXT,
metadata TEXT, -- JSON
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_service_requests_user_id ON service_requests(user_id);
CREATE INDEX IF NOT EXISTS idx_service_requests_status ON service_requests(status);
CREATE INDEX IF NOT EXISTS idx_service_requests_priority ON service_requests(priority);

-- ── Notifications ─────────────────────────────────────────────────────────────

CREATE TABLE IF NOT EXISTS notifications (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
title TEXT NOT NULL,
message TEXT NOT NULL,
channel TEXT NOT NULL DEFAULT 'in_app',
read INTEGER NOT NULL DEFAULT 0,
metadata TEXT, -- JSON
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);

CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id);
CREATE INDEX IF NOT EXISTS idx_notifications_read ON notifications(read);
CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at);
Loading
Loading