diff --git a/.gitignore b/.gitignore index c276d7a..d36b5ac 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,7 @@ migration-data/ # Sauvegardes locales — ne jamais committer de dump de données clients ManagerService/Deployment/backups/ + +# Provisioning : seuls le .example et les scripts sont versionnés +ManagerService/Deployment/provisioning/.env +ManagerService/Deployment/provisioning/jwt.txt diff --git a/ManagerService/Deployment/provisioning/.env.example b/ManagerService/Deployment/provisioning/.env.example new file mode 100644 index 0000000..b846578 --- /dev/null +++ b/ManagerService/Deployment/provisioning/.env.example @@ -0,0 +1,16 @@ +# Copier en .env et remplir. Le .env n'est jamais versionne. +# +# Generer un mot de passe fort (PowerShell) : +# -join ((1..32) | % { (([char[]](48..57+65..90+97..122)) | Get-Random) }) +# +# Ne JAMAIS reutiliser mym/mym : cette valeur a servi sur un Postgres expose +# publiquement entre juin et octobre 2025, qui a encaisse 1 074 465 tentatives +# d'authentification. Voir README.md, section « Ce qui a mal tourne ». + +POSTGRES_USER=myim +POSTGRES_PASSWORD= +POSTGRES_DATABASE=my_info_mate + +# Liaison du port. 127.0.0.1 = joignable seulement depuis l'hote et par tunnel SSH. +# En production, laisser 127.0.0.1. Ne jamais mettre 0.0.0.0 ni "5432:5432". +POSTGRES_BIND=127.0.0.1 diff --git a/ManagerService/Deployment/provisioning/1-bootstrap-superadmin.ps1 b/ManagerService/Deployment/provisioning/1-bootstrap-superadmin.ps1 new file mode 100644 index 0000000..485f41a --- /dev/null +++ b/ManagerService/Deployment/provisioning/1-bootstrap-superadmin.ps1 @@ -0,0 +1,50 @@ +<# + Genere le SQL qui cree le PREMIER compte SuperAdmin. + + Pourquoi ce detour plutot qu'un INSERT direct avec un mot de passe : + les mots de passe sont haches en scrypt (PasswordUtils, 16384 iterations), + impossible a calculer hors .NET. Le token de reinitialisation, lui, est un + simple SHA-256 hexadecimal (PasswordTokenHelper). On laisse donc l'application + faire le hachage scrypt via set-password : le format est garanti correct, et + personne d'autre que toi ne connait le mot de passe. + + MigrationController est [Authorize(Policy = SuperAdmin)] et la migration ne + cree aucun SuperAdmin (Mongo n'a pas de champ Role). Ce compte est donc le + prealable oblige a toute migration de donnees. +#> +param( + [string]$Email, + # L'instance doit EXISTER : TokensService.cs:82 fait Instances.Find(user.InstanceId) + # puis lit .PinCode sans test de nullite -> 500 au login sinon. + [string]$InstanceId = 'sa-bootstrap' +) + +if ([string]::IsNullOrWhiteSpace($Email)) { $Email = Read-Host "Email du compte SuperAdmin" } +$Email = $Email.ToLower() # Authenticate fait login.email.ToLower() + +$token = -join ((1..40) | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 16) }) +$sha = [System.Security.Cryptography.SHA256]::Create() +$hash = ($sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($token)) | + ForEach-Object { $_.ToString('x2') }) -join '' + +Write-Host "" +Write-Host "=== ETAPE A : sur une base VIERGE uniquement, creer l'instance d'echafaudage ===" -ForegroundColor Cyan +Write-Host " (a sauter si tu passes -InstanceId d'une instance deja migree)" +Write-Host @" +insert into "Instances" ("Id","Name","DateCreation") +values ('$InstanceId','Bootstrap SuperAdmin', now()); +"@ + +Write-Host "=== ETAPE B : creer le compte ===" -ForegroundColor Cyan +Write-Host @" +insert into "Users" + ("Id","Email","Password","FirstName","LastName","Token","DateCreation","InstanceId","Role","PasswordTokenHash","PasswordTokenExpiresAt") +values + ('sa-bootstrap-0001','$Email','pending','Super','Admin','',now(),'$InstanceId',0,'$hash', now() + interval '48 hours'); +"@ + +Write-Host "=== ETAPE C : poser le mot de passe (service demarre) ===" -ForegroundColor Cyan +Write-Host " run.cmd 3-set-password.ps1 -Token $token" +Write-Host "" +Write-Host "Le token vaut 48 h et n'est ecrit dans aucun fichier : garde cette fenetre." -ForegroundColor Yellow +Write-Host "N'oublie pas de COMMITER les INSERT : DBeaver n'auto-commite pas toujours." -ForegroundColor Yellow diff --git a/ManagerService/Deployment/provisioning/2-run-service-local.ps1 b/ManagerService/Deployment/provisioning/2-run-service-local.ps1 new file mode 100644 index 0000000..255ec22 --- /dev/null +++ b/ManagerService/Deployment/provisioning/2-run-service-local.ps1 @@ -0,0 +1,55 @@ +<# + Lance manager-service EN LOCAL, branche sur un Mongo source et sur la base + cible (preprod ou prod) a travers un tunnel SSH. + + Pourquoi en local plutot que sur le serveur : la migration ne demande pas de + tester un deploiement, et builder l'image evite d'affronter en meme temps le + Dockerfile de manager-app (Flutter 3.7 = Dart 2.19, alors que le pubspec exige + >= 3.1) et la source NuGet git.dev-espaces-naturels.lu qui repond 401. + + Rien n'est ecrit dans appsettings : les variables d'environnement ASP.NET Core + (double underscore = ':') prennent le dessus. Aucun secret ne part dans le depot. +#> +param( + [string]$MongoUri = "mongodb://localhost:27018", # Mongo SOURCE (dump restaure) + [string]$PgHost = "127.0.0.1", + [int] $PgPort = 5433, # extremite locale du tunnel SSH + [string]$EnvFile = "$PSScriptRoot\.env" +) +$ErrorActionPreference = "Stop" + +if (-not (Test-Path $EnvFile)) { throw "Fichier introuvable : $EnvFile (copier .env.example)" } +$cfg = @{} +Get-Content $EnvFile | Where-Object { $_ -match '^\s*[^#].*=' } | ForEach-Object { + $k,$v = $_ -split '=', 2 ; $cfg[$k.Trim()] = $v.Trim() +} +foreach ($k in 'POSTGRES_USER','POSTGRES_PASSWORD','POSTGRES_DATABASE') { + if ([string]::IsNullOrWhiteSpace($cfg[$k])) { throw "$k absent ou vide dans $EnvFile" } +} + +# Le repo est 3 niveaux au-dessus : Deployment/provisioning -> Deployment -> ManagerService -> repo +$repo = (Resolve-Path "$PSScriptRoot\..\..\..").Path + +Write-Host "Verifications..." -ForegroundColor Cyan +$mongoPort = if ($MongoUri -match ':(\d+)') { [int]$Matches[1] } else { 27017 } +if (-not (Test-NetConnection 127.0.0.1 -Port $mongoPort -InformationLevel Quiet -WarningAction SilentlyContinue)) { + throw "Mongo source injoignable sur $mongoPort. Le conteneur est-il demarre ?" +} +Write-Host " Mongo source $mongoPort : OK" -ForegroundColor Green +if (-not (Test-NetConnection $PgHost -Port $PgPort -InformationLevel Quiet -WarningAction SilentlyContinue)) { + throw "Postgres injoignable sur ${PgHost}:${PgPort}. Le tunnel SSH est-il ouvert ?" +} +Write-Host " Postgres ${PgHost}:${PgPort} : OK" -ForegroundColor Green + +$env:ASPNETCORE_ENVIRONMENT = "Development" +$env:ConnectionStrings__TabletDb = $MongoUri +$env:ConnectionStrings__PostgresConnection = "Host=$PgHost;Port=$PgPort;Database=$($cfg['POSTGRES_DATABASE']);Username=$($cfg['POSTGRES_USER']);Password=$($cfg['POSTGRES_PASSWORD'])" + +Write-Host "" +Write-Host " TabletDb -> $MongoUri" -ForegroundColor Yellow +Write-Host " PostgresConnection -> ${PgHost}:${PgPort}/$($cfg['POSTGRES_DATABASE'])" -ForegroundColor Yellow +Write-Host " Swagger -> http://localhost:5000/swagger" -ForegroundColor Cyan +Write-Host "" + +Set-Location $repo +dotnet run --project ManagerService diff --git a/ManagerService/Deployment/provisioning/3-set-password.ps1 b/ManagerService/Deployment/provisioning/3-set-password.ps1 new file mode 100644 index 0000000..927ba4d --- /dev/null +++ b/ManagerService/Deployment/provisioning/3-set-password.ps1 @@ -0,0 +1,29 @@ +<# + Consomme le token de reinitialisation et pose le mot de passe. + Invoke-RestMethod construit le JSON depuis un objet : aucun probleme de + guillemets (cmd ne connait pas l'apostrophe) ni de caracteres speciaux + (&& est l'operateur d'enchainement de cmd). +#> +param( + [Parameter(Mandatory=$true)][string]$Token, + [string]$Password, + [string]$BaseUrl = "http://localhost:5000" +) +$ErrorActionPreference = "Stop" +. "$PSScriptRoot\_common.ps1" + +if ([string]::IsNullOrWhiteSpace($Password)) { $Password = Read-PlainPassword "Mot de passe a poser (8 car. min)" } +if ($Password.Length -lt 8) { throw "Le code exige 8 caracteres minimum." } + +try { + $r = Invoke-RestMethod -Method Post -Uri "$BaseUrl/api/Authentication/set-password" ` + -ContentType "application/json" ` + -Body (@{ token = $Token; newPassword = $Password } | ConvertTo-Json) + Write-Host " $r" -ForegroundColor Green + Write-Host " Etape suivante : run.cmd 4-get-jwt.ps1 -Email " -ForegroundColor Cyan +} catch { + Write-Host " ECHEC : $(Get-HttpErrorBody $_)" -ForegroundColor Red + Write-Host " Causes usuelles : token deja consomme, expire (48 h)," -ForegroundColor Yellow + Write-Host " ou INSERT non commite dans le client SQL." -ForegroundColor Yellow + exit 1 +} diff --git a/ManagerService/Deployment/provisioning/4-get-jwt.ps1 b/ManagerService/Deployment/provisioning/4-get-jwt.ps1 new file mode 100644 index 0000000..b9a0a07 --- /dev/null +++ b/ManagerService/Deployment/provisioning/4-get-jwt.ps1 @@ -0,0 +1,35 @@ +<# S'authentifie et ecrit le jeton dans jwt.txt (non versionne). #> +param( + [Parameter(Mandatory=$true)][string]$Email, + [string]$Password, + [string]$BaseUrl = "http://localhost:5000" +) +$ErrorActionPreference = "Stop" +. "$PSScriptRoot\_common.ps1" + +if ([string]::IsNullOrWhiteSpace($Password)) { $Password = Read-PlainPassword "Mot de passe" } + +try { + $auth = Invoke-RestMethod -Method Post -Uri "$BaseUrl/api/Authentication/Authenticate" ` + -ContentType "application/json" ` + -Body (@{ email = $Email.ToLower(); password = $Password } | ConvertTo-Json) +} catch { + Write-Host " ECHEC : $(Get-HttpErrorBody $_)" -ForegroundColor Red + Write-Host " 'Object reference not set' = l'instance du compte n'existe pas en base" -ForegroundColor Yellow + Write-Host " (TokensService.cs:82 lit .PinCode sans test de nullite)." -ForegroundColor Yellow + exit 1 +} + +$jwt = $auth.access_token +if (-not $jwt) { Write-Host "Reponse inattendue :"; $auth | ConvertTo-Json -Depth 4; exit 1 } + +Write-Utf8NoBom "$PSScriptRoot\jwt.txt" $jwt +# JsonStringEnumConverter (Startup.cs) serialise les enums en chaine : role vaut +# "SuperAdmin", pas 0. On accepte les deux formes. +$role = "$($auth.role)" +Write-Host " Jeton ecrit dans jwt.txt ($($jwt.Length) caracteres)" -ForegroundColor Green +if ($role -eq "SuperAdmin" -or $role -eq "0") { + Write-Host " Role : $role -- MigrationController acceptera." -ForegroundColor Green +} else { + Write-Host " Role : $role -- il faut SuperAdmin pour migrer." -ForegroundColor Red +} diff --git a/ManagerService/Deployment/provisioning/5-migrate.ps1 b/ManagerService/Deployment/provisioning/5-migrate.ps1 new file mode 100644 index 0000000..7bc8c4b --- /dev/null +++ b/ManagerService/Deployment/provisioning/5-migrate.ps1 @@ -0,0 +1,71 @@ +<# + Joue la migration Mongo -> Postgres. + + Sans -Apply : dry run global (n'ecrit rien). + Avec -Apply : joue pour de vrai, instance par instance, de la plus petite a la + plus grande — c'est ce qui fait qu'un defaut casse sur le plus petit perimetre + possible. Chaque appel est une transaction unique : soit tout, soit rien. + + ⚠️ Un dry run ne peut detecter AUCUNE violation de contrainte : il n'ecrit rien, + donc SaveChanges n'est jamais appele. Un NOT NULL viole ne sort qu'avec -Apply. + C'est arrive le 07/09/2026 (LastName null sur test@email.be). + + ⚠️ Compter 2 a 5 min : la sonde HEAD interroge chaque blob Firebase, 30 a la + fois. Elle tourne AUSSI en dry run. +#> +param( + [switch]$Apply, + [string]$InstanceId, + [string]$BaseUrl = "http://localhost:5000" +) +$ErrorActionPreference = "Stop" +. "$PSScriptRoot\_common.ps1" +$jwt = Get-Jwt $PSScriptRoot +$headers = @{ Authorization = "Bearer $jwt" } + +function Invoke-Migration($dryRun, $instance) { + $uri = "$BaseUrl/api/migration/run?dryRun=$($dryRun.ToString().ToLower())" + if ($instance) { $uri += "&instanceId=$instance" } + $label = if ($dryRun) { "DRY RUN" } else { "REEL" } + if ($instance) { $label += " / $instance" } + Write-Host "=== $label ===" -ForegroundColor Cyan + $t0 = Get-Date + try { + $r = Invoke-RestMethod -Method Post -Uri $uri -Headers $headers -ContentType "application/json" -Body "{}" + } catch { + Write-Host " ECHEC HTTP : $(Get-HttpErrorBody $_)" -ForegroundColor Red + Write-Host " Un 500 signifie que la transaction a ete ANNULEE : la base est intacte." -ForegroundColor Yellow + return $false + } + $sec = [int]((Get-Date) - $t0).TotalSeconds + Write-Host " duree : $sec s" -ForegroundColor Gray + $m = $r.migrated + ($m.PSObject.Properties | Where-Object { $_.Value -is [int] -and $_.Value -gt 0 } | + ForEach-Object { "$($_.Name)=$($_.Value)" }) -join ", " | ForEach-Object { Write-Host " migre : $_" } + Write-Host " erreurs : $($r.errors.Count) | signalees : $($r.skipped.Count)" + $r.errors | Select-Object -First 5 | ForEach-Object { Write-Host " ! $_" -ForegroundColor Yellow } + if ($r.fatalError) { Write-Host " FATAL : $($r.fatalError)" -ForegroundColor Red; return $false } + Write-Host "" + return $true +} + +if (-not $Apply) { + Write-Host "Mode simulation. Ajouter -Apply pour ecrire." -ForegroundColor Yellow + Invoke-Migration $true $InstanceId | Out-Null + exit 0 +} + +if ($InstanceId) { if (Invoke-Migration $false $InstanceId) { exit 0 } else { exit 1 } } + +# ⚠️ L'API ne donne pas le volume par instance : cet ordre est ALPHABETIQUE, +# pas croissant. Or jouer la plus petite d'abord est ce qui fait qu'un defaut +# casse sur le plus petit perimetre — c'est ce qui a permis de trouver le +# LastName null sur 1 utilisateur au lieu de 10, le 07/09/2026. +# Pour maitriser l'ordre, enchainer les appels avec -InstanceId a la main. +Write-Host "Ordre ALPHABETIQUE (l'API ne donne pas les volumes)." -ForegroundColor Yellow +Write-Host "Pour commencer par la plus petite, appeler -InstanceId une par une." -ForegroundColor Yellow +Write-Host "" +$ids = (Invoke-RestMethod -Uri "$BaseUrl/api/Instance" -Headers $headers) | + Sort-Object { $_.name } | Select-Object -ExpandProperty id +foreach ($id in $ids) { if (-not (Invoke-Migration $false $id)) { exit 1 } } +Write-Host "Toutes les instances sont passees." -ForegroundColor Green diff --git a/ManagerService/Deployment/provisioning/README.md b/ManagerService/Deployment/provisioning/README.md new file mode 100644 index 0000000..b1819f3 --- /dev/null +++ b/ManagerService/Deployment/provisioning/README.md @@ -0,0 +1,159 @@ +# Provisionner une base MyInfoMate — préprod ou production + +Le flux complet, du serveur nu à une base Postgres remplie avec les données MongoDB. +Éprouvé le **2026-09-07** sur la préprod `51.77.222.154` : 4 instances, 315 sections, +2383 ressources migrées sans perte. + +> Ces scripts sont en PowerShell mais s'exécutent depuis `cmd` via `run.cmd` — +> dans `cmd`, un `.ps1` n'est pas exécuté, il est passé à l'association de fichier +> et ne fait rien. +> +> ``` +> run.cmd 1-bootstrap-superadmin.ps1 -Email moi@exemple.be +> ``` + +--- + +## 0. Prérequis sur le serveur + +```bash +docker image prune -f # 7,5 Go récupérés sur la préprod : 3 ans de builds +df -h / # il faut ~1 Go pour les images +``` + +## 1. La base + +```bash +cp .env.example .env # puis remplir POSTGRES_PASSWORD +docker-compose -p myim-preprod -f docker-compose.postgres.yml up -d +``` + +**Le `-p` n'est pas optionnel.** Compose est en 1.21.0 (2018) sur ce serveur : il ne +connaît pas la clé `name:`, et sans `-p` il déduit le projet du dossier courant — +lancé depuis `/home/debian`, il réutiliserait le volume `debian_postgres-data`, +c'est-à-dire la base cassée d'octobre 2025. + +Vérifier que le port n'est **pas** exposé : + +```bash +docker port myim_pg # doit afficher 127.0.0.1:5432, jamais 0.0.0.0 +``` + +## 2. Le schéma + +Aucun `Migrate()` au démarrage : les migrations s'appliquent explicitement. +Depuis le poste de dev, à travers un tunnel SSH : + +```bash +ssh -f -N -L 5433:127.0.0.1:5432 -p 55522 user@serveur +``` + +```powershell +$env:MIGRATIONS_CONNECTION = "Host=127.0.0.1;Port=5433;Database=my_info_mate;Username=myim;Password=..." +dotnet ef database update --project ManagerService +``` + +`MIGRATIONS_CONNECTION` est prévue pour ça dans `MyInfoMateDbContextFactory`. +Contrôler ensuite que `postgis` **et** `vector` sont bien installées : + +```sql +select extname, extversion from pg_extension; +``` + +## 3. Le Mongo source + +`MigrationController` lit un MongoDB **vivant**, pas des fichiers. Un export JSON +ne suffit donc pas : il faut le restaurer dans un Mongo jetable. + +```bash +docker run -d --name myim_mongo_src -p 27018:27017 mongo:6 +docker run --rm --network container:myim_mongo_src -v "$PWD:/src:ro" mongo:6 \ + mongorestore --uri mongodb://localhost:27017 --gzip --archive=/src/dump.gz --drop +``` + +⚠️ **Ne jamais pointer sur le Mongo de production.** Les `DatabaseService` exposent +`InsertOne`/`ReplaceOne`/`DeleteOne`, et `dryRun` ne protège que Postgres. + +## 4. Le compte SuperAdmin + +`MigrationController` est `[Authorize(Policy = SuperAdmin)]`, et la migration n'en +crée aucun — MongoDB n'a pas de champ `Role`. C'est donc le préalable obligé. + +``` +run.cmd 1-bootstrap-superadmin.ps1 -Email moi@exemple.be +``` + +Il affiche deux `INSERT` à exécuter dans le client SQL (**penser à commiter**), puis +la commande de l'étape suivante. Les mots de passe étant hachés en scrypt, on laisse +l'application faire le hachage via `set-password` : personne d'autre que toi ne +connaît le mot de passe. + +## 5. Le service, en local + +``` +run.cmd 2-run-service-local.ps1 +``` + +Le brancher en local évite de builder une image, donc d'affronter au même moment le +`Dockerfile` de manager-app (Flutter 3.7 = Dart 2.19, alors que le `pubspec` exige +`>= 3.1`) et la source NuGet `git.dev-espaces-naturels.lu` qui répond 401. + +Puis, dans une autre fenêtre : + +``` +run.cmd 3-set-password.ps1 -Token +run.cmd 4-get-jwt.ps1 -Email moi@exemple.be +``` + +## 6. La migration + +``` +run.cmd 5-migrate.ps1 # simulation +run.cmd 5-migrate.ps1 -Apply -InstanceId # pour de vrai, une instance +``` + +**Commencer par la plus petite instance.** Chaque appel est une transaction unique : +en cas d'échec, rien n'est écrit. + +> ⚠️ **Un dry run ne peut détecter aucune violation de contrainte** : il n'écrit rien, +> donc `SaveChanges` n'est jamais appelé. Un `NOT NULL` violé ne sort qu'au run réel. +> C'est exactement ce qui est arrivé le 07/09/2026 — `LastName` null sur +> `test@email.be` — et commencer par la plus petite instance l'a fait apparaître +> en 3 secondes sur 1 utilisateur au lieu de 10. + +Compter 2 à 5 min : la sonde HEAD interroge chaque blob Firebase, 30 à la fois. +Elle tourne **aussi** en dry run. + +## 7. Le backfill des tailles + +``` +POST /api/Resource/backfill-storage?dryRun=true puis dryRun=false +``` + +La sonde HEAD est **instable sous charge** : une ressource peut arriver à +`SizeBytes = 0` alors que son blob existe. Le 07/09, le backfill a rattrapé une +ressource sur 2383 et confirmé 3 vrais 404. Un `SizeBytes` à 0 n'est donc jamais +une preuve de blob manquant. + +## 8. Nettoyage et vérifications + +```sql +-- rattacher le SuperAdmin à une vraie instance, puis supprimer l'échafaudage +update "Users" set "InstanceId" = '' where "Id" = 'sa-bootstrap-0001'; +delete from "Instances" where "Id" = 'sa-bootstrap'; +``` + +Puis `pg_dump -Fc` (voir `../backup/`), et les vérifications qui ne se voient qu'à +l'œil : login manager, une app visiteur avec sa clé API, un parcours, une carte, +un PDF, un quiz avec ses questions. + +--- + +## Ce qui a mal tourné, et qu'on ne refait pas + +| | | +|---|---| +| **`5432:5432`** sur le Postgres précédent | exposé à l'internet avec `mym`/`mym` du 06/06 au 22/10/2025 : **1 074 465 échecs d'authentification** sur autant de noms distincts. Le compte `mym` n'a jamais été tenté — c'est le seul hasard qui a sauvé la base. D'où le `127.0.0.1` du compose | +| **Aucune limite de log** | un seul conteneur avait atteint 516 Mo sur un disque de 20 Go | +| **Image `postgres:16` nue** | sans PostGIS, la migration `UpdateCoordinatesToGeom` ne passe pas : le schéma était bloqué à 14 migrations sur 68 | +| **`mym`/`mym`** | ne jamais réutiliser ce couple | diff --git a/ManagerService/Deployment/provisioning/_common.ps1 b/ManagerService/Deployment/provisioning/_common.ps1 new file mode 100644 index 0000000..3dfb643 --- /dev/null +++ b/ManagerService/Deployment/provisioning/_common.ps1 @@ -0,0 +1,37 @@ +# Fonctions partagees par les scripts d'appel HTTP. + +# PowerShell 5.1 ne remplit pas ErrorDetails sur une erreur HTTP : sans lire le +# flux de reponse on n'obtient qu'un « 400 Bad Request » opaque, alors que le +# message du serveur est la seule information utile. +function Get-HttpErrorBody($err) { + if ($err.ErrorDetails.Message) { return $err.ErrorDetails.Message } + try { + $resp = $err.Exception.Response + if ($resp) { + $reader = New-Object IO.StreamReader($resp.GetResponseStream()) + $body = $reader.ReadToEnd(); $reader.Close() + if ($body) { return $body } + } + } catch { } + return $err.Exception.Message +} + +function Read-PlainPassword($prompt) { + $secure = Read-Host $prompt -AsSecureString + [Runtime.InteropServices.Marshal]::PtrToStringAuto( + [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure)) +} + +# Set-Content -Encoding utf8 ecrit un BOM en PowerShell 5.1, et un BOM dans un +# en-tete Authorization invalide le jeton. +function Write-Utf8NoBom($path, $text) { + [System.IO.File]::WriteAllText($path, $text, (New-Object System.Text.UTF8Encoding($false))) +} + +function Get-Jwt($dir) { + $p = Join-Path $dir "jwt.txt" + if (-not (Test-Path $p)) { throw "jwt.txt absent. Lancer d'abord 4-get-jwt.ps1" } + # [string] est indispensable : la surcharge Replace(char, char) refuse une + # chaine vide. Sans le cast, on tombe dessus et l'appel echoue. + (Get-Content $p -Raw).Replace([string][char]0xFEFF, '').Trim() +} diff --git a/ManagerService/Deployment/provisioning/docker-compose.postgres.yml b/ManagerService/Deployment/provisioning/docker-compose.postgres.yml new file mode 100644 index 0000000..16a7784 --- /dev/null +++ b/ManagerService/Deployment/provisioning/docker-compose.postgres.yml @@ -0,0 +1,48 @@ +# Base PostgreSQL de MyInfoMate — preprod comme production. +# +# Lancement (toujours avec -p, voir README) : +# docker-compose -p myim-preprod -f docker-compose.postgres.yml up -d +# +# ⚠️ Compose du serveur 51.77.222.154 = 1.21.0 (2018) : format 3.6 maximum, +# et la cle « name: » n'existe pas. D'ou le -p obligatoire. +version: '3.6' + +services: + postgres: + build: + context: .. + dockerfile: Dockerfile.postgres + image: myinfomate/postgres-pgvector:16-3.4 + container_name: ${POSTGRES_CONTAINER:-myim_pg} + environment: + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DATABASE} + # Liaison sur la boucle locale : joignable par tunnel SSH, jamais depuis + # l'internet. C'est la lecon du Postgres precedent, expose par un simple + # "5432:5432" et martele pendant quatre mois et demi. + ports: + - "${POSTGRES_BIND:-127.0.0.1}:5432:5432" + volumes: + - pg-data:/var/lib/postgresql/data + networks: + - myim + restart: unless-stopped + # Aucun conteneur du serveur n'avait de limite : un seul log avait atteint + # 516 Mo sur un disque de 20 Go, et a probablement tue la base. + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + pg-data: + +networks: + myim: diff --git a/ManagerService/Deployment/provisioning/run.cmd b/ManagerService/Deployment/provisioning/run.cmd new file mode 100644 index 0000000..cbf51bb --- /dev/null +++ b/ManagerService/Deployment/provisioning/run.cmd @@ -0,0 +1,10 @@ +@echo off +REM Lance un script .ps1 de ce dossier depuis cmd, ou un .ps1 ne s'execute pas. +REM run.cmd 1-bootstrap-superadmin.ps1 -Email moi@exemple.be +if "%~1"=="" ( + echo Usage : run.cmd ^ [arguments] + echo. + dir /b "%~dp0*.ps1" + exit /b 1 +) +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0%~1" %2 %3 %4 %5 %6 %7 %8 %9