Backend
- Custom Ally driver Microsoft (Oauth2Driver) — Microsoft n'est pas dans
les providers built-in, mais le driver dérive de Oauth2Driver en quelques
lignes. Endpoints v2.0 (Microsoft Identity Platform), Graph /me pour le
profil, fallback userPrincipalName si mail null (comptes perso).
- Tenant configurable via MICROSOFT_TENANT (défaut 'common' — accepte
work/school + perso ; 'organizations' pour M365 strict).
- Migration 1400 : ajout microsoft_id nullable unique sur users.
- AuthMicrosoftController : redirect + callback (même pattern que Google).
- Refacto : extraction d'un service sso_session.ts (findOrCreateUserFromSso,
nextRouteAfterSso, emitSsoSessionAndRedirect) → AuthGoogle + AuthMicrosoft
partagent la logique.
- Routes /api/v1/auth/microsoft/{redirect,callback}.
Frontend
- Composant SsoButton générique (provider="google"|"microsoft") avec logo
officiel inline pour chaque. Remplace l'ancien GoogleButton.
- Login + signup : pile verticale "Continuer avec Google" + "Continuer
avec Microsoft", puis séparateur "ou", puis form email/password.
- Route SPA renommée /auth/google/complete → /auth/sso/complete (partagée
entre les deux providers, la callback API redirige toujours dessus).
- Erreurs SSO sur /login : ?google=... ET ?microsoft=... → toast contextuel.
K3s
- ConfigMap rubis-api-config : ajout MICROSOFT_TENANT + MICROSOFT_CALLBACK_URL.
- Secret rubis-app-secrets : ajout MICROSOFT_CLIENT_ID + MICROSOFT_CLIENT_SECRET.
Doc
- .claude/deploy-memory.md : procédure Azure / Entra ID app registration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
230 lines
10 KiB
Markdown
230 lines
10 KiB
Markdown
# Deploy memory — rubis
|
|
|
|
Ce repo contient **deux déploiements distincts** sur la même infra K3s,
|
|
namespace `rubis` :
|
|
|
|
1. **Landing** statique (`rubis.arthurbarre.fr`) — image nginx-alpine.
|
|
2. **App SaaS** (`app.rubis.arthurbarre.fr`) — image AdonisJS + React (V1).
|
|
|
|
Chacun a sa propre image, son propre Dockerfile, son propre workflow CI.
|
|
|
|
---
|
|
|
|
## 1. Landing (`rubis.arthurbarre.fr`)
|
|
|
|
### Infra
|
|
- Deployment : `rubis` · Container : `rubis` · NodePort : `30109`
|
|
- Image : `git.arthurbarre.fr/ordinarthur/rubis`
|
|
- Domaine : https://rubis.arthurbarre.fr (sous-domaine **temporaire** —
|
|
domaine définitif `rubis-sur-l-ongle.fr` 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.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.arthurbarre.fr`)
|
|
|
|
**Architecture en 2 services** : nginx en frontal (web) + API Node interne.
|
|
|
|
```
|
|
Traefik :443 → app.rubis.arthurbarre.fr: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.arthurbarre.fr (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=... \
|
|
--dry-run=client -o yaml | kubectl apply -f -
|
|
```
|
|
|
|
### 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.arthurbarre.fr`
|
|
- `http://localhost:5173` (dev SPA)
|
|
4. **Authorized redirect URIs** :
|
|
- `https://app.rubis.arthurbarre.fr/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.arthurbarre.fr/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.
|
|
|
|
### 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.arthurbarre.fr)
|
|
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).
|
|
|
|
---
|
|
|
|
## Quand le domaine définitif sera acheté (`rubis-sur-l-ongle.fr`)
|
|
|
|
1. Records DNS chez OVH : A `@`, A `app`, MX/SPF/DKIM pour Resend
|
|
2. Modifier les rules dans `rubis.yml.j2` et `rubis-app.yml.j2` (Host)
|
|
3. Replay `gateway.yml`
|
|
4. Maj `MAIL_FROM_ADDRESS=relances@rubis-sur-l-ongle.fr` dans le secret
|
|
5. (Optionnel) supprimer les A records `rubis.arthurbarre.fr` et
|
|
`app.rubis.arthurbarre.fr` chez OVH
|
|
|
|
---
|
|
|
|
## 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)
|
|
- ConfigMap `rubis-api-config` (env non-sensibles)
|
|
|
|
Les prochains `/deploy` font uniquement build + rollout via push git.
|