# reactbin
_Organize your reaction images._

![Reactbin UI](.img/reactbin-ui.png)

A self-hosted reaction image board. Single owner account, tag-based browsing, S3-compatible image storage.

---

## Local development

```bash
cp .env.example .env
# Edit .env — defaults work out of the box for local dev
docker compose up
```

- UI: http://localhost:4200
- API: http://localhost:8000
- MinIO console: http://localhost:9001 (minioadmin / minioadmin)

The API serves on port 8000 directly in dev. In production the nginx ingress routes `/api/` there.

### Running tests

```bash
make test-unit          # pytest unit tests (no Docker)
make test-integration   # builds api-test image, runs full suite against Postgres + MinIO
```

### Production image builds

```bash
make build-prod         # builds reactbin-api-prod:latest from api/Dockerfile.prod
make verify-prod        # smoke-tests the production image
make build-ui-prod      # builds reactbin-ui-prod:latest from ui/Dockerfile.prod
make verify-ui-prod     # smoke-tests the production UI image
```

---

## Production deployment (k3s)

### Cluster prerequisites

- nginx ingress controller
- cert-manager with a `letsencrypt-prod` ClusterIssuer
- Vault Secrets Operator (VSO) installed and connected to Vault
- Vault KV v2 secrets populated (see below)

### Vault secrets

Two KV v2 paths. VSO syncs these into Kubernetes Secrets automatically.

**`reactbin/api/config`** → K8s Secret `api-env`

| Key | Notes |
|-----|-------|
| `DATABASE_URL` | `postgresql+asyncpg://user:pass@host:5432/db` |
| `JWT_SECRET_KEY` | Long random string — `openssl rand -base64 48` |
| `OWNER_USERNAME` | Login username |
| `OWNER_PASSWORD` | Login password |
| `S3_ENDPOINT_URL` | `http://minio.reactbin.svc.cluster.local:9000` |
| `S3_BUCKET_NAME` | `reactbin` |
| `S3_ACCESS_KEY_ID` | Same value as `MINIO_ROOT_USER` |
| `S3_SECRET_ACCESS_KEY` | Same value as `MINIO_ROOT_PASSWORD` |
| `API_BASE_URL` | `https://<your-domain>` |
| `LOGIN_TRUSTED_PROXY_IPS` | Pod CIDR of nginx ingress pods, e.g. `10.42.0.0/16` — needed for per-client login rate limiting behind the ingress |

**`reactbin/minio/credentials`** → K8s Secret `minio-credentials`

| Key | Notes |
|-----|-------|
| `MINIO_ROOT_USER` | MinIO admin username |
| `MINIO_ROOT_PASSWORD` | `openssl rand -base64 32` |

### Apply order

```bash
# 1. Namespace first
kubectl apply -f k8s/namespace.yaml

# 2. Vault CRDs — wait for VSO to create api-env and minio-credentials Secrets
kubectl apply -f k8s/vault/
kubectl get secret -n reactbin api-env minio-credentials   # wait until both appear

# 3. API, UI, Ingress — replace 'latest' tags and <your-domain> first
kubectl apply -f k8s/api/ -f k8s/ui/ -f k8s/ingress.yaml
kubectl rollout status deployment/api -n reactbin          # Alembic init container runs here

# 4. MinIO — wait for StatefulSet ready before running the bucket init Job
kubectl apply -f k8s/minio/service.yaml -f k8s/minio/statefulset.yaml
kubectl rollout status statefulset/minio -n reactbin
kubectl apply -f k8s/minio/init-job.yaml
```

Before applying: substitute real image tags in the Deployment manifests and replace `<your-domain>` in `k8s/ingress.yaml`.

### Updating a secret

1. Update the value in Vault
2. Force VSO to sync immediately (otherwise waits up to 1 hour):
   ```bash
   kubectl annotate vaultstaticsecret api-secret -n reactbin \
     secrets.hashicorp.com/force-sync=$(date +%s) --overwrite
   ```
3. Restart the deployment to pick up the new Secret:
   ```bash
   kubectl rollout restart deployment/api -n reactbin
   ```

### Validating manifests

```bash
make validate-k8s   # yamllint + kubectl apply --dry-run=client (requires kubeconfig)
```

---

## Environment variables reference

All variables are read at startup from environment / `.env`.

| Variable | Default | Notes |
|----------|---------|-------|
| `DATABASE_URL` | — | Async DSN: `postgresql+asyncpg://...` |
| `JWT_SECRET_KEY` | — | Required; use a long random string in production |
| `JWT_EXPIRY_SECONDS` | `86400` | Token lifetime (24 h) |
| `OWNER_USERNAME` | — | Single owner account username |
| `OWNER_PASSWORD` | — | Single owner account password |
| `S3_ENDPOINT_URL` | — | MinIO or any S3-compatible endpoint |
| `S3_BUCKET_NAME` | `reactbin` | |
| `S3_ACCESS_KEY_ID` | — | |
| `S3_SECRET_ACCESS_KEY` | — | |
| `S3_REGION` | `us-east-1` | |
| `MAX_UPLOAD_BYTES` | `52428800` | 50 MiB |
| `API_BASE_URL` | — | Used for generating public URLs |
| `API_DOCS_ENABLED` | `true` | Set to `false` in production |
| `LOGIN_MAX_FAILURES` | `5` | Failed attempts before cooldown |
| `LOGIN_WINDOW_SECONDS` | `300` | Sliding window for failure count |
| `LOGIN_COOLDOWN_SECONDS` | `900` | Lock duration after threshold hit |
| `LOGIN_TRUSTED_PROXY_IPS` | `""` | Comma-separated CIDRs of trusted upstream proxies |
