Files
formations-app/docs/superpowers/plans/2026-06-11-sp4-certs-domain.md
T
2026-06-11 01:21:10 +02:00

7.7 KiB
Raw Blame History

SP4 — Certificates + Domain Swap Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Moodle-generated certificates emailed automatically on course completion, celebrated in the app; then the app takes moodle.saillant.cc and Moodle moves to lms-admin.saillant.cc.

Stack/Repo: unchanged. Branch feat/sp4-certs-domain off main. Runner as before. Secrets only in env files (Tower /home/clems/formations-app.env; SMTP creds read from ES /home/clems/electron-rare-site/mail-api.env at provisioning time, stored in Moodle config — never in git).


Task 1: Moodle certificates provisioning

Files: Create ops/provision-certs.php; update ops/README.md; host crontab on Tower.

  • 1.1 ops/provision-certs.php (CLI, idempotent, style of provision-ws.php). For each course id>1:
    • find the customcert cm + the 4 quiz cmids (modules join, like export script);
    • set course_modules.availability on the customcert cm to {"op":"&","c":[{"type":"completion","cm":<quizcmid>,"e":1},...×4],"showc":[true,true,true,true]} (e:1 = COMPLETE). Use $DB->set_field + rebuild_course_cache($courseid, true).
    • set customcert.emailstudents=1 ($DB->set_field('customcert','emailstudents',1,...)). Then SMTP config from env vars (script reads SMTP_HOST/SMTP_PORT/SMTP_USER/SMTP_PASS/SMTP_FROM passed via docker exec -e): set_config('smtphosts', host.':'.port); set_config('smtpsecure','tls'); set_config('smtpuser',...); set_config('smtppass',...); set_config('noreplyaddress', SMTP_FROM); set_config('allowedemaildomains','');. Print summary.
  • 1.2 Run on Tower (SMTP values from ES sudo grep -E 'SMTP_|ICLOUD' /home/clems/electron-rare-site/mail-api.env — From = the iCloud account address, NOT contact@lelectronrare.fr; the 550 trap).
  • 1.3 Cron: on Tower crontab -l + add idempotently: */2 * * * * docker exec moodle php /var/www/html/admin/cli/cron.php >/dev/null 2>&1. Run once manually; expect task list output incl. email_certificate_task scheduled (it ships enabled by default — verify in mdl_task_scheduled, enable if disabled).
  • 1.4 PROOF: pick test user = wsreader (id 3, enrolled): temporarily set its email to a real mailbox? NO — instead: override the 4 quiz completions for wsreader on course 5 (freertos) via the existing MOODLE_SYNC_TOKEN + core_completion_override_activity_completion_status (cmids from QuizBank rows), run cron, then check SELECT * FROM mdl_customcert_issues WHERE userid=3 (row = issuance works) and the cron output mentions the email task (send will fail or go to wsreader@saillant.cc — irrelevant, issuance row is the proof; report the email task log line). REVERT the 4 overrides (newstate=0) and DELETE the test issue row.
  • 1.5 README section (what the script sets, SMTP source, cron line, rollback notes) + commit feat: certificate issuing provisioning.

Task 2: App certificate page + hub CTA (TDD on the predicate)

Files: Create src/lib/completion.ts + test, src/pages/cours/[slug]/certificat.astro; modify hub page.

  • 2.1 src/lib/completion.ts: export function courseComplete(modules: {chapters:{...}[]}[], reads: ReadMark[], quizzes: Quiz[], attempts: QuizAttempt[]): {complete: boolean; chaptersRead: number; chaptersTotal: number; quizzesPassed: number; quizzesTotal: number} — pure; complete = every module fully read (isBookComplete) AND every quiz passed (best attempt isPassed). Tests (≥4): complete, missing chapters, missing quiz, empty.
  • 2.2 /cours/<slug>/certificat: published slug else 404-rewrite; anon → redirect connexion. Load modules/reads/quizzes/attempts (try/catch → 503 friendly). If complete: dark celebration page — big copper nucleus glow (pure CSS, no three.js), « Félicitations ! », course title, X chapitres · 4 quiz réussis, panel « Votre certificat officiel est envoyé par email à <user.email> (vérifiez vos indésirables). Généré par notre plateforme de formation. », contact link mailto:contact@lelectronrare.fr, « Retour au cours ». If incomplete: progress view (chapters n/N, quiz m/4, links to first missing chapter + first unpassed quiz).
  • 2.3 Hub: when user && complete → copper card/CTA « 🎓 Certificat disponible → /cours//certificat » under the progress lines; when user && !complete → muted line « Certificat : termine les chapitres et les 4 quiz ». Reuse courseComplete (single computation with already-loaded data).
  • 2.4 Runner green → commit feat: certificate celebration page.

Task 3: Domain swap

Files (repo): modify src/middleware.ts (formations→moodle 301 host redirect). Infra: traefik dynamic files on ES, CF ingress, Moodle config.php (Tower volume), Tower env file, Keycloak client, lelectronrare.fr links.

  • 3.1 middleware.ts: at top of onRequest — if (ctx.url.hostname === 'formations.saillant.cc') return ctx.redirect('https://moodle.saillant.cc' + ctx.url.pathname + ctx.url.search, 301);. Commit feat: formations host 301 to moodle domain.
  • 3.2 Keycloak: add redirect URI https://moodle.saillant.cc/auth/callback + webOrigin to client formations-app (kcadm update, KEEP existing URIs).
  • 3.3 lms-admin.saillant.cc: traefik lms-admin.yml on ES (→ http://100.78.6.122:8095, websecure letsencrypt) + CF tunnel ingress (recipe). Verify 200 via --resolve BEFORE wwwroot change (Moodle may redirect to wwwroot — a 3xx to moodle.saillant.cc is fine at this stage).
  • 3.4 Moodle wwwroot: on Tower edit config.php inside volume (docker exec moodle sed -i "s|https://moodle.saillant.cc|https://lms-admin.saillant.cc|" /var/www/html/config.php) + purge caches (php admin/cli/purge_caches.php). Verify https://lms-admin.saillant.cc 200 + login page assets load from lms-admin.
  • 3.5 App env on Tower: MOODLE_BASE_URL=https://lms-admin.saillant.cc, APP_BASE_URL=https://moodle.saillant.cc (sed in /home/clems/formations-app.env). NOTE: MOODLE_WS_TOKEN/MOODLE_SYNC_TOKEN remain valid (tokens are host-independent).
  • 3.6 Swap moodle.saillant.cc: edit ES traefik moodle.yml service url → http://100.78.6.122:8096. formations.yml unchanged (still → 8096; the app's middleware now 301s that host). Pull+rebuild app on Tower (gets middleware + env).
  • 3.7 lelectronrare.fr: replace formations.saillant.ccmoodle.saillant.cc in the 3 link files (src/pages/formations.astro, src/components/Formations.astro, src/components/Footer.astro), commit, push gitea+origin, rebuild image (--network=host) + recreate container on ES (compose chez clems).

Task 4: E2E

  • App on moodle.saillant.cc: / 200 catalogue; /cours/kicad-makers 200 with data-hub; chapter 200; /connexion 302 Keycloak; legacy /course/view.php?id=3 301 → /cours/kicad-makers.
  • lms-admin.saillant.cc: 200, Moodle login page; WS sanity: hub renders (BFF green via lms-admin); login/token.php reachable.
  • formations.saillant.cc/* → 301 moodle.saillant.cc same path.
  • certificat page: anon → 302; (logged user manual: incomplete → progress view).
  • lelectronrare.fr/formations links → moodle.saillant.cc (curl grep).
  • Moodle cron ran in last 5 min (mdl_config_plugins lastcron or task log).
  • Merge ff-only → main, push; memories update.

Self-review

  • Order: app-side first, swap last; each infra step has a verify; rollback = revert 2 traefik files + wwwroot sed back.
  • 550 trap: From = iCloud-owned address (noreplyaddress), documented.
  • Availability lockdown closes the "cert downloadable by anyone enrolled" hole found in probe.