diff --git a/.env.development b/.env.development index fad89d7..b930b72 100644 --- a/.env.development +++ b/.env.development @@ -25,8 +25,8 @@ NEXT_PUBLIC_BRAND_OPERATOR_LOGO_ORIENTATION=horizontal TEMPLATES_TMPDIR="./templates_tmp" TEMPLATES_ADMINS="lilian.sagetlethias,julien.bouquillon" TEMPLATES_GIT_URL="https://github.com/incubateur-ademe/legal-site-templates-test" -TEMPLATES_GIT_GPG_PRIVATE_KEY="" -TEMPLATES_GIT_GPG_PASSPHRASE="" +TEMPLATES_GIT_GPG_PRIVATE_KEY_BASE64= +TEMPLATES_GIT_GPG_PUBLIC_KEY_BASE64= TEMPLATES_GIT_COMMITTER_EMAIL="bot@email.com" TEMPLATES_GIT_COMMITTER_NAME="Bot" TEMPLATES_GIT_MAIN_BRANCH="main" @@ -44,6 +44,7 @@ MAILER_FROM_EMAIL="Pages Légales Faciles " ## Security SECURITY_JWT_SECRET="sikretfordevonly" +SECURITY_WEBHOOK_SECRET=="sikretfordevonly" ## Redis REDIS_BASE="pages-legales-faciles" diff --git a/.env.production b/.env.production index 22b979a..27db1a2 100644 --- a/.env.production +++ b/.env.production @@ -30,7 +30,8 @@ ESPACE_MEMBRE_API_KEY= # TEMPLATES_TMPDIR="./templates_tmp" # TEMPLATES_ADMINS="lilian.sagetlethias,julien.bouquillon" TEMPLATES_GIT_URL= -# TEMPLATES_GIT_GPG_PRIVATE_KEY="" +# TEMPLATES_GIT_GPG_PRIVATE_KEY_BASE64="" +# TEMPLATES_GIT_GPG_PUBLIC_KEY_BASE64="" # TEMPLATES_GIT_GPG_PASSPHRASE="" TEMPLATES_GIT_COMMITTER_EMAIL="bot@email.com" TEMPLATES_GIT_COMMITTER_NAME="Bot" @@ -49,6 +50,7 @@ MAILER_SMTP_SSL=false ## Security SECURITY_JWT_SECRET= +SECURITY_WEBHOOK_SECRET= ## Redis (url or host/port/password) # REDIS_BASE="pages-legales-faciles" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 11209c2..8d5108a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: Build on: push: - branches: [main] + branches: [main, dev] pull_request: - branches: [main] + branches: [dev] concurrency: cancel-in-progress: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index cea7551..7316a0c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -13,10 +13,10 @@ name: "CodeQL" on: push: - branches: ["main"] + branches: [main, dev] pull_request: # The branches below must be a subset of the branches above - branches: ["main"] + branches: [dev] schedule: - cron: "16 1 * * 5" diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5ab69f3..5d9342d 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,9 +2,9 @@ name: Lint on: push: - branches: [main] + branches: [main, dev] pull_request: - branches: [main] + branches: [dev] concurrency: cancel-in-progress: true diff --git a/.github/workflows/teardown-review-db.yml b/.github/workflows/teardown-review-db.yml index f9ed5aa..4428bf3 100644 --- a/.github/workflows/teardown-review-db.yml +++ b/.github/workflows/teardown-review-db.yml @@ -2,6 +2,7 @@ name: Teardown review db on PR close on: pull_request: + branches: [dev] types: - closed diff --git a/Procfile b/Procfile index 2be5f80..0f18b03 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -web: yarn start +web: ./scripts/import_gpg.sh && yarn start diff --git a/TODO.md b/TODO.md index aae2aa5..115710e 100644 --- a/TODO.md +++ b/TODO.md @@ -30,7 +30,7 @@ - [ ] possibilité de recover un fichier supprimé (si fonction de suppression) - [ ] possibilité de rollback à une version précédente (ajout d'une version rollback comme nouvelle version) - [x] anticiper le fait d'avoir plusieurs mentions legales si plusieurs produit (d'ou le "[variableId]"), sinon "default" par défaut -- [ ] GPG pour les commits +- [x] GPG pour les commits - [x] teardown "db" review app/pr avec github actions ## Navigation diff --git a/cron.json b/cron.json new file mode 100644 index 0000000..2d766e0 --- /dev/null +++ b/cron.json @@ -0,0 +1,7 @@ +{ + "jobs": [ + { + "command": "0 0 * * * ./scripts/call_gpg_refresh_webhook.sh" + } + ] +} \ No newline at end of file diff --git a/db/seed/templates/default/mentions-legales.md b/db/seed/templates/default/mentions-legales.md index 542793a..bcf7ee6 100644 --- a/db/seed/templates/default/mentions-legales.md +++ b/db/seed/templates/default/mentions-legales.md @@ -12,36 +12,37 @@ variables: nom_hebergeur: Nom de l'hébergeur adresse_herbergeur: Adresse de l'hébergeur --- -# Mentions légales de {{nom_produit}} +# Mentions légales de {{ nom_produit }} ## Editeur de la Plateforme -La Plateforme **{{nom_produit}}** est éditée par {{nom_editeur}} situé : -
- {{ adresse_editeur }} +La Plateforme **{{ nom_produit }}** est éditée par {{ nom_editeur }} situé : +
+ {{ adresse_editeur }}
- {{ telephone_editeur }} + {{ telephone_editeur }}
{{ email_editeur }}
## Directeur de la publication -{{directeur_publication}} +{{ directeur_publication }} ## Hébergement de la Plateforme -Ce site est hébergé en propre par {{ nom_hebergeur}} : -
-
{{ adresse_herbergeur }}
+Ce site est hébergé en propre par {{ nom_hebergeur }} : + +
{{ adresse_herbergeur }}
## Accessibilité -La conformité aux normes d’accessibilité numérique est un objectif ultérieur mais nous tâchons de rendre ce site accessible à toutes et à tous. +La conformité aux normes d'accessibilité numérique est un objectif ultérieur mais nous tâchons de rendre ce site accessible à toutes et à tous. ### Signaler un dysfonctionnement -Si vous rencontrez un défaut d’accessibilité vous empêchant d’accéder à un contenu ou une fonctionnalité du site, merci de nous en faire part. -Si vous n’obtenez pas de réponse rapide de notre part, vous êtes en droit de faire parvenir vos doléances ou une demande de saisine au Défenseur des droits. +Si vous rencontrez un défaut d'accessibilité vous empêchant d'accéder à un contenu ou une fonctionnalité du site, merci de nous en faire part. +Si vous n'obtenez pas de réponse rapide de notre part, vous êtes en droit de faire parvenir vos doléances ou une demande de saisine au Défenseur des droits. ### En savoir plus -Pour en savoir plus sur la politique d’accessibilité numérique de l’État : http://references.modernisation.gouv.fr/accessibilite-numerique +Pour en savoir plus sur la politique d'accessibilité numérique de l'État : [http://references.modernisation.gouv.fr/accessibilite-numerique](http://references.modernisation.gouv.fr/accessibilite-numerique) ## Sécurité Le site est protégé par un certificat électronique, matérialisé pour la grande majorité des navigateurs par un cadenas. Cette protection participe à la confidentialité des échanges. -En aucun cas les services associés à la plateforme ne seront à l’origine d’envoi de courriels pour demander la saisie d’informations personnelles. +En aucun cas les services associés à la plateforme ne seront à l'origine d'envoi de courriels pour demander la saisie d'informations personnelles. + diff --git a/env.d.ts b/env.d.ts index 0f61a76..e3d7ac6 100644 --- a/env.d.ts +++ b/env.d.ts @@ -107,12 +107,12 @@ Française` * No dist value. * {@link [Local Env Dist](.env.development)} */ - TEMPLATES_GIT_GPG_PRIVATE_KEY?: string; + TEMPLATES_GIT_GPG_PRIVATE_KEY_BASE64?: string; /** * No dist value. * {@link [Local Env Dist](.env.development)} */ - TEMPLATES_GIT_GPG_PASSPHRASE?: string; + TEMPLATES_GIT_GPG_PUBLIC_KEY_BASE64?: string; /** * Dist: `bot@email.com` * {@link [Local Env Dist](.env.development)} @@ -178,6 +178,11 @@ Française` * {@link [Local Env Dist](.env.development)} */ SECURITY_JWT_SECRET?: string; + /** + * Dist: `="sikretfordevonly"` + * {@link [Local Env Dist](.env.development)} + */ + SECURITY_WEBHOOK_SECRET?: string; /** * Dist: `pages-legales-faciles` * {@link [Local Env Dist](.env.development)} @@ -291,8 +296,8 @@ declare type ProcessEnvCustomKeys = | 'TEMPLATES_TMPDIR' | 'TEMPLATES_ADMINS' | 'TEMPLATES_GIT_URL' - | 'TEMPLATES_GIT_GPG_PRIVATE_KEY' - | 'TEMPLATES_GIT_GPG_PASSPHRASE' + | 'TEMPLATES_GIT_GPG_PRIVATE_KEY_BASE64' + | 'TEMPLATES_GIT_GPG_PUBLIC_KEY_BASE64' | 'TEMPLATES_GIT_COMMITTER_EMAIL' | 'TEMPLATES_GIT_COMMITTER_NAME' | 'TEMPLATES_GIT_MAIN_BRANCH' @@ -306,6 +311,7 @@ declare type ProcessEnvCustomKeys = | 'MAILER_SMTP_SSL' | 'MAILER_FROM_EMAIL' | 'SECURITY_JWT_SECRET' + | 'SECURITY_WEBHOOK_SECRET' | 'REDIS_BASE' | 'REDIS_HOST' | 'REDIS_PORT' diff --git a/next-sitemap.config.js b/next-sitemap.config.js index 5617eb5..39de90d 100644 --- a/next-sitemap.config.js +++ b/next-sitemap.config.js @@ -9,7 +9,7 @@ const priorities = { /** @type {import('next-sitemap').IConfig} */ const config = { generateRobotsTxt: false, - siteUrl: isDeployment ? `https://${process.env.NEXT_PUBLIC_SITE_URL}` : "http://localhost:3000", + siteUrl: isDeployment ? `${process.env.NEXT_PUBLIC_SITE_URL}` : "http://localhost:3000", changefreq: "weekly", transform: async (config, path) => { return { diff --git a/next.config.mjs b/next.config.mjs index f0ad971..fd0b2f2 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -27,7 +27,8 @@ const csp = { "'self'", "'unsafe-inline'", process.env.NEXT_PUBLIC_MATOMO_URL, - process.env.NODE_ENV === "development" && "'unsafe-eval' http://localhost", + "'unsafe-eval'", + process.env.NODE_ENV === "development" && "http://localhost", ], "style-src": ["'self'", "'unsafe-inline'"], "object-src": ["'self'", "data:"], diff --git a/scalingo.json b/scalingo.json index 7cf9510..80d3161 100644 --- a/scalingo.json +++ b/scalingo.json @@ -16,41 +16,36 @@ } ], "scripts": { - "first-deploy": "NODE_ENV=development yarn install --frozen-lockfile && NODE_ENV=production yarn seed && rm -rf node_modules" + "first-deploy": "./scripts/import_gpg.sh && NODE_ENV=development yarn install --frozen-lockfile && NODE_ENV=production yarn seed && rm -rf node_modules" }, "env": { "APP_ENV": { - "description": "État applicatif custom. Valeurs possibles : 'review', 'staging', 'dev', ou 'prod'", "value": "review" }, "MAINTENANCE_MODE": { - "description": "Affiche ou non une page de maintenance au lieu d'une navigation normale.", "value": "false" }, "NODE_ENV": { - "description": "Node environment", "value": "production" }, "NEXT_PUBLIC_BRAND_NAME": { - "description": "Nom de la marque", "generator": "template", "template": "%APP%" }, "TEMPLATES_GIT_MAIN_BRANCH": { - "description": "Nom de la branche principale du dépôt git contenant les templates", "generator": "template", "template": "%APP%" }, "TEMPLATES_GIT_URL": { - "description": "URL du dépôt git contenant les templates", "value": "https://github.com/incubateur-ademe/pages-legales-faciles-db-staging" }, "SECURITY_JWT_SECRET": { - "description": "Secret pour la génération des JWT", + "generator": "secret" + }, + "SECURITY_WEBHOOK_SECRET": { "generator": "secret" }, "REDIS_URL": { - "description": "URL du serveur Redis", "value": "$SCALINGO_REDIS_URL" }, "AUTH_URL": { @@ -61,11 +56,9 @@ "value": "1" }, "NEXT_PUBLIC_REPOSITORY_URL": { - "description": "URL du dépôt git du projet", "value": "https://github.com/incubateur-ademe/pages-legales-faciles" }, "NEXT_PUBLIC_SITE_URL": { - "description": "URL du site", "generator": "url" } } diff --git a/scripts/call_gpg_refresh_webhook.sh b/scripts/call_gpg_refresh_webhook.sh new file mode 100755 index 0000000..171ebe5 --- /dev/null +++ b/scripts/call_gpg_refresh_webhook.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +if [ -n "$TEMPLATES_GIT_GPG_PASSPHRASE" ]; then + # URL de l'API et clé API + API_URL="$NEXT_PUBLIC_SITE_URL/api/webhook/gpg/refresh" + API_KEY="$SECURITY_WEBHOOK_SECRET" + + # Exécuter la requête avec curl + response=$(curl -s -X GET "$API_URL" -H "x-api-key: $API_KEY") + + # Vérifier si la requête a réussi + if echo "$response" | grep -q '"ok":true'; then + echo "Succès : $(echo "$response" | jq -r '.message')" + else + echo "Erreur : $(echo "$response" | jq -r '.error')" + fi +else + echo "$(date): No TEMPLATES_GIT_GPG_PASSPHRASE provided. Skipping refresh." >&2 + exit 1 +fi diff --git a/scripts/import_gpg.sh b/scripts/import_gpg.sh new file mode 100755 index 0000000..1a003f7 --- /dev/null +++ b/scripts/import_gpg.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +# Vérifier si les clés publiques et privées sont définies +if [ -z "$TEMPLATES_GIT_GPG_PRIVATE_KEY_BASE64" ] || [ -z "$TEMPLATES_GIT_GPG_PUBLIC_KEY_BASE64" ]; then + echo "Clés GPG non définies. Signature des commits désactivée." + exit 0 +fi + +echo "Les clés GPG sont présentes dans les variables d'environnement. Importation en cours..." + +# Importer la clé privée +echo -n "$TEMPLATES_GIT_GPG_PRIVATE_KEY_BASE64" | base64 --decode | gpg --batch --import +if [ $? -ne 0 ]; then + echo "Erreur : Échec de l'importation de la clé privée." + exit 1 +fi + +# Importer la clé publique +echo -n "$TEMPLATES_GIT_GPG_PUBLIC_KEY_BASE64" | base64 --decode | gpg --batch --import +if [ $? -ne 0 ]; then + echo "Erreur : Échec de l'importation de la clé publique." + exit 1 +fi + +echo "Clés GPG importées avec succès." + +TEMPLATE_GIT_GPG_SIGNINKEY="$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)" + +# Configurer gpg-agent pour le mode non interactif +echo "allow-loopback-pinentry" >> ~/.gnupg/gpg-agent.conf +echo "default-cache-ttl 115200 " >> ~/.gnupg/gpg-agent.conf # 32 heures (en secondes) +echo "max-cache-ttl 115200" >> ~/.gnupg/gpg-agent.conf +gpgconf --kill gpg-agent +gpgconf --launch gpg-agent + +# Injecter la passphrase dans le cache si nécessaire +if [ -n "$TEMPLATES_GIT_GPG_PASSPHRASE" ]; then + gpg --batch --yes --pinentry-mode loopback --default-key "$TEMPLATE_GIT_GPG_SIGNINKEY" --passphrase "$TEMPLATES_GIT_GPG_PASSPHRASE" --sign <<< "refresh-cache" + if [ $? -eq 0 ]; then + echo "Passphrase injectée avec succès dans le cache." + else + echo "Erreur : Échec de l'injection de la passphrase dans le cache." + exit 1 + fi +fi + +# Configurer Git pour utiliser la clé GPG +git config --global user.signingkey "$TEMPLATE_GIT_GPG_SIGNINKEY" +git config --global commit.gpgSign true +git config --global gpg.program gpg + +echo "Configuration GPG terminée." diff --git a/scripts/refresh_gpg_passphrase.sh b/scripts/refresh_gpg_passphrase.sh new file mode 100755 index 0000000..e4309ea --- /dev/null +++ b/scripts/refresh_gpg_passphrase.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +TEMPLATE_GIT_GPG_SIGNINKEY="$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)" +# Vérifie et rafraîchit le cache GPG +if [ -n "$TEMPLATES_GIT_GPG_PASSPHRASE" ]; then + gpg --batch --yes --pinentry-mode loopback --default-key "$TEMPLATE_GIT_GPG_SIGNINKEY" --passphrase "$TEMPLATES_GIT_GPG_PASSPHRASE" --sign <<< "refresh-cache" > /dev/null 2>&1 + if [ $? -eq 0 ]; then + echo "$(date): GPG cache refreshed successfully for 32 hours." + else + echo "$(date): Failed to refresh GPG cache." >&2 + exit 1 + fi +else + echo "$(date): No TEMPLATES_GIT_GPG_PASSPHRASE provided. Skipping refresh." >&2 + exit 1 +fi diff --git a/src/app/(api)/api/webhook/gpg/refresh/route.ts b/src/app/(api)/api/webhook/gpg/refresh/route.ts new file mode 100644 index 0000000..449f9ac --- /dev/null +++ b/src/app/(api)/api/webhook/gpg/refresh/route.ts @@ -0,0 +1,50 @@ +import { execFile } from "child_process"; +import { StatusCodes } from "http-status-codes"; +import { unauthorized } from "next/navigation"; +import { type NextRequest } from "next/server"; +import { promisify } from "util"; + +import { config } from "@/config"; + +const execFileAsync = promisify(execFile); + +export const GET = async (req: NextRequest) => { + const headerSecret = req.headers.get("x-api-key"); + if (headerSecret !== config.security.webhook.secret) { + unauthorized(); + } + + try { + const { stderr, stdout } = await execFileAsync("./scripts/refresh_gpg_passphrase.sh"); + if (stderr) { + console.error(`[gpg-refresh] ❌ ${stderr}`); + return Response.json( + { + ok: false, + error: stderr, + }, + { + status: StatusCodes.INTERNAL_SERVER_ERROR, + }, + ); + } + + console.log(`[gpg-refresh] ✅ ${stdout}`); + return Response.json({ + ok: true, + message: stdout, + }); + } catch (error) { + const message = (error as Error).message; + console.error(`[gpg-refresh] ❌ ${message}`); + return Response.json( + { + ok: false, + error: message, + }, + { + status: StatusCodes.INTERNAL_SERVER_ERROR, + }, + ); + } +}; diff --git a/src/config.ts b/src/config.ts index a9b50f5..2b60e3d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -50,6 +50,9 @@ export const config = { auth: { secret: ensureApiEnvVar(process.env.SECURITY_JWT_SECRET, "secret"), }, + webhook: { + secret: ensureApiEnvVar(process.env.SECURITY_WEBHOOK_SECRET, "secret"), + }, }, espaceMembre: { apiKey: ensureApiEnvVar(process.env.ESPACE_MEMBRE_API_KEY, ""), @@ -78,16 +81,6 @@ export const config = { process.env.TEMPLATES_GIT_URL, "https://github.com/incubateur-ademe/legal-site-templates-test", ), - // gpgPrivateKey: ensureApiEnvVar(process.env.TEMPLATES_GIT_GPG_PRIVATE_KEY, ""), - get gpgPrivateKey() { - console.warn("TEMPLATES_GIT_GPG_PRIVATE_KEY is not supported yet"); - return ""; - }, - // gpgPassphrase: ensureApiEnvVar(process.env.TEMPLATES_GIT_GPG_PASSPHRASE, ""), - get gpgPassphrase() { - console.warn("TEMPLATES_GIT_GPG_PASSPHRASE is not supported yet"); - return ""; - }, committer: { email: ensureApiEnvVar(process.env.TEMPLATES_GIT_COMMITTER_EMAIL, "bot@email.com"), name: ensureApiEnvVar(process.env.TEMPLATES_GIT_COMMITTER_NAME, "Bot"), diff --git a/src/lib/repo/impl/SimpleGitRepo.ts b/src/lib/repo/impl/SimpleGitRepo.ts index 7b23ffe..c707adf 100644 --- a/src/lib/repo/impl/SimpleGitRepo.ts +++ b/src/lib/repo/impl/SimpleGitRepo.ts @@ -18,7 +18,6 @@ import { CONFIG_EXT, GROUP_FILE, type IGitRepo, TEMPLATE_DIR, TEMPLATE_EXT, VARI export class SimpleGitRepo implements IGitRepo { private readonly git: SimpleGit; - // private readonly remote: "local" | "origin" = config.templates.git.provider === "local" ? "local" : "origin"; private readonly remote = "origin"; private configDone = false; private readonly tmpdir = path.resolve(config.templates.tmpdir); @@ -48,16 +47,6 @@ export class SimpleGitRepo implements IGitRepo { .addConfig("user.name", config.templates.git.committer.name) .addConfig("pull.rebase", "false"); - if (config.templates.git.gpgPrivateKey) { - await this.git - .addConfig("user.signingkey", config.templates.git.gpgPrivateKey) - .addConfig("commit.gpgSign", "true"); - } - - // const remotes = await this.git.getRemotes(); - // if (!remotes.some(r => r.name === this.remote)) { - // await this.git.addRemote(this.remote, this.getAuthRemoteUrl()); - // } await this.git.removeRemote(this.remote).addRemote(this.remote, this.getAuthRemoteUrl()); this.configDone = true; @@ -66,9 +55,8 @@ export class SimpleGitRepo implements IGitRepo { if (withPull) { await this.git.fetch(this.remote, ["-p"]); await this.git.checkout(config.templates.git.mainBranch); - await this.git.pull(this.remote, config.templates.git.mainBranch, { - // "--set-upstream-to": `${this.remote}/${config.templates.git.mainBranch}`, - }); + await this.git.branch({ "--set-upstream-to": `${this.remote}/${config.templates.git.mainBranch}` }); + await this.git.pull(this.remote, config.templates.git.mainBranch); } } @@ -375,7 +363,8 @@ export class SimpleGitRepo implements IGitRepo { await this.init(false); const branches = await this.git.branch(); if (branches.all.includes(config.templates.git.mainBranch)) { - throw new UnexpectedError(`Branch ${config.templates.git.mainBranch} already exists. Aborting seeding.`); + console.info(`Branch ${config.templates.git.mainBranch} already exists. Aborting seeding.`); + return; } console.info("↳ Creating branch", config.templates.git.mainBranch);