# Deploy memory — rubis

Ce repo contient **deux déploiements distincts** sur la même infra K3s,
namespace `rubis` :

1. **Landing** statique (`rubis.pro`) — image nginx-alpine.
2. **App SaaS** (`app.rubis.pro`) — image AdonisJS + React (V1).

Chacun a sa propre image, son propre Dockerfile, son propre workflow CI.

---

## 1. Landing (`rubis.pro`)

### Infra
- Deployment : `rubis` · Container : `rubis` · NodePort : `30109`
- Image : `git.arthurbarre.fr/ordinarthur/rubis`
- Domaine : https://rubis.pro (sous-domaine **temporaire** —
  domaine définitif `rubis.pro` pas encore acheté)
- Manifests : `k3s/{namespace,deployment,service}.yml`
- Route Traefik : `~/dev/perso/proxmox/ansible/roles/traefik/templates/rubis.yml.j2`
- Workflow CI : `.gitea/workflows/deploy-landing.yml`
- Source : `landing/` (servi par nginx)

### Mise à jour
Push git, le CI build + rollout auto (filter sur `landing/**`, `Dockerfile`,
`k3s/{namespace,deployment,service}.yml`).

---

## 2. App SaaS (`app.rubis.pro`)

**Architecture en 2 services** : nginx en frontal (web) + API Node interne.

```
Traefik :443 → app.rubis.pro:30110 → rubis-web (nginx)
                                                    ├─ /        → SPA static (try_files)
                                                    └─ /api/*   → rubis-api (ClusterIP :3333)
```

### rubis-web — frontend (NodePort 30110, exposé)
- Image : `git.arthurbarre.fr/ordinarthur/rubis-web`
- Container : `web` (nginx-alpine + SPA Vite dist)
- Sert /assets/* (cache 1y immutable), / (SPA fallback try_files)
- Reverse-proxy /api/* → rubis-api.rubis.svc.cluster.local:3333
- Manifest : `k3s/app/web.yml`
- Workflow CI : `.gitea/workflows/deploy-web.yml`
- Source : `apps/web`, `packages/shared` + `apps/web/nginx.conf`

### rubis-api — backend (ClusterIP, interne uniquement)
- Image : `git.arthurbarre.fr/ordinarthur/rubis-api`
- Container : `api` (Node 22 + AdonisJS V7)
- Workers BullMQ dans le même process (cf. `start/queue.ts`)
- Init container `migrate` : `node ace.js migration:run --force` depuis
  `/app/apps/api/build` (idempotent)
- Probes K3s sur `/api/v1/health`
- Manifest : `k3s/app/api.yml` (Deployment + Service ClusterIP + ConfigMap)
- Workflow CI : `.gitea/workflows/deploy-api.yml`
- Source : `apps/api`, `packages/shared`

### rubis-redis — backend BullMQ + cache (ClusterIP)
- Image : `redis:7.4-alpine`
- PVC 1Gi local-path, AOF on, maxmemory 256mb allkeys-lru
- Manifest : `k3s/app/redis.yml`
- Re-déployé par le workflow API (path filter inclut `redis.yml`)

### Infra commune
- Domaine : https://app.rubis.pro (DNS A 5413305619)
- Route Traefik : `~/dev/perso/proxmox/ansible/roles/traefik/templates/rubis-app.yml.j2`

### Dépendances externes (déjà déployées)
- **Postgres** : 10.10.10.3:5432, base `rubis_prod`, user `rubis`
- **MinIO** : `minio.minio.svc.cluster.local:9000`, bucket
  `rubis-prod-invoices`, creds = root MinIO
- **Redis** : interne au namespace `rubis`, pas de dépendance externe

### Secrets K3s (`rubis-app-secrets`)
Posés une fois manuellement via `kubectl create secret generic` (jamais
dans les manifests). Re-pose si rotation ou ajout :
```bash
kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
  -n rubis create secret generic rubis-app-secrets \
  --from-literal=APP_KEY=... \
  --from-literal=PG_PASSWORD=... \
  --from-literal=S3_ACCESS_KEY=... \
  --from-literal=S3_SECRET_KEY=... \
  --from-literal=RESEND_API_KEY=... \
  --from-literal=MISTRAL_API_KEY=... \
  --from-literal=REDIS_PASSWORD="" \
  --from-literal=GOOGLE_CLIENT_ID=... \
  --from-literal=GOOGLE_CLIENT_SECRET=... \
  --from-literal=MICROSOFT_CLIENT_ID=... \
  --from-literal=MICROSOFT_CLIENT_SECRET=... \
  --from-literal=STRIPE_SECRET_KEY=... \
  --from-literal=STRIPE_WEBHOOK_SECRET=... \
  --from-literal=SENTRY_DSN_API=... \
  --from-literal=POWENS_CLIENT_ID=... \
  --from-literal=POWENS_CLIENT_SECRET=... \
  --from-literal=POWENS_WEBHOOK_SECRET=... \
  --dry-run=client -o yaml | kubectl apply -f -
```

> ⚠️ Si tu rotates UN secret, repose la commande complète avec **toutes**
> les vars (sinon `apply` supprime celles omises). Garde la liste à jour
> dans un coffre-fort perso (1Password, Bitwarden…).
>
> **Alternative pour ajouter UNE/quelques clés sans toucher au reste** :
> ```bash
> kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
>   -n rubis patch secret rubis-app-secrets --type=strategic --patch '{
>   "stringData": { "NOUVELLE_VAR": "valeur" }
> }'
> ```
> `stringData` accepte du clair — Kubernetes encode en base64 automatiquement.

### Google SSO — setup Google Cloud Console
Si la clé OAuth est perdue ou qu'on doit la régénérer :
1. https://console.cloud.google.com/apis/credentials → projet courant
2. **Create Credentials** → **OAuth client ID** → type **Web application**
3. **Authorized JavaScript origins** :
   - `https://app.rubis.pro`
   - `http://localhost:5173` (dev SPA)
4. **Authorized redirect URIs** :
   - `https://app.rubis.pro/api/v1/auth/google/callback`
   - `http://localhost:3333/api/v1/auth/google/callback` (dev API)
5. Copier `Client ID` + `Client secret` → mettre dans `apps/api/.env` (dev)
   et `rubis-app-secrets` (prod, snippet ci-dessus).

L'écran de consentement OAuth doit être configuré au moins en mode "Testing"
avec l'email du user courant ajouté en testeur. Pour la prod (n'importe qui
peut se connecter), il faut passer en "In production" (vérification Google
si scopes sensibles ; les nôtres `userinfo.email` + `userinfo.profile` sont
non-sensibles, validation auto).

### Microsoft SSO — setup Azure / Entra ID
1. https://portal.azure.com → **Microsoft Entra ID** → **App registrations**
   → **New registration**
2. **Name** : `Rubis Sur l'Ongle` ; **Supported account types** :
   - "Accounts in any organizational directory and personal Microsoft accounts"
     (tenant=common, recommandé)
   - ou "Accounts in any organizational directory" (tenant=organizations,
     M365 strict)
3. **Redirect URI** type **Web** :
   - `https://app.rubis.pro/api/v1/auth/microsoft/callback`
4. Après création : ajouter en plus le redirect dev via **Authentication →
   Add a platform → Web** :
   - `http://localhost:3333/api/v1/auth/microsoft/callback`
5. **Certificates & secrets → New client secret** : créer, copier la
   *Value* (visible une seule fois) → `MICROSOFT_CLIENT_SECRET`
6. Page **Overview** → copier *Application (client) ID* → `MICROSOFT_CLIENT_ID`
7. **API permissions** : `User.Read` (déjà délégué par défaut), pas besoin
   d'admin consent pour les comptes individuels
8. Mettre les valeurs dans `apps/api/.env` (dev) et `rubis-app-secrets` (prod).

Le client secret expire (Azure force 6 ou 12 mois max) — penser à le
renouveler avant échéance ; sinon les nouvelles connexions échoueront
en silence après expiration.

### Banking (Powens) — activation prod

La feature banking est **déployée mais désactivée par défaut** en prod via
le flag `BANKING_ENABLED: 'false'` dans le ConfigMap `rubis-api-config`.
La section banque dans /parametres reste invisible, et `/api/v1/banking/*`
renvoie 503 `banking_disabled`. Procédure d'activation une fois le KYC
Powens prod validé :

1. **Powens** — créer un domaine prod chez Powens (KYC requis : Kbis,
   contrat AISP/DSP2). Récupérer :
   - Slug du domaine (ex : `rubis` → URL `rubis.biapi.pro`).
   - `client_id` + `client_secret` prod (différents du sandbox).
   - Webhook secret (à générer dans la console Powens prod).

2. **Whitelist côté console Powens prod** :
   - Allowed redirect URIs :
     `https://app.rubis.pro/api/v1/banking/powens/callback`
   - Webhook URL :
     `https://app.rubis.pro/api/v1/webhooks/powens`

3. **Mettre à jour les secrets K3s** (re-pose le snippet
   `kubectl create secret generic` ci-dessus en remplissant les 3 vars
   `POWENS_CLIENT_ID` / `POWENS_CLIENT_SECRET` / `POWENS_WEBHOOK_SECRET`
   avec les valeurs prod).

4. **Mettre à jour le ConfigMap** dans `k3s/app/api.yml` si le slug
   diffère de `rubis` (changer `POWENS_DOMAIN` et `POWENS_API_BASE_URL`).
   Puis `BANKING_ENABLED: 'true'`. Commit + push → CI redéploie.

5. **Smoke test prod** : se connecter avec un compte Pro/Business, aller
   sur `/parametres` → la section "Connecter votre banque" doit
   apparaître. Cliquer "Connecter une banque" → la webview Powens prod
   doit s'ouvrir. Tester avec un vrai compte bancaire perso d'abord
   pour valider toute la chaîne (sync + reconcile + emails).

6. **Backup avant flip** : la 1re connexion crée un user Powens prod et
   stocke `powens_user_id` + token chiffré (clé `APP_KEY`) sur l'org.
   Si on doit un jour faire rotate `APP_KEY`, prévoir une migration des
   tokens (déchiffrer avec ancienne clé → re-chiffrer avec nouvelle).

> ℹ️ Le webhook Powens (`POST /api/v1/webhooks/powens`) attend la
> signature HMAC SHA-256 dans le header `BI-Signature` (ou
> `X-Webhook-Signature`). Vérification automatique dans le controller
> avec `POWENS_WEBHOOK_SECRET`. Si Powens reçoit autre chose qu'un 200
> en réponse il retry agressivement → garder un œil sur les logs au
> démarrage.

### Mise à jour
Push git → un (ou les deux) workflow(s) CI se déclenchent selon les paths
modifiés. Build+rollout indépendants.

| Path modifié | Workflow déclenché |
|---|---|
| `apps/web/**`, `Dockerfile.web`, `k3s/app/web.yml` | deploy-web.yml |
| `apps/api/**`, `Dockerfile.api`, `k3s/app/api.yml`, `k3s/app/redis.yml` | deploy-api.yml |
| `packages/shared/**`, `pnpm-lock.yaml`, `tsconfig.base.json`, … | les deux |

### Particularités du Dockerfile.api
- `node ace build` plante avec `ERR_UNKNOWN_FILE_EXTENSION` car
  `@poppinss/ts-exec` ne s'enregistre pas à temps avant l'import de
  `bin/console.ts`. **Solution** : `pnpm exec tsx ace.js build
  --ignore-ts-errors` (tsx = esbuild, gère .ts nativement).
- `--ignore-ts-errors` car `tests/bootstrap.ts` référence un type généré
  tardivement (`.adonisjs/client/registry/schema.d.ts`). Le typecheck
  strict est exécuté en CI séparée (`pnpm typecheck`).

### Particularités du Dockerfile.web
- Vite build appelé directement (`pnpm exec vite build`) au lieu du
  script `tsc -b && vite build` qui plante sans cache `.tsbuildinfo` à
  cause de @tanstack/router-core.
- nginx.conf inclut le upstream `rubis-api.rubis.svc.cluster.local:3333`.
  Si on renomme le service API, c'est ici qu'il faut mettre à jour
  (en plus du manifest).

---

## Voie manuelle (debug, hors CI)

```bash
TAG=$(git rev-parse --short HEAD)
# Landing (rubis.pro)
docker build --platform linux/amd64 -t git.arthurbarre.fr/ordinarthur/rubis:$TAG .
docker push git.arthurbarre.fr/ordinarthur/rubis:$TAG
kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
  -n rubis set image deploy/rubis rubis=git.arthurbarre.fr/ordinarthur/rubis:$TAG

# App — web (frontend)
docker build --platform linux/amd64 -f Dockerfile.web \
  -t git.arthurbarre.fr/ordinarthur/rubis-web:$TAG .
docker push git.arthurbarre.fr/ordinarthur/rubis-web:$TAG
kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
  -n rubis set image deploy/rubis-web web=git.arthurbarre.fr/ordinarthur/rubis-web:$TAG

# App — api (backend)
docker build --platform linux/amd64 -f Dockerfile.api \
  -t git.arthurbarre.fr/ordinarthur/rubis-api:$TAG .
docker push git.arthurbarre.fr/ordinarthur/rubis-api:$TAG
kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
  -n rubis set image deploy/rubis-api api=git.arthurbarre.fr/ordinarthur/rubis-api:$TAG \
  && kubectl --kubeconfig ~/dev/perso/proxmox/k3s/kubeconfig.yaml \
       -n rubis patch deploy/rubis-api --type=json \
       -p="[{\"op\":\"replace\",\"path\":\"/spec/template/spec/initContainers/0/image\",\"value\":\"git.arthurbarre.fr/ordinarthur/rubis-api:$TAG\"}]"
```

⚠️ Cross-compile ARM Mac → linux/amd64 plante sur `@swc/core` au build de
l'app. Préférer pousser et laisser le CI Gitea (linux/amd64 natif).

Si changement route/domaine : `cd ~/dev/perso/proxmox/ansible && ansible-playbook playbooks/gateway.yml`. Penser à `sudo systemctl restart traefik`
sur la gateway si un nouveau domaine ne récupère pas son cert ACME tout
seul (le hot-reload de la dynamic config ne déclenche pas toujours le
challenge LE).

---

## Domaine actuel — `rubis.pro` ✅ migré (2026-05-07)

- **Landing** : https://rubis.pro
- **App SaaS** : https://app.rubis.pro
- **Compat legacy** : `rubis.arthurbarre.fr` / `app.rubis.arthurbarre.fr` → 301 vers `rubis.pro` / `app.rubis.pro` (Traefik dynamic config dans `~/dev/perso/proxmox/ansible/roles/traefik/templates/rubis*.yml.j2`)

### Email rubis.pro

| Flux | Provider | Setup | Adresses |
|---|---|---|---|
| **Sortant** (transactionnel) | Resend | DKIM + SPF + DMARC sur `send.rubis.pro` (verified dashboard Resend) | `relances@rubis.pro` (via `MAIL_FROM_ADDRESS` secret K3s) |
| **Entrant** (humain) | OVH MX Plan (gratuit) | MX `@` → OVH (auto-configuré via Espace Client) | `contact@rubis.pro`, `dev@rubis.pro` |

⚠️ Resend Inbound (toggle "Enable Receiving") doit rester **OFF** : il veut le MX `@`, qui est pris par OVH MX Plan.

### Décommissionnement progressif `arthurbarre.fr` (à faire dans 30-90 jours)

Quand confiance acquise et plus aucune référence vivante :
1. Vire les blocs `rubis-legacy-redirect` / `rubis-app-legacy-redirect` (et middlewares) dans `rubis*.yml.j2`
2. `ansible-playbook playbooks/gateway.yml`
3. Supprime A records `rubis` (id 5413044152) + `app.rubis` (id 5413305619) dans la zone DNS OVH d'`arthurbarre.fr`

---

## Déjà fait — NE PAS refaire
- Dockerfile (landing) + Dockerfile.web + Dockerfile.api (app split)
- nginx.conf (apps/web/nginx.conf) avec upstream rubis-api
- Manifests `k3s/` (landing) + `k3s/app/{api,web,redis}.yml`
- Routes Traefik `rubis.yml.j2` + `rubis-app.yml.j2`
- DNS OVH : A records `rubis` (id 5413044152) + `app.rubis` (id 5413305619)
- Repo Gitea + secrets CI (`KUBECONFIG`, `REGISTRY_PASSWORD`)
- Namespace + secret registry K3s (`gitea-registry`)
- Postgres : base `rubis_prod` + user `rubis` (10.10.10.3)
- MinIO : bucket `rubis-prod-invoices`
- Secret K3s `rubis-app-secrets` (APP_KEY, DB pwd, MinIO, Resend, Mistral,
  Google/Microsoft SSO, Stripe ; Powens à poser au moment de l'activation
  KYC)
- ConfigMap `rubis-api-config` (env non-sensibles incl. banking flags
  désactivés par défaut)

Les prochains `/deploy` font uniquement build + rollout via push git.
