feat(api): implement caching and logging in health check endpoint (#526)

* feat(api): implement caching and logging in health check endpoint

- Added in-memory caching for health check responses to reduce load on Postgres.
- Introduced logging for error handling in health check.
- Updated response structure to exclude error details from public responses.

feat(api): enhance OAuth consent UI and scope handling

- Improved consent UI to reflect exact requested scopes and added better user guidance.
- Updated scope handling logic to ensure least-privilege access.
- Enhanced styling for better user experience and accessibility.

chore(docker): improve security and resource management in Docker setup

- Updated Docker Compose configuration to enforce read-only file systems and resource limits.
- Added health checks and logging options for better observability.
- Introduced optional Caddy reverse proxy for TLS termination.

fix(migrations): resolve ambiguity in create_company_with_owner function

- Dropped orphaned 3-arg overload of create_company_with_owner function.
- Recreated canonical 4-arg version with cash account seeding logic.
- Ensured proper permissions for function execution in Postgres.

* feat: enhance security checks for team membership in company creation
This commit is contained in:
Mattsson
2026-05-19 17:52:11 +02:00
committed by GitHub
parent 8a6ce7093e
commit 9aced4790c
15 changed files with 1155 additions and 142 deletions
+84 -17
View File
@@ -44,13 +44,19 @@ Open `.env` and fill in the **required** values:
| `NEXT_PUBLIC_APP_URL` | The URL where you'll access gnubok (e.g. `https://gnubok.example.com`) |
| `CRON_SECRET` | Any random string — `openssl rand -hex 32` works |
Once `.env` is filled in, **restrict its permissions** so other users on the host can't read your service-role key or cron secret:
```bash
chmod 600 .env
```
### 3. Start
```bash
docker compose up -d
```
That's it. The app is now running at `http://localhost:3000` (or whatever port you set with `PORT`).
The app is now reachable on **loopback only** at `http://127.0.0.1:3000`. This is intentional — direct internet exposure over HTTP is not safe for an accounting app. The next section enables HTTPS.
### 4. Verify
@@ -61,6 +67,42 @@ curl http://localhost:3000/api/health
---
## Enable HTTPS (recommended)
Ship a Caddy reverse proxy alongside the app — it auto-provisions Let's Encrypt certificates and renews them forever.
### 1. Point a domain at the host
`gnubok.example.com → <your-public-ip>` (A record). Ports 80 and 443 must be reachable from the internet (Let's Encrypt's HTTP-01 challenge uses port 80).
### 2. Set `DOMAIN` in `.env`
```env
DOMAIN=gnubok.example.com
NEXT_PUBLIC_APP_URL=https://gnubok.example.com
```
### 3. Download the overlay + Caddyfile
```bash
curl -fsSLO https://raw.githubusercontent.com/gnubok/gnubok/main/docker-compose.caddy.yml
mkdir -p docker
curl -fsSL -o docker/Caddyfile \
https://raw.githubusercontent.com/gnubok/gnubok/main/docker/Caddyfile
```
### 4. Start with the overlay
```bash
docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -d
```
Caddy obtains a cert on first boot (takes ~10 s). Visit `https://gnubok.example.com`.
If you already have nginx / a managed load balancer / Cloudflare in front, skip Caddy and point your existing proxy at `127.0.0.1:3000` — set `NEXT_PUBLIC_APP_URL` to match the public URL.
---
## Optional Extensions
The self-hosted image ships with all extensions enabled (except Enable Banking, which requires private PSD2 credentials). Each extension activates when you provide its env vars — without them, the app works normally and the feature is simply unavailable.
@@ -98,12 +140,27 @@ No env vars needed — always available.
## Updating
```bash
docker compose pull # pulls latest app image from GHCR
docker compose up -d # recreates containers if image changed
The default `IMAGE_TAG=latest` follows `main` and updates on every `docker compose pull`. For production, **pin to a specific release** so updates are deliberate:
```env
# .env
IMAGE_TAG=1.2.3
```
The `latest` tag always points to the newest build from `main`. The cron sidecar is a small Alpine image built locally — it updates automatically on `up` if you re-download `docker/cron.Dockerfile`.
Browse available tags at https://github.com/erp-mafia/gnubok/pkgs/container/gnubok. For maximum integrity, pin by digest:
```env
IMAGE_TAG=1.2.3@sha256:abcdef...
```
Apply updates:
```bash
docker compose pull
docker compose up -d
```
The cron sidecar is a small Alpine image built locally — it rebuilds automatically on `up --build` if you re-download `docker/cron.Dockerfile`. Base-image digests (node, alpine, caddy) are pinned in source; [Dependabot](.github/dependabot.yml) opens PRs weekly when upstream ships security updates.
---
@@ -137,34 +194,44 @@ The cron container waits for the app's healthcheck to pass before starting. It c
### How NEXT_PUBLIC_* injection works
The Docker image is built with placeholder values (e.g. `__NEXT_PUBLIC_SUPABASE_URL__`) baked into the JavaScript bundles. When the container starts, `docker-entrypoint.sh` replaces those placeholders with your actual env vars via `sed`. This means the same image works for any Supabase project — no rebuilding needed.
The image is built with placeholder values (e.g. `__NEXT_PUBLIC_SUPABASE_URL__`) baked into the JavaScript bundles. At container start, `docker-entrypoint.sh` runs as `root`, `sed`-substitutes the placeholders with your runtime env vars, then runs `chmod -R a-w /app/.next/static` and drops privileges with `su-exec nextjs:nodejs` before exec'ing Node. The served JS bundle is owned by `root` and read-only by the time the application starts — a runtime RCE in the Node process cannot rewrite what other users will receive.
---
## Ports
The app listens on port 3000 inside the container. To map it to a different host port:
The app listens on port 3000 inside the container. The base compose binds it to `127.0.0.1:3000` on the host — change `PORT` in `.env` to remap. To expose on all interfaces (only do this if you're putting your own reverse proxy in front), override the port binding in a local `docker-compose.override.yml`:
```env
PORT=8080
```yaml
services:
app:
ports: !override
- "${PORT:-3000}:3000"
```
Then access at `http://localhost:8080`.
---
## Reverse Proxy
For production, put the app behind a reverse proxy (nginx, Caddy, Traefik) that handles TLS. Example with Caddy:
The preferred path is the bundled Caddy overlay — see [Enable HTTPS](#enable-https-recommended). If you already run nginx, Traefik, or sit behind Cloudflare, leave the app on `127.0.0.1:3000` and point your existing proxy at it. Set `NEXT_PUBLIC_APP_URL` to the public URL.
```
gnubok.example.com {
reverse_proxy localhost:3000
Example nginx upstream:
```nginx
server {
server_name gnubok.example.com;
listen 443 ssl http2;
# ssl_certificate / ssl_certificate_key / etc.
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
Make sure `NEXT_PUBLIC_APP_URL` matches the public URL (e.g. `https://gnubok.example.com`).
---
## Troubleshooting