Skip to content

Commit 057c4a9

Browse files
lucas19919claude
andcommitted
Rewrite API into route modules, harden sessions, drop scraper + MCP
Lands the reorganisation documented in PROGRESS.md: - Structure: api/src/index.ts becomes a slim composition root; every endpoint moves to api/src/routes/* (one module per domain) with shared middleware.ts and helpers.ts - Security: server-side sessions (SHA-256 token hashes, real revocation on logout/password change), SESSION_SECRET removed, hono/csrf origin checks, secure headers (+ CSP in prod), request-size limits, proxy-aware login rate limiting, audited logins, /api/settings admin-only both directions - Efficiency: bulk-loaded document items/payments, explicit column lists so signed-doc BLOBs stay out of list queries - Removed: scraper/ workspace and /api/scraper/*, stale mcp/ workspace, SERVICE_TOKEN, docs/INTEGRATIONS.md - isarwebsites: fresh installs seed an 11-item Leistungskatalog and default business name; prompts/branding/docs updated 125 API tests green, api typecheck clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 6b1439e commit 057c4a9

67 files changed

Lines changed: 2136 additions & 6574 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/launch.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,28 @@
1010
{
1111
"name": "openleads-api",
1212
"runtimeExecutable": "npm",
13-
"runtimeArgs": ["--prefix", "D:/Repos/openleads/api", "run", "dev"],
13+
"runtimeArgs": ["--prefix", "D:/Repos/openleads/api", "run", "start"],
1414
"port": 8787
1515
},
1616
{
1717
"name": "openleads-web",
1818
"runtimeExecutable": "npm",
1919
"runtimeArgs": ["--prefix", "D:/Repos/openleads/web", "run", "dev"],
2020
"port": 5173
21+
},
22+
{
23+
"name": "openleads-web-alt",
24+
"runtimeExecutable": "npm",
25+
"runtimeArgs": ["--prefix", "D:/Repos/openleads/web", "run", "dev", "--", "--port", "5175"],
26+
"port": 5175,
27+
"autoPort": false
28+
},
29+
{
30+
"name": "openleads-web-redesign",
31+
"runtimeExecutable": "npm",
32+
"runtimeArgs": ["--prefix", "D:/Repos/openleads/web", "run", "dev", "--", "--port", "5176", "--strictPort"],
33+
"port": 5176,
34+
"autoPort": false
2135
}
2236
]
2337
}
Lines changed: 42 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,31 @@
11
---
22
name: setup-openleads
33
description: >-
4-
Sets up a working OpenLeads instance from a fresh clone — installs the api/,
5-
web/ and (optional) scraper/ workspaces, creates the .env files with freshly
6-
generated secrets (SESSION_SECRET, SETTINGS_KEY, SERVICE_TOKEN), seeds a login,
7-
optionally wires up local Ollama AI, then starts and verifies the dev servers.
8-
Use this whenever someone wants to set up, install, bootstrap, configure, get
9-
started with, onboard onto, or "run OpenLeads for the first time" — including
10-
vague asks like "how do I run this?", "get this working", "set it up", or
11-
"I just cloned this, now what?" while in the OpenLeads repo. Also the entry
12-
point for production deployment, which it routes to deploy/DEPLOY.md.
4+
Sets up a working OpenLeads instance from a fresh clone — installs the api/
5+
and web/ workspaces, creates api/.env with a freshly generated SETTINGS_KEY,
6+
seeds a login, optionally wires up local Ollama AI, then starts and verifies
7+
the dev servers. Use this whenever someone wants to set up, install,
8+
bootstrap, configure, get started with, onboard onto, or "run OpenLeads for
9+
the first time" — including vague asks like "how do I run this?", "get this
10+
working", "set it up", or "I just cloned this, now what?" while in the
11+
OpenLeads repo. Also the entry point for production deployment, which it
12+
routes to deploy/DEPLOY.md.
1313
---
1414

1515
# Set up OpenLeads
1616

1717
Drive a first-time OpenLeads setup end to end: dependencies → secrets → login →
18-
running, verified servers. OpenLeads is a three-workspace monorepo — `api/`
19-
(Hono + `node:sqlite`), `web/` (React + Vite), `scraper/` (Anthropic SDK) — so
20-
"setup" means getting the pieces installed, configured, and talking to each
21-
other, not just one `npm install`.
18+
running, verified servers. OpenLeads is a two-workspace monorepo — `api/`
19+
(Hono + `node:sqlite`) and `web/` (React + Vite) — so "setup" means getting the
20+
pieces installed, configured, and talking to each other, not just one
21+
`npm install`.
2222

2323
The canonical instructions live in the repo and may drift ahead of this skill.
2424
**Read these first and treat them as the source of truth** if they disagree with
2525
anything below:
2626

2727
- `README.md` → "Quick start (development)" and "Configuration"
28-
- `api/.env.example` and `scraper/.env.example` → the authoritative env var list
28+
- `api/.env.example` → the authoritative env var list
2929
- `deploy/DEPLOY.md` → production (Docker Compose + nginx)
3030

3131
## Step 0 — Decide scope and gather inputs
@@ -38,10 +38,7 @@ Ask the user only what you can't infer, then proceed. Don't over-interview.
3838
2. **Login credentials** — you need a username and password to seed the first
3939
user. If they don't offer one, suggest `admin` and generate a strong password
4040
(and show it to them so they can save it).
41-
3. **Scraper?** Optional. Only needed to auto-discover leads, and it needs an
42-
`ANTHROPIC_API_KEY`. Skip it if they don't have a key or don't ask — the CRM
43-
and invoicing run fine without it.
44-
4. **Local AI (chat copilot)?** Optional. The Chat module needs an
41+
3. **Local AI (chat copilot)?** Optional. The Chat module needs an
4542
OpenAI-compatible endpoint; the default targets a **local Ollama**. Skip if
4643
they don't want it — the rest of the app still runs, only Chat is inert.
4744

@@ -56,7 +53,7 @@ OpenLeads uses Node's built-in SQLite, which needs a recent Node:
5653
`node --version` and stop here with a clear message if it's older — nothing
5754
downstream will work.
5855
- `npm` ships with Node.
59-
- Ollama is only needed if they chose local AI (Step 5).
56+
- Ollama is only needed if they chose local AI (Step 4).
6057

6158
## Step 2 — API (`api/`)
6259

@@ -72,22 +69,18 @@ already there, read it and only fill missing secrets; otherwise start from the
7269
example:
7370

7471
1. Read `api/.env.example` to get the current, authoritative set of variables.
75-
2. Generate three independent secrets (see
76-
[Generating secrets](#generating-secrets)):
77-
- `SESSION_SECRET` — signs session cookies
72+
2. Generate one secret (see [Generating secrets](#generating-secrets)):
7873
- `SETTINGS_KEY` — AES-256-GCM key for credentials saved via the Settings UI
79-
- `SERVICE_TOKEN` — bearer token the scraper uses to POST leads
80-
3. Write `api/.env` from the example with those three placeholders replaced.
81-
Leave `NODE_ENV=development`, `DB_PATH`, ports, and the AI/SMTP block at their
74+
3. Write `api/.env` from the example with that placeholder replaced. Leave
75+
`NODE_ENV=development`, `DB_PATH`, ports, and the AI/SMTP block at their
8276
example defaults unless the user asked otherwise.
8377

84-
> **Why generate all three even in dev?** In development the app falls back to
85-
> insecure built-in defaults (with a warning) if these are unset, so it *would*
86-
> boot without them. But it **fails closed in production**`api/src/auth.ts`
87-
> throws if `SESSION_SECRET` is the dev default under `NODE_ENV=production`, and
88-
> `api/src/secrets.ts` refuses to encrypt credentials without a real
89-
> `SETTINGS_KEY`. Generating them now means the same `.env` works when they later
90-
> flip to production, and it's the correct habit to model.
78+
> **Why generate it even in dev?** In development the app falls back to an
79+
> insecure built-in default (with a warning), so it *would* boot without it. But
80+
> it **fails closed in production**`api/src/secrets.ts` refuses to encrypt
81+
> credentials without a real `SETTINGS_KEY`. Generating it now means the same
82+
> `.env` works when they later flip to production. (Sessions are stored
83+
> server-side in the DB — there is no session secret anymore.)
9184
9285
**Seed the login** (creates the first user; the app has no signup):
9386

@@ -110,50 +103,23 @@ No env file needed in dev — Vite serves on **http://localhost:5173** and proxi
110103
`web/vite.config.ts`). (If `README.md` quotes a different port, trust the actual
111104
`vite.config.ts`.)
112105

113-
## Step 4 — Scraper (`scraper/`, optional)
114-
115-
Skip unless the user wants automated lead discovery and has an Anthropic key.
116-
117-
```
118-
cd ../scraper
119-
npm install
120-
```
121-
122-
Create `scraper/.env` from `scraper/.env.example`, then set:
123-
124-
- `ANTHROPIC_API_KEY` — their key from https://console.anthropic.com
125-
- `CRM_API_URL``http://127.0.0.1:8787` for local
126-
- `CRM_SERVICE_TOKEN`**must exactly equal** the `SERVICE_TOKEN` you put in
127-
`api/.env`. This is the single most common setup mistake: if they differ, the
128-
API rejects every posted lead with 401 and nothing shows up. Copy the value
129-
across literally.
130-
131-
Verify offline first — this uses fixtures, no Claude calls, no network:
132-
133-
```
134-
npm run dry-run
135-
```
136-
137-
A live run is `npm start` (costs Anthropic tokens) — only run it if asked.
138-
139-
## Step 5 — Local AI / Ollama (optional)
106+
## Step 4 — Local AI / Ollama (optional)
140107

141108
Only if the user wants the Chat copilot. The API defaults
142109
(`AI_BASE_URL=http://localhost:11434/v1`, `AI_MODEL=llama3.1:8b`) target a local
143110
Ollama, so no `api/.env` change is needed — just make the models available:
144111

145112
1. Install Ollama (https://ollama.com) if `ollama --version` fails.
146-
2. Pull the chat + embedding models named in `api/.env.example`:
113+
2. Pull the chat model named in `api/.env.example`:
147114
```
148115
ollama pull llama3.1:8b
149-
ollama pull nomic-embed-text
150116
```
151117

152118
If they'd rather use a hosted OpenAI-compatible endpoint, set `AI_BASE_URL`,
153119
`AI_MODEL`, and `AI_API_KEY` in `api/.env` instead (or configure it later in the
154120
Settings page, which encrypts the key at rest with `SETTINGS_KEY`).
155121

156-
## Step 6 — Start and verify
122+
## Step 5 — Start and verify
157123

158124
Start both dev servers (run them in the background / separate processes so they
159125
keep running):
@@ -173,24 +139,25 @@ Then **verify rather than assuming** — don't just report "done":
173139
- Confirm Vite is serving on 5173.
174140
- Tell the user to open **http://localhost:5173** and log in with the credentials
175141
from Step 2. The UI is in German (it targets the DACH market) — mention that so
176-
they're not surprised.
142+
they're not surprised. A fresh database starts with the isarwebsites
143+
Leistungskatalog (website packages, hosting/Pflege, SEO) prefilled.
177144

178145
Report exactly what came up and what's still optional/disabled (e.g. "Chat is
179-
inactive until you finish Step 5", "lead discovery off — no scraper key").
146+
inactive until you finish Step 4").
180147

181148
## Production
182149

183-
Don't hand-roll this — OpenLeads ships one Docker image (web + API + scraper)
184-
and `deploy/DEPLOY.md` is the maintained walkthrough (Docker Compose, nginx +
185-
TLS, secrets via `api.env`/`scraper.env`, seeding the login in-container,
186-
scheduling the scraper via cron, backups). Read it and follow it, adapting the
187-
domain and secrets to the user's host. The same secret rules apply, plus
188-
`WEB_ORIGIN` must be the public origin and `NODE_ENV=production` (which makes the
189-
secret checks fail-closed — so the secrets are now mandatory, not optional).
150+
Don't hand-roll this — OpenLeads ships one Docker image (web + API) and
151+
`deploy/DEPLOY.md` is the maintained walkthrough (Docker Compose, nginx + TLS,
152+
secrets via `api.env`, seeding the login in-container, backups). Read it and
153+
follow it, adapting the domain and secrets to the user's host. The same secret
154+
rules apply, plus `WEB_ORIGIN` must be the public origin and
155+
`NODE_ENV=production` (which makes the secret checks fail-closed — so
156+
`SETTINGS_KEY` is then mandatory, not optional).
190157

191158
## Generating secrets
192159

193-
Each secret is an independent long random string. Cross-platform:
160+
A secret is a long random string. Cross-platform:
194161

195162
**Node (works everywhere Node is installed — the safe default here):**
196163
```
@@ -199,17 +166,15 @@ node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
199166

200167
**macOS / Linux:** `openssl rand -hex 32`
201168

202-
Generate a *fresh* value per secret — never reuse one string for two variables,
203-
and never reuse the example's `change-me-...` placeholders.
169+
Never reuse the example's `change-me-...` placeholder.
204170

205171
## Common pitfalls
206172

207173
- **Node too old**`node:sqlite` import fails. Needs 22.5+.
208-
- **`SERVICE_TOKEN``CRM_SERVICE_TOKEN`** → scraper gets 401, no leads land.
209174
- **Editing `.env.example` instead of `.env`** → app reads `.env`; the example is
210175
only a template. Never commit `.env`.
211176
- **Treating the `ExperimentalWarning` as a failure** → it's normal.
212177
- **Expecting Chat to work without an AI endpoint** → it's inert until Ollama (or
213178
a hosted endpoint) is reachable; the rest of the app is unaffected.
214-
- **Production boot refuses to start** → that's the fail-closed check; set real
215-
`SESSION_SECRET` / `SETTINGS_KEY`, don't downgrade `NODE_ENV`.
179+
- **Saving AI/SMTP credentials fails in production** → that's the fail-closed
180+
check; set a real `SETTINGS_KEY`, don't downgrade `NODE_ENV`.

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111
strategy:
1212
matrix:
13-
pkg: [api, web, scraper]
13+
pkg: [api, web]
1414
steps:
1515
- uses: actions/checkout@v4
1616
- uses: actions/setup-node@v4

Dockerfile

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
# syntax=docker/dockerfile:1
2-
# One image holds: the built internal web app, the API that serves it, and the
3-
# scraper. Build context is the crm/ directory.
2+
# One image holds the built internal web app and the API that serves it.
43

54
# ---- Stage 1: build the internal web app (Vite) ----
65
FROM node:24-alpine AS web
@@ -15,15 +14,12 @@ RUN npm run build
1514
FROM node:24-alpine AS runtime
1615
WORKDIR /app
1716

18-
# Install deps (cached unless the lockfiles change).
17+
# Install deps (cached unless the lockfile changes).
1918
COPY api/package.json api/package-lock.json ./api/
2019
RUN cd api && npm ci
21-
COPY scraper/package.json scraper/package-lock.json ./scraper/
22-
RUN cd scraper && npm ci
2320

2421
# App source.
2522
COPY api/ ./api/
26-
COPY scraper/ ./scraper/
2723

2824
# Built web app, served by the API in production.
2925
COPY --from=web /web/dist ./web/dist
@@ -39,5 +35,4 @@ EXPOSE 8787
3935
# Liveness: the API exposes an unauthenticated /api/health.
4036
HEALTHCHECK --interval=30s --timeout=4s --start-period=20s --retries=3 \
4137
CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||8787)+'/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
42-
# Default command runs the API. The scraper service overrides this.
4338
CMD ["npm", "start"]

PROGRESS.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,33 @@ A running log of what's landed, so picking the work back up is easy. Newest firs
44

55
## Latest
66

7+
- **Rewrite: reorganised, hardened, scraper removed, tailored to isarwebsites.**
8+
**Structure:** the 1,500-line `api/src/index.ts` is now a slim composition root;
9+
every endpoint moved into `api/src/routes/*` (one module per domain) with shared
10+
`middleware.ts` (auth gates, proxy-aware client IP) and `helpers.ts` (one upload
11+
validator instead of three copies, CSV/inline-file responses). **Security:**
12+
sessions are now server-side DB rows (SHA-256 token hashes only) — logout,
13+
password resets and user deletion revoke them for real, and `SESSION_SECRET` is
14+
gone entirely; added `hono/csrf` origin checks on mutating requests,
15+
`secure-headers` (+ CSP in prod), request-size limits (2 MB JSON / 12 MB uploads /
16+
200 MB restore), login rate-limit keyed by real client IP (`TRUST_PROXY`), audited
17+
`login.success`/`login.failed` with IP, and `/api/settings` is admin-only in both
18+
directions. **Efficiency:** `GET /api/documents` no longer runs 2 queries per
19+
document (bulk items + payments buckets), and document reads use an explicit
20+
column list so signed-doc BLOBs never ride along in lists. **Removed:** the whole
21+
`scraper/` workspace, `/api/scraper/*`, `scrape.ts`, the scraper settings/secret
22+
columns (dropped by migration), the Scraper tab/view/types, `SERVICE_TOKEN`
23+
(the xlsx CLI now writes via `insertLead` directly), the stale `mcp/` workspace
24+
(targeted the removed `/api/v1`), and `docs/INTEGRATIONS.md` (documented removed
25+
subsystems). **isarwebsites:** fresh installs seed an 11-item Leistungskatalog
26+
(Website Starter/Business/Premium, Relaunch, Hosting & Pflege, SEO, Google
27+
Business Profil, …) + default business name; copilot/analyst/outreach prompts
28+
now sell websites for isarwebsites; UI rebranded (nav, login, title); README/
29+
ROADMAP/DEPLOY/setup-skill rewritten. **125 API tests green**, api + web
30+
typecheck clean, web builds; verified live in the browser (login, tabs, seeded
31+
catalog under Einstellungen, draft invoice, dashboards) and via curl (401 gating,
32+
form-CSRF 403, security headers, logout revocation).
33+
734
- **Streamline: cut Zeiterfassung + Customer-360, unify document saving.** Trimmed
835
back toward a focused tool rather than a swiss-army knife. **Removed** the
936
Zeiterfassung module (table, `timetracking.ts`, `/api/time*`, AI time tools,

0 commit comments

Comments
 (0)