From 7e5f8d824bb9bfd7a8db785be18197c1975fb082 Mon Sep 17 00:00:00 2001 From: MarioGervais <17948611+MarioGervais@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:55:27 -0500 Subject: [PATCH 01/19] Update fr.json feat(i18n): complete French translation - add missing certificate key-type strings --- frontend/src/locale/src/fr.json | 1299 ++++++++++++++++--------------- 1 file changed, 654 insertions(+), 645 deletions(-) diff --git a/frontend/src/locale/src/fr.json b/frontend/src/locale/src/fr.json index c715c028..d8a6de69 100644 --- a/frontend/src/locale/src/fr.json +++ b/frontend/src/locale/src/fr.json @@ -1,647 +1,656 @@ { - "access-list": { - "defaultMessage": "Liste d'accès" - }, - "access-list.access-count": { - "defaultMessage": "{count} {count, plural, one {Règle} other {Règles}}" - }, - "access-list.auth-count": { - "defaultMessage": "{count} {count, plural, one {Utilisateur} other {Utilisateurs}}" - }, - "access-list.help-rules-last": { - "defaultMessage": "S'il existe au moins une règle, cette règle de refuser tout sera ajoutée en dernier." - }, - "access-list.help.rules-order": { - "defaultMessage": "Notez que les directives autoriser et refuser seront appliquées dans l'ordre où elles sont définies." - }, - "access-list.pass-auth": { - "defaultMessage": "Transmettre l'authentification au serveur en amont" - }, - "access-list.public": { - "defaultMessage": "Accessible au public" - }, - "access-list.public.subtitle": { - "defaultMessage": "Aucune authentification de base requise" - }, - "access-list.satisfy-any": { - "defaultMessage": "Valide n'importe quelle règle" - }, - "access-list.subtitle": { - "defaultMessage": "{utilisateurs} {utilisateurs, plural, one {Utilisateur} other {Utilisateurs}}, {règles} {règles, plural, one {Règle} other {Règles}} - Crée : {date}" - }, - "access-lists": { - "defaultMessage": "Listes d'accès" - }, - "action.add": { - "defaultMessage": "Ajouter" - }, - "action.add-location": { - "defaultMessage": "Ajouter localisation" - }, - "action.close": { - "defaultMessage": "Fermer" - }, - "action.delete": { - "defaultMessage": "Supprimer" - }, - "action.disable": { - "defaultMessage": "Désactiver" - }, - "action.download": { - "defaultMessage": "Télécharger" - }, - "action.edit": { - "defaultMessage": "Modifier" - }, - "action.enable": { - "defaultMessage": "Activer" - }, - "action.permissions": { - "defaultMessage": "Permissions" - }, - "action.renew": { - "defaultMessage": "Renouveler" - }, - "action.view-details": { - "defaultMessage": "Voir les Détails" - }, - "auditlogs": { - "defaultMessage": "Journaux d'audit" - }, - "cancel": { - "defaultMessage": "Annuler" - }, - "certificate": { - "defaultMessage": "Certificat" - }, - "certificate.custom-certificate": { - "defaultMessage": "Certificat" - }, - "certificate.custom-certificate-key": { - "defaultMessage": "Clé du Certificat" - }, - "certificate.custom-intermediate": { - "defaultMessage": "Certificat intermédiaire" - }, - "certificate.in-use": { - "defaultMessage": "Utilisé" - }, - "certificate.none.subtitle": { - "defaultMessage": "Aucun certificat assigné" - }, - "certificate.none.subtitle.for-http": { - "defaultMessage": "Cet hôte n'utilisera pas le HTTPS" - }, - "certificate.none.title": { - "defaultMessage": "Aucun" - }, - "certificate.not-in-use": { - "defaultMessage": "Non utilisé" - }, - "certificate.renew": { - "defaultMessage": "Renouveler Certificat" - }, - "certificates": { - "defaultMessage": "Certificats" - }, - "certificates.custom": { - "defaultMessage": "Certificat personnalisé" - }, - "certificates.custom.warning": { - "defaultMessage": "Les fichiers de clé protégés par une passphrase ne sont pas acceptés." - }, - "certificates.dns.credentials": { - "defaultMessage": "Contenu du fichier d'identifiants" - }, - "certificates.dns.credentials-note": { - "defaultMessage": "Ce plugin nécessite un fichier de configuration contenant un jeton d'API ou d'autres informations d'identification pour votre fournisseur." - }, - "certificates.dns.credentials-warning": { - "defaultMessage": "Ces données seront stockées en clair dans la base de données et dans un fichier !" - }, - "certificates.dns.propagation-seconds": { - "defaultMessage": "Propagation Seconds" - }, - "certificates.dns.propagation-seconds-note": { - "defaultMessage": "Laisser vide pour utiliser la valeur par défaut du plugin. Nombre de secondes à attendre pour la propagation DNS." - }, - "certificates.dns.provider": { - "defaultMessage": "Fournisseur DNS" - }, - "certificates.dns.warning": { - "defaultMessage": "Cette section requiert une certaine connaissance de Certbot et de ses plugins DNS. Veuillez consulter la documentation des plugins correspondants." - }, - "certificates.http.reachability-404": { - "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il ne semble pas s'agir de Nginx Proxy Manager. Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." - }, - "certificates.http.reachability-failed-to-check": { - "defaultMessage": "Impossible de vérifier l'accessibilité en raison d'une erreur de communication avec site24x7.com." - }, - "certificates.http.reachability-not-resolved": { - "defaultMessage": "Aucun serveur n'est disponible pour ce domaine. Veuillez vérifier que votre domaine existe et pointe vers l'adresse IP où votre instance NPM est exécutée. Si nécessaire, le port 80 est ouvert dans votre routeur." - }, - "certificates.http.reachability-ok": { - "defaultMessage": "Votre serveur est accessible et la création de certificats devrait être possible." - }, - "certificates.http.reachability-other": { - "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé un code d'état inattendu {code}. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." - }, - "certificates.http.reachability-wrong-data": { - "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé des données inattendues. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." - }, - "certificates.http.test-results": { - "defaultMessage": "Résultats du test" - }, - "certificates.http.warning": { - "defaultMessage": "Ces domaines doivent déjà être configurés pour pointer vers cette installation." - }, - "certificates.request.subtitle": { - "defaultMessage": "avec Let's Encrypt" - }, - "certificates.request.title": { - "defaultMessage": "Demander un nouveau certificat" - }, - "column.access": { - "defaultMessage": "Accès" - }, - "column.authorization": { - "defaultMessage": "Autorisation" - }, - "column.authorizations": { - "defaultMessage": "Autorisations" - }, - "column.custom-locations": { - "defaultMessage": "Emplacement personnalisé" - }, - "column.destination": { - "defaultMessage": "Destination" - }, - "column.details": { - "defaultMessage": "Détails" - }, - "column.email": { - "defaultMessage": "eMail" - }, - "column.event": { - "defaultMessage": "Évènement" - }, - "column.expires": { - "defaultMessage": "Expire" - }, - "column.http-code": { - "defaultMessage": "Code HTTP" - }, - "column.incoming-port": { - "defaultMessage": "Port entrant" - }, - "column.name": { - "defaultMessage": "Nom" - }, - "column.protocol": { - "defaultMessage": "Protocole" - }, - "column.provider": { - "defaultMessage": "Fournisseur" - }, - "column.roles": { - "defaultMessage": "Rôles" - }, - "column.rules": { - "defaultMessage": "Règles" - }, - "column.satisfy": { - "defaultMessage": "Valide" - }, - "column.satisfy-all": { - "defaultMessage": "All" - }, - "column.satisfy-any": { - "defaultMessage": "Any" - }, - "column.scheme": { - "defaultMessage": "Schéma" - }, - "column.source": { - "defaultMessage": "Source" - }, - "column.ssl": { - "defaultMessage": "SSL" - }, - "column.status": { - "defaultMessage": "Statut" - }, - "created-on": { - "defaultMessage": "Créé : {date}" - }, - "dashboard": { - "defaultMessage": "Tableau de bord" - }, - "dead-host": { - "defaultMessage": "Hôte 404" - }, - "dead-hosts": { - "defaultMessage": "Hôtes 404" - }, - "dead-hosts.count": { - "defaultMessage": "{count} {count, plural, one {Hôte 404} other {Hôtes 404}}" - }, - "disabled": { - "defaultMessage": "Désactivé" - }, - "domain-names": { - "defaultMessage": "Noms de domaine" - }, - "domain-names.max": { - "defaultMessage": "{count} noms de domaine au maximum" - }, - "domain-names.placeholder": { - "defaultMessage": "Commencez à écrire pour ajouter un domaine…" - }, - "domain-names.wildcards-not-permitted": { - "defaultMessage": "Les Wildcards ne sont pas permises dans ce cas" - }, - "domain-names.wildcards-not-supported": { - "defaultMessage": "Les Wildcards ne sont pas prises en charge par cette autorité de certification." - }, - "domains.force-ssl": { - "defaultMessage": "Forcer SSL" - }, - "domains.hsts-enabled": { - "defaultMessage": "HSTS activé" - }, - "domains.hsts-subdomains": { - "defaultMessage": "Sous-domaines HSTS" - }, - "domains.http2-support": { - "defaultMessage": "Prise en charge de HTTP/2" - }, - "domains.use-dns": { - "defaultMessage": "Utiliser le challenge DNS" - }, - "email-address": { - "defaultMessage": "Adresse eMail" - }, - "empty-search": { - "defaultMessage": "Aucun résultat trouvé" - }, - "empty-subtitle": { - "defaultMessage": "Pourquoi n'en créez-vous pas un ?" - }, - "enabled": { - "defaultMessage": "Activé" - }, - "error.access.at-least-one": { - "defaultMessage": "Une autorisation ou une règle d'accès est requise." - }, - "error.access.duplicate-usernames": { - "defaultMessage": "Les noms d'utilisateurs autorisés doivent être uniques" - }, - "error.invalid-auth": { - "defaultMessage": "Adresse eMail ou mot de passe invalide" - }, - "error.invalid-domain": { - "defaultMessage": "Domaine invalide : {domain}" - }, - "error.invalid-email": { - "defaultMessage": "Adresse eMail invalide" - }, - "error.max-character-length": { - "defaultMessage": "La longueur maximale est {max} caractère{max, plural, one {} other {s}}" - }, - "error.max-domains": { - "defaultMessage": "Trop de domaines, le maximum est {max}" - }, - "error.maximum": { - "defaultMessage": "Le maximum est {max}" - }, - "error.min-character-length": { - "defaultMessage": "La longueur minimale est {min} caractère{min, plural, one {} other {s}}" - }, - "error.minimum": { - "defaultMessage": "Le minimum est {min}" - }, - "error.passwords-must-match": { - "defaultMessage": "Les mots de passe doivent correspondre" - }, - "error.required": { - "defaultMessage": "Ceci est obligatoire" - }, - "expires.on": { - "defaultMessage": "Expire : {date}" - }, - "footer.github-fork": { - "defaultMessage": "Forkez-moi sur Github" - }, - "host.flags.block-exploits": { - "defaultMessage": "Bloquer les exploits courants" - }, - "host.flags.cache-assets": { - "defaultMessage": "Ressources du cache" - }, - "host.flags.preserve-path": { - "defaultMessage": "Préserver le chemin" - }, - "host.flags.protocols": { - "defaultMessage": "Protocoles" - }, - "host.flags.websockets-upgrade": { - "defaultMessage": "Prise en charge de Websockets" - }, - "host.forward-port": { - "defaultMessage": "Port de redirection" - }, - "host.forward-scheme": { - "defaultMessage": "Schéma" - }, - "hosts": { - "defaultMessage": "Hôtes" - }, - "http-only": { - "defaultMessage": "HTTP uniquement" - }, - "lets-encrypt": { - "defaultMessage": "Let's Encrypt" - }, - "lets-encrypt-via-dns": { - "defaultMessage": "Let's Encrypt via DNS" - }, - "lets-encrypt-via-http": { - "defaultMessage": "Let's Encrypt via HTTP" - }, - "loading": { - "defaultMessage": "Chargement…" - }, - "login.title": { - "defaultMessage": "Connectez-vous à votre compte" - }, - "nginx-config.label": { - "defaultMessage": "Configuration Nginx personnalisée" - }, - "nginx-config.placeholder": { - "defaultMessage": "# Mettez ici votre configuration Nginx personnalisé à vos risques et périls !" - }, - "no-permission-error": { - "defaultMessage": "Vous n'avez pas la permission de voir ce contenu." - }, - "notfound.action": { - "defaultMessage": "Ramenez-moi à l'accueil" - }, - "notfound.content": { - "defaultMessage": "Nous sommes désolés, mais la page que vous cherchez est introuvable" - }, - "notfound.title": { - "defaultMessage": "Oops… Vous avez découvert une page d'erreur" - }, - "notification.error": { - "defaultMessage": "Erreur" - }, - "notification.object-deleted": { - "defaultMessage": "{object} a été supprimé" - }, - "notification.object-disabled": { - "defaultMessage": "{object} a été désactivé" - }, - "notification.object-enabled": { - "defaultMessage": "{object} a été activé" - }, - "notification.object-renewed": { - "defaultMessage": "{object} a été renouvelé" - }, - "notification.object-saved": { - "defaultMessage": "{object} a été enregistré" - }, - "notification.success": { - "defaultMessage": "Réussi" - }, - "object.actions-title": { - "defaultMessage": "{object} #{id}" - }, - "object.add": { - "defaultMessage": "Ajouter {object}" - }, - "object.delete": { - "defaultMessage": "Supprimer {object}" - }, - "object.delete.content": { - "defaultMessage": "Êtes-vous sûr de vouloir supprimer {object} ?" - }, - "object.edit": { - "defaultMessage": "Modifier {object}" - }, - "object.empty": { - "defaultMessage": "Il n'y a aucun {objects}" - }, - "object.event.created": { - "defaultMessage": "{object} créé" - }, - "object.event.deleted": { - "defaultMessage": "{object} supprimé" - }, - "object.event.disabled": { - "defaultMessage": "{object} désactivé" - }, - "object.event.enabled": { - "defaultMessage": "{object} activé" - }, - "object.event.renewed": { - "defaultMessage": "{object} renouvelé" - }, - "object.event.updated": { - "defaultMessage": "{object} mis à jour" - }, - "offline": { - "defaultMessage": "Hors ligne" - }, - "online": { - "defaultMessage": "En ligne" - }, - "options": { - "defaultMessage": "Options" - }, - "password": { - "defaultMessage": "Mot de passe" - }, - "password.generate": { - "defaultMessage": "Générer un mot de passe aléatoire" - }, - "password.hide": { - "defaultMessage": "Masquer le mot de passe" - }, - "password.show": { - "defaultMessage": "Afficher le mot de passe" - }, - "permissions.hidden": { - "defaultMessage": "Masquer" - }, - "permissions.manage": { - "defaultMessage": "Gérer" - }, - "permissions.view": { - "defaultMessage": "Voir uniquement" - }, - "permissions.visibility.all": { - "defaultMessage": "Tous les éléments" - }, - "permissions.visibility.title": { - "defaultMessage": "Éléments visibles" - }, - "permissions.visibility.user": { - "defaultMessage": "Éléments créés uniquement" - }, - "proxy-host": { - "defaultMessage": "Hôte proxy" - }, - "proxy-host.forward-host": { - "defaultMessage": "Nom d'hôte de redirection / IP" - }, - "proxy-hosts": { - "defaultMessage": "Hôtes proxy" - }, - "proxy-hosts.count": { - "defaultMessage": "{count} {count, plural, one {Hôte proxy} other {Hôtes proxy}}" - }, - "public": { - "defaultMessage": "Publique" - }, - "redirection-host": { - "defaultMessage": "Hôte de redirection" - }, - "redirection-host.forward-domain": { - "defaultMessage": "Domaine de redirection" - }, - "redirection-host.forward-http-code": { - "defaultMessage": "Code HTTP" - }, - "redirection-hosts": { - "defaultMessage": "Hôtes de redirection" - }, - "redirection-hosts.count": { - "defaultMessage": "{count} {count, plural, one {Hôte de redirection} other {Hôtes de redirection}}" - }, - "role.admin": { - "defaultMessage": "Administrateur" - }, - "role.standard-user": { - "defaultMessage": "Utilisateur standard" - }, - "save": { - "defaultMessage": "Enregistrer" - }, - "setting": { - "defaultMessage": "Paramètre" - }, - "settings": { - "defaultMessage": "Paramètres" - }, - "settings.default-site": { - "defaultMessage": "Site par défaut" - }, - "settings.default-site.404": { - "defaultMessage": "Page 404" - }, - "settings.default-site.444": { - "defaultMessage": "Aucune réponse (444)" - }, - "settings.default-site.congratulations": { - "defaultMessage": "Page de félicitations" - }, - "settings.default-site.description": { - "defaultMessage": "ce qu'il faut afficher lorsqu'un hôte inconnu est détecté par Nginx" - }, - "settings.default-site.html": { - "defaultMessage": "HTML personnalisé" - }, - "settings.default-site.html.placeholder": { - "defaultMessage": "" - }, - "settings.default-site.redirect": { - "defaultMessage": "Redirection" - }, - "setup.preamble": { - "defaultMessage": "Commencez par créer votre compte administrateur." - }, - "setup.title": { - "defaultMessage": "Bienvenue !" - }, - "sign-in": { - "defaultMessage": "Se connecter" - }, - "ssl-certificate": { - "defaultMessage": "Certificat SSL" - }, - "stream": { - "defaultMessage": "Stream" - }, - "stream.forward-host": { - "defaultMessage": "Hôte destinataire" - }, - "stream.incoming-port": { - "defaultMessage": "Port d'entrée" - }, - "streams": { - "defaultMessage": "Streams" - }, - "streams.count": { - "defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}" - }, - "streams.tcp": { - "defaultMessage": "TCP" - }, - "streams.udp": { - "defaultMessage": "UDP" - }, - "test": { - "defaultMessage": "Test" - }, - "update-available": { - "defaultMessage": "Mise à jour disponible : {latestVersion}" - }, - "user": { - "defaultMessage": "Utilisateur" - }, - "user.change-password": { - "defaultMessage": "Modifier le mot de passe" - }, - "user.confirm-password": { - "defaultMessage": "Confirmer le mot de passe" - }, - "user.current-password": { - "defaultMessage": "Mot de passe actuel" - }, - "user.edit-profile": { - "defaultMessage": "Modifier le profil" - }, - "user.full-name": { - "defaultMessage": "Nom complet" - }, - "user.login-as": { - "defaultMessage": "Se connecter en tant que {name}" - }, - "user.logout": { - "defaultMessage": "Déconnexion" - }, - "user.new-password": { - "defaultMessage": "Nouveau mot de passe" - }, - "user.nickname": { - "defaultMessage": "Pseudonyme" - }, - "user.set-password": { - "defaultMessage": "Définir le mot de passe" - }, - "user.set-permissions": { - "defaultMessage": "Définir les autorisations pour {name}" - }, - "user.switch-dark": { - "defaultMessage": "Passer au mode Sombre" - }, - "user.switch-light": { - "defaultMessage": "Passer au mode Lumineux" - }, - "username": { - "defaultMessage": "Nom d'utilisateur" - }, - "users": { - "defaultMessage": "Utilisateurs" - } + "access-list": { + "defaultMessage": "Liste d'accès" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Règle} other {Règles}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {Utilisateur} other {Utilisateurs}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "S'il existe au moins une règle, cette règle de refuser tout sera ajoutée en dernier." + }, + "access-list.help.rules-order": { + "defaultMessage": "Notez que les directives autoriser et refuser seront appliquées dans l'ordre où elles sont définies." + }, + "access-list.pass-auth": { + "defaultMessage": "Transmettre l'authentification au serveur en amont" + }, + "access-list.public": { + "defaultMessage": "Accessible au public" + }, + "access-list.public.subtitle": { + "defaultMessage": "Aucune authentification de base requise" + }, + "access-list.satisfy-any": { + "defaultMessage": "Valide n'importe quelle règle" + }, + "access-list.subtitle": { + "defaultMessage": "{utilisateurs} {utilisateurs, plural, one {Utilisateur} other {Utilisateurs}}, {règles} {règles, plural, one {Règle} other {Règles}} - Crée : {date}" + }, + "access-lists": { + "defaultMessage": "Listes d'accès" + }, + "action.add": { + "defaultMessage": "Ajouter" + }, + "action.add-location": { + "defaultMessage": "Ajouter localisation" + }, + "action.close": { + "defaultMessage": "Fermer" + }, + "action.delete": { + "defaultMessage": "Supprimer" + }, + "action.disable": { + "defaultMessage": "Désactiver" + }, + "action.download": { + "defaultMessage": "Télécharger" + }, + "action.edit": { + "defaultMessage": "Modifier" + }, + "action.enable": { + "defaultMessage": "Activer" + }, + "action.permissions": { + "defaultMessage": "Permissions" + }, + "action.renew": { + "defaultMessage": "Renouveler" + }, + "action.view-details": { + "defaultMessage": "Voir les Détails" + }, + "auditlogs": { + "defaultMessage": "Journaux d'audit" + }, + "cancel": { + "defaultMessage": "Annuler" + }, + "certificate": { + "defaultMessage": "Certificat" + }, + "certificate.custom-certificate": { + "defaultMessage": "Certificat" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Clé du Certificat" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Certificat intermédiaire" + }, + "certificate.in-use": { + "defaultMessage": "Utilisé" + }, + "certificate.none.subtitle": { + "defaultMessage": "Aucun certificat assigné" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Cet hôte n'utilisera pas le HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Aucun" + }, + "certificate.not-in-use": { + "defaultMessage": "Non utilisé" + }, + "certificate.renew": { + "defaultMessage": "Renouveler Certificat" + }, + "certificates": { + "defaultMessage": "Certificats" + }, + "certificates.custom": { + "defaultMessage": "Certificat personnalisé" + }, + "certificates.custom.warning": { + "defaultMessage": "Les fichiers de clé protégés par une passphrase ne sont pas acceptés." + }, + "certificates.dns.credentials": { + "defaultMessage": "Contenu du fichier d'identifiants" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Ce plugin nécessite un fichier de configuration contenant un jeton d'API ou d'autres informations d'identification pour votre fournisseur." + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Ces données seront stockées en clair dans la base de données et dans un fichier !" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Propagation Seconds" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Laisser vide pour utiliser la valeur par défaut du plugin. Nombre de secondes à attendre pour la propagation DNS." + }, + "certificates.dns.provider": { + "defaultMessage": "Fournisseur DNS" + }, + "certificates.dns.warning": { + "defaultMessage": "Cette section requiert une certaine connaissance de Certbot et de ses plugins DNS. Veuillez consulter la documentation des plugins correspondants." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il ne semble pas s'agir de Nginx Proxy Manager. Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Impossible de vérifier l'accessibilité en raison d'une erreur de communication avec site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Aucun serveur n'est disponible pour ce domaine. Veuillez vérifier que votre domaine existe et pointe vers l'adresse IP où votre instance NPM est exécutée. Si nécessaire, le port 80 est ouvert dans votre routeur." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Votre serveur est accessible et la création de certificats devrait être possible." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé un code d'état inattendu {code}. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé des données inattendues. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." + }, + "certificates.http.test-results": { + "defaultMessage": "Résultats du test" + }, + "certificates.http.warning": { + "defaultMessage": "Ces domaines doivent déjà être configurés pour pointer vers cette installation." + }, + "certificates.key-type": { + "defaultMessage": "Type de clé" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA est largement répandu, ECDSA est plus rapide et plus sécurisé, mais peut ne pas être supporté par les systèmes plus anciens" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "certificates.request.subtitle": { + "defaultMessage": "avec Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Demander un nouveau certificat" + }, + "column.access": { + "defaultMessage": "Accès" + }, + "column.authorization": { + "defaultMessage": "Autorisation" + }, + "column.authorizations": { + "defaultMessage": "Autorisations" + }, + "column.custom-locations": { + "defaultMessage": "Emplacement personnalisé" + }, + "column.destination": { + "defaultMessage": "Destination" + }, + "column.details": { + "defaultMessage": "Détails" + }, + "column.email": { + "defaultMessage": "eMail" + }, + "column.event": { + "defaultMessage": "Évènement" + }, + "column.expires": { + "defaultMessage": "Expire" + }, + "column.http-code": { + "defaultMessage": "Code HTTP" + }, + "column.incoming-port": { + "defaultMessage": "Port entrant" + }, + "column.name": { + "defaultMessage": "Nom" + }, + "column.protocol": { + "defaultMessage": "Protocole" + }, + "column.provider": { + "defaultMessage": "Fournisseur" + }, + "column.roles": { + "defaultMessage": "Rôles" + }, + "column.rules": { + "defaultMessage": "Règles" + }, + "column.satisfy": { + "defaultMessage": "Valide" + }, + "column.satisfy-all": { + "defaultMessage": "All" + }, + "column.satisfy-any": { + "defaultMessage": "Any" + }, + "column.scheme": { + "defaultMessage": "Schéma" + }, + "column.source": { + "defaultMessage": "Source" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Statut" + }, + "created-on": { + "defaultMessage": "Créé : {date}" + }, + "dashboard": { + "defaultMessage": "Tableau de bord" + }, + "dead-host": { + "defaultMessage": "Hôte 404" + }, + "dead-hosts": { + "defaultMessage": "Hôtes 404" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Hôte 404} other {Hôtes 404}}" + }, + "disabled": { + "defaultMessage": "Désactivé" + }, + "domain-names": { + "defaultMessage": "Noms de domaine" + }, + "domain-names.max": { + "defaultMessage": "{count} noms de domaine au maximum" + }, + "domain-names.placeholder": { + "defaultMessage": "Commencez à écrire pour ajouter un domaine…" + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Les Wildcards ne sont pas permises dans ce cas" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Les Wildcards ne sont pas prises en charge par cette autorité de certification." + }, + "domains.force-ssl": { + "defaultMessage": "Forcer SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS activé" + }, + "domains.hsts-subdomains": { + "defaultMessage": "Sous-domaines HSTS" + }, + "domains.http2-support": { + "defaultMessage": "Prise en charge de HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Utiliser le challenge DNS" + }, + "email-address": { + "defaultMessage": "Adresse eMail" + }, + "empty-search": { + "defaultMessage": "Aucun résultat trouvé" + }, + "empty-subtitle": { + "defaultMessage": "Pourquoi n'en créez-vous pas un ?" + }, + "enabled": { + "defaultMessage": "Activé" + }, + "error.access.at-least-one": { + "defaultMessage": "Une autorisation ou une règle d'accès est requise." + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Les noms d'utilisateurs autorisés doivent être uniques" + }, + "error.invalid-auth": { + "defaultMessage": "Adresse eMail ou mot de passe invalide" + }, + "error.invalid-domain": { + "defaultMessage": "Domaine invalide : {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Adresse eMail invalide" + }, + "error.max-character-length": { + "defaultMessage": "La longueur maximale est {max} caractère{max, plural, one {} other {s}}" + }, + "error.max-domains": { + "defaultMessage": "Trop de domaines, le maximum est {max}" + }, + "error.maximum": { + "defaultMessage": "Le maximum est {max}" + }, + "error.min-character-length": { + "defaultMessage": "La longueur minimale est {min} caractère{min, plural, one {} other {s}}" + }, + "error.minimum": { + "defaultMessage": "Le minimum est {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Les mots de passe doivent correspondre" + }, + "error.required": { + "defaultMessage": "Ceci est obligatoire" + }, + "expires.on": { + "defaultMessage": "Expire : {date}" + }, + "footer.github-fork": { + "defaultMessage": "Forkez-moi sur Github" + }, + "host.flags.block-exploits": { + "defaultMessage": "Bloquer les exploits courants" + }, + "host.flags.cache-assets": { + "defaultMessage": "Ressources du cache" + }, + "host.flags.preserve-path": { + "defaultMessage": "Préserver le chemin" + }, + "host.flags.protocols": { + "defaultMessage": "Protocoles" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Prise en charge de Websockets" + }, + "host.forward-port": { + "defaultMessage": "Port de redirection" + }, + "host.forward-scheme": { + "defaultMessage": "Schéma" + }, + "hosts": { + "defaultMessage": "Hôtes" + }, + "http-only": { + "defaultMessage": "HTTP uniquement" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt via DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt via HTTP" + }, + "loading": { + "defaultMessage": "Chargement…" + }, + "login.title": { + "defaultMessage": "Connectez-vous à votre compte" + }, + "nginx-config.label": { + "defaultMessage": "Configuration Nginx personnalisée" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Mettez ici votre configuration Nginx personnalisé à vos risques et périls !" + }, + "no-permission-error": { + "defaultMessage": "Vous n'avez pas la permission de voir ce contenu." + }, + "notfound.action": { + "defaultMessage": "Ramenez-moi à l'accueil" + }, + "notfound.content": { + "defaultMessage": "Nous sommes désolés, mais la page que vous cherchez est introuvable" + }, + "notfound.title": { + "defaultMessage": "Oops… Vous avez découvert une page d'erreur" + }, + "notification.error": { + "defaultMessage": "Erreur" + }, + "notification.object-deleted": { + "defaultMessage": "{object} a été supprimé" + }, + "notification.object-disabled": { + "defaultMessage": "{object} a été désactivé" + }, + "notification.object-enabled": { + "defaultMessage": "{object} a été activé" + }, + "notification.object-renewed": { + "defaultMessage": "{object} a été renouvelé" + }, + "notification.object-saved": { + "defaultMessage": "{object} a été enregistré" + }, + "notification.success": { + "defaultMessage": "Réussi" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Ajouter {object}" + }, + "object.delete": { + "defaultMessage": "Supprimer {object}" + }, + "object.delete.content": { + "defaultMessage": "Êtes-vous sûr de vouloir supprimer {object} ?" + }, + "object.edit": { + "defaultMessage": "Modifier {object}" + }, + "object.empty": { + "defaultMessage": "Il n'y a aucun {objects}" + }, + "object.event.created": { + "defaultMessage": "{object} créé" + }, + "object.event.deleted": { + "defaultMessage": "{object} supprimé" + }, + "object.event.disabled": { + "defaultMessage": "{object} désactivé" + }, + "object.event.enabled": { + "defaultMessage": "{object} activé" + }, + "object.event.renewed": { + "defaultMessage": "{object} renouvelé" + }, + "object.event.updated": { + "defaultMessage": "{object} mis à jour" + }, + "offline": { + "defaultMessage": "Hors ligne" + }, + "online": { + "defaultMessage": "En ligne" + }, + "options": { + "defaultMessage": "Options" + }, + "password": { + "defaultMessage": "Mot de passe" + }, + "password.generate": { + "defaultMessage": "Générer un mot de passe aléatoire" + }, + "password.hide": { + "defaultMessage": "Masquer le mot de passe" + }, + "password.show": { + "defaultMessage": "Afficher le mot de passe" + }, + "permissions.hidden": { + "defaultMessage": "Masquer" + }, + "permissions.manage": { + "defaultMessage": "Gérer" + }, + "permissions.view": { + "defaultMessage": "Voir uniquement" + }, + "permissions.visibility.all": { + "defaultMessage": "Tous les éléments" + }, + "permissions.visibility.title": { + "defaultMessage": "Éléments visibles" + }, + "permissions.visibility.user": { + "defaultMessage": "Éléments créés uniquement" + }, + "proxy-host": { + "defaultMessage": "Hôte proxy" + }, + "proxy-host.forward-host": { + "defaultMessage": "Nom d'hôte de redirection / IP" + }, + "proxy-hosts": { + "defaultMessage": "Hôtes proxy" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Hôte proxy} other {Hôtes proxy}}" + }, + "public": { + "defaultMessage": "Publique" + }, + "redirection-host": { + "defaultMessage": "Hôte de redirection" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Domaine de redirection" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "Code HTTP" + }, + "redirection-hosts": { + "defaultMessage": "Hôtes de redirection" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Hôte de redirection} other {Hôtes de redirection}}" + }, + "role.admin": { + "defaultMessage": "Administrateur" + }, + "role.standard-user": { + "defaultMessage": "Utilisateur standard" + }, + "save": { + "defaultMessage": "Enregistrer" + }, + "setting": { + "defaultMessage": "Paramètre" + }, + "settings": { + "defaultMessage": "Paramètres" + }, + "settings.default-site": { + "defaultMessage": "Site par défaut" + }, + "settings.default-site.404": { + "defaultMessage": "Page 404" + }, + "settings.default-site.444": { + "defaultMessage": "Aucune réponse (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Page de félicitations" + }, + "settings.default-site.description": { + "defaultMessage": "ce qu'il faut afficher lorsqu'un hôte inconnu est détecté par Nginx" + }, + "settings.default-site.html": { + "defaultMessage": "HTML personnalisé" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Redirection" + }, + "setup.preamble": { + "defaultMessage": "Commencez par créer votre compte administrateur." + }, + "setup.title": { + "defaultMessage": "Bienvenue !" + }, + "sign-in": { + "defaultMessage": "Se connecter" + }, + "ssl-certificate": { + "defaultMessage": "Certificat SSL" + }, + "stream": { + "defaultMessage": "Stream" + }, + "stream.forward-host": { + "defaultMessage": "Hôte destinataire" + }, + "stream.incoming-port": { + "defaultMessage": "Port d'entrée" + }, + "streams": { + "defaultMessage": "Streams" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Test" + }, + "user": { + "defaultMessage": "Utilisateur" + }, + "user.change-password": { + "defaultMessage": "Modifier le mot de passe" + }, + "user.confirm-password": { + "defaultMessage": "Confirmer le mot de passe" + }, + "user.current-password": { + "defaultMessage": "Mot de passe actuel" + }, + "user.edit-profile": { + "defaultMessage": "Modifier le profil" + }, + "user.full-name": { + "defaultMessage": "Nom complet" + }, + "user.login-as": { + "defaultMessage": "Se connecter en tant que {name}" + }, + "user.logout": { + "defaultMessage": "Déconnexion" + }, + "user.new-password": { + "defaultMessage": "Nouveau mot de passe" + }, + "user.nickname": { + "defaultMessage": "Pseudonyme" + }, + "user.set-password": { + "defaultMessage": "Définir le mot de passe" + }, + "user.set-permissions": { + "defaultMessage": "Définir les autorisations pour {name}" + }, + "user.switch-dark": { + "defaultMessage": "Passer au mode Sombre" + }, + "user.switch-light": { + "defaultMessage": "Passer au mode Lumineux" + }, + "username": { + "defaultMessage": "Nom d'utilisateur" + }, + "users": { + "defaultMessage": "Utilisateurs" + } } From dea7007802aef7a0f0005e243352190c2566f919 Mon Sep 17 00:00:00 2001 From: MarioGervais <17948611+MarioGervais@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:08:53 -0500 Subject: [PATCH 02/19] feat(i18n): complete French (fr) translation Add 4 missing certificate key-type strings, remove obsolete update-available key --- frontend/src/locale/src/fr.json | 1308 +++++++++++++++---------------- 1 file changed, 654 insertions(+), 654 deletions(-) diff --git a/frontend/src/locale/src/fr.json b/frontend/src/locale/src/fr.json index d8a6de69..f3756127 100644 --- a/frontend/src/locale/src/fr.json +++ b/frontend/src/locale/src/fr.json @@ -1,656 +1,656 @@ { - "access-list": { - "defaultMessage": "Liste d'accès" - }, - "access-list.access-count": { - "defaultMessage": "{count} {count, plural, one {Règle} other {Règles}}" - }, - "access-list.auth-count": { - "defaultMessage": "{count} {count, plural, one {Utilisateur} other {Utilisateurs}}" - }, - "access-list.help-rules-last": { - "defaultMessage": "S'il existe au moins une règle, cette règle de refuser tout sera ajoutée en dernier." - }, - "access-list.help.rules-order": { - "defaultMessage": "Notez que les directives autoriser et refuser seront appliquées dans l'ordre où elles sont définies." - }, - "access-list.pass-auth": { - "defaultMessage": "Transmettre l'authentification au serveur en amont" - }, - "access-list.public": { - "defaultMessage": "Accessible au public" - }, - "access-list.public.subtitle": { - "defaultMessage": "Aucune authentification de base requise" - }, - "access-list.satisfy-any": { - "defaultMessage": "Valide n'importe quelle règle" - }, - "access-list.subtitle": { - "defaultMessage": "{utilisateurs} {utilisateurs, plural, one {Utilisateur} other {Utilisateurs}}, {règles} {règles, plural, one {Règle} other {Règles}} - Crée : {date}" - }, - "access-lists": { - "defaultMessage": "Listes d'accès" - }, - "action.add": { - "defaultMessage": "Ajouter" - }, - "action.add-location": { - "defaultMessage": "Ajouter localisation" - }, - "action.close": { - "defaultMessage": "Fermer" - }, - "action.delete": { - "defaultMessage": "Supprimer" - }, - "action.disable": { - "defaultMessage": "Désactiver" - }, - "action.download": { - "defaultMessage": "Télécharger" - }, - "action.edit": { - "defaultMessage": "Modifier" - }, - "action.enable": { - "defaultMessage": "Activer" - }, - "action.permissions": { - "defaultMessage": "Permissions" - }, - "action.renew": { - "defaultMessage": "Renouveler" - }, - "action.view-details": { - "defaultMessage": "Voir les Détails" - }, - "auditlogs": { - "defaultMessage": "Journaux d'audit" - }, - "cancel": { - "defaultMessage": "Annuler" - }, - "certificate": { - "defaultMessage": "Certificat" - }, - "certificate.custom-certificate": { - "defaultMessage": "Certificat" - }, - "certificate.custom-certificate-key": { - "defaultMessage": "Clé du Certificat" - }, - "certificate.custom-intermediate": { - "defaultMessage": "Certificat intermédiaire" - }, - "certificate.in-use": { - "defaultMessage": "Utilisé" - }, - "certificate.none.subtitle": { - "defaultMessage": "Aucun certificat assigné" - }, - "certificate.none.subtitle.for-http": { - "defaultMessage": "Cet hôte n'utilisera pas le HTTPS" - }, - "certificate.none.title": { - "defaultMessage": "Aucun" - }, - "certificate.not-in-use": { - "defaultMessage": "Non utilisé" - }, - "certificate.renew": { - "defaultMessage": "Renouveler Certificat" - }, - "certificates": { - "defaultMessage": "Certificats" - }, - "certificates.custom": { - "defaultMessage": "Certificat personnalisé" - }, - "certificates.custom.warning": { - "defaultMessage": "Les fichiers de clé protégés par une passphrase ne sont pas acceptés." - }, - "certificates.dns.credentials": { - "defaultMessage": "Contenu du fichier d'identifiants" - }, - "certificates.dns.credentials-note": { - "defaultMessage": "Ce plugin nécessite un fichier de configuration contenant un jeton d'API ou d'autres informations d'identification pour votre fournisseur." - }, - "certificates.dns.credentials-warning": { - "defaultMessage": "Ces données seront stockées en clair dans la base de données et dans un fichier !" - }, - "certificates.dns.propagation-seconds": { - "defaultMessage": "Propagation Seconds" - }, - "certificates.dns.propagation-seconds-note": { - "defaultMessage": "Laisser vide pour utiliser la valeur par défaut du plugin. Nombre de secondes à attendre pour la propagation DNS." - }, - "certificates.dns.provider": { - "defaultMessage": "Fournisseur DNS" - }, - "certificates.dns.warning": { - "defaultMessage": "Cette section requiert une certaine connaissance de Certbot et de ses plugins DNS. Veuillez consulter la documentation des plugins correspondants." - }, - "certificates.http.reachability-404": { - "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il ne semble pas s'agir de Nginx Proxy Manager. Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." - }, - "certificates.http.reachability-failed-to-check": { - "defaultMessage": "Impossible de vérifier l'accessibilité en raison d'une erreur de communication avec site24x7.com." - }, - "certificates.http.reachability-not-resolved": { - "defaultMessage": "Aucun serveur n'est disponible pour ce domaine. Veuillez vérifier que votre domaine existe et pointe vers l'adresse IP où votre instance NPM est exécutée. Si nécessaire, le port 80 est ouvert dans votre routeur." - }, - "certificates.http.reachability-ok": { - "defaultMessage": "Votre serveur est accessible et la création de certificats devrait être possible." - }, - "certificates.http.reachability-other": { - "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé un code d'état inattendu {code}. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." - }, - "certificates.http.reachability-wrong-data": { - "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé des données inattendues. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." - }, - "certificates.http.test-results": { - "defaultMessage": "Résultats du test" - }, - "certificates.http.warning": { - "defaultMessage": "Ces domaines doivent déjà être configurés pour pointer vers cette installation." - }, - "certificates.key-type": { - "defaultMessage": "Type de clé" - }, - "certificates.key-type-description": { - "defaultMessage": "RSA est largement répandu, ECDSA est plus rapide et plus sécurisé, mais peut ne pas être supporté par les systèmes plus anciens" - }, - "certificates.key-type-ecdsa": { - "defaultMessage": "ECDSA 256" - }, - "certificates.key-type-rsa": { - "defaultMessage": "RSA 2048" - }, - "certificates.request.subtitle": { - "defaultMessage": "avec Let's Encrypt" - }, - "certificates.request.title": { - "defaultMessage": "Demander un nouveau certificat" - }, - "column.access": { - "defaultMessage": "Accès" - }, - "column.authorization": { - "defaultMessage": "Autorisation" - }, - "column.authorizations": { - "defaultMessage": "Autorisations" - }, - "column.custom-locations": { - "defaultMessage": "Emplacement personnalisé" - }, - "column.destination": { - "defaultMessage": "Destination" - }, - "column.details": { - "defaultMessage": "Détails" - }, - "column.email": { - "defaultMessage": "eMail" - }, - "column.event": { - "defaultMessage": "Évènement" - }, - "column.expires": { - "defaultMessage": "Expire" - }, - "column.http-code": { - "defaultMessage": "Code HTTP" - }, - "column.incoming-port": { - "defaultMessage": "Port entrant" - }, - "column.name": { - "defaultMessage": "Nom" - }, - "column.protocol": { - "defaultMessage": "Protocole" - }, - "column.provider": { - "defaultMessage": "Fournisseur" - }, - "column.roles": { - "defaultMessage": "Rôles" - }, - "column.rules": { - "defaultMessage": "Règles" - }, - "column.satisfy": { - "defaultMessage": "Valide" - }, - "column.satisfy-all": { - "defaultMessage": "All" - }, - "column.satisfy-any": { - "defaultMessage": "Any" - }, - "column.scheme": { - "defaultMessage": "Schéma" - }, - "column.source": { - "defaultMessage": "Source" - }, - "column.ssl": { - "defaultMessage": "SSL" - }, - "column.status": { - "defaultMessage": "Statut" - }, - "created-on": { - "defaultMessage": "Créé : {date}" - }, - "dashboard": { - "defaultMessage": "Tableau de bord" - }, - "dead-host": { - "defaultMessage": "Hôte 404" - }, - "dead-hosts": { - "defaultMessage": "Hôtes 404" - }, - "dead-hosts.count": { - "defaultMessage": "{count} {count, plural, one {Hôte 404} other {Hôtes 404}}" - }, - "disabled": { - "defaultMessage": "Désactivé" - }, - "domain-names": { - "defaultMessage": "Noms de domaine" - }, - "domain-names.max": { - "defaultMessage": "{count} noms de domaine au maximum" - }, - "domain-names.placeholder": { - "defaultMessage": "Commencez à écrire pour ajouter un domaine…" - }, - "domain-names.wildcards-not-permitted": { - "defaultMessage": "Les Wildcards ne sont pas permises dans ce cas" - }, - "domain-names.wildcards-not-supported": { - "defaultMessage": "Les Wildcards ne sont pas prises en charge par cette autorité de certification." - }, - "domains.force-ssl": { - "defaultMessage": "Forcer SSL" - }, - "domains.hsts-enabled": { - "defaultMessage": "HSTS activé" - }, - "domains.hsts-subdomains": { - "defaultMessage": "Sous-domaines HSTS" - }, - "domains.http2-support": { - "defaultMessage": "Prise en charge de HTTP/2" - }, - "domains.use-dns": { - "defaultMessage": "Utiliser le challenge DNS" - }, - "email-address": { - "defaultMessage": "Adresse eMail" - }, - "empty-search": { - "defaultMessage": "Aucun résultat trouvé" - }, - "empty-subtitle": { - "defaultMessage": "Pourquoi n'en créez-vous pas un ?" - }, - "enabled": { - "defaultMessage": "Activé" - }, - "error.access.at-least-one": { - "defaultMessage": "Une autorisation ou une règle d'accès est requise." - }, - "error.access.duplicate-usernames": { - "defaultMessage": "Les noms d'utilisateurs autorisés doivent être uniques" - }, - "error.invalid-auth": { - "defaultMessage": "Adresse eMail ou mot de passe invalide" - }, - "error.invalid-domain": { - "defaultMessage": "Domaine invalide : {domain}" - }, - "error.invalid-email": { - "defaultMessage": "Adresse eMail invalide" - }, - "error.max-character-length": { - "defaultMessage": "La longueur maximale est {max} caractère{max, plural, one {} other {s}}" - }, - "error.max-domains": { - "defaultMessage": "Trop de domaines, le maximum est {max}" - }, - "error.maximum": { - "defaultMessage": "Le maximum est {max}" - }, - "error.min-character-length": { - "defaultMessage": "La longueur minimale est {min} caractère{min, plural, one {} other {s}}" - }, - "error.minimum": { - "defaultMessage": "Le minimum est {min}" - }, - "error.passwords-must-match": { - "defaultMessage": "Les mots de passe doivent correspondre" - }, - "error.required": { - "defaultMessage": "Ceci est obligatoire" - }, - "expires.on": { - "defaultMessage": "Expire : {date}" - }, - "footer.github-fork": { - "defaultMessage": "Forkez-moi sur Github" - }, - "host.flags.block-exploits": { - "defaultMessage": "Bloquer les exploits courants" - }, - "host.flags.cache-assets": { - "defaultMessage": "Ressources du cache" - }, - "host.flags.preserve-path": { - "defaultMessage": "Préserver le chemin" - }, - "host.flags.protocols": { - "defaultMessage": "Protocoles" - }, - "host.flags.websockets-upgrade": { - "defaultMessage": "Prise en charge de Websockets" - }, - "host.forward-port": { - "defaultMessage": "Port de redirection" - }, - "host.forward-scheme": { - "defaultMessage": "Schéma" - }, - "hosts": { - "defaultMessage": "Hôtes" - }, - "http-only": { - "defaultMessage": "HTTP uniquement" - }, - "lets-encrypt": { - "defaultMessage": "Let's Encrypt" - }, - "lets-encrypt-via-dns": { - "defaultMessage": "Let's Encrypt via DNS" - }, - "lets-encrypt-via-http": { - "defaultMessage": "Let's Encrypt via HTTP" - }, - "loading": { - "defaultMessage": "Chargement…" - }, - "login.title": { - "defaultMessage": "Connectez-vous à votre compte" - }, - "nginx-config.label": { - "defaultMessage": "Configuration Nginx personnalisée" - }, - "nginx-config.placeholder": { - "defaultMessage": "# Mettez ici votre configuration Nginx personnalisé à vos risques et périls !" - }, - "no-permission-error": { - "defaultMessage": "Vous n'avez pas la permission de voir ce contenu." - }, - "notfound.action": { - "defaultMessage": "Ramenez-moi à l'accueil" - }, - "notfound.content": { - "defaultMessage": "Nous sommes désolés, mais la page que vous cherchez est introuvable" - }, - "notfound.title": { - "defaultMessage": "Oops… Vous avez découvert une page d'erreur" - }, - "notification.error": { - "defaultMessage": "Erreur" - }, - "notification.object-deleted": { - "defaultMessage": "{object} a été supprimé" - }, - "notification.object-disabled": { - "defaultMessage": "{object} a été désactivé" - }, - "notification.object-enabled": { - "defaultMessage": "{object} a été activé" - }, - "notification.object-renewed": { - "defaultMessage": "{object} a été renouvelé" - }, - "notification.object-saved": { - "defaultMessage": "{object} a été enregistré" - }, - "notification.success": { - "defaultMessage": "Réussi" - }, - "object.actions-title": { - "defaultMessage": "{object} #{id}" - }, - "object.add": { - "defaultMessage": "Ajouter {object}" - }, - "object.delete": { - "defaultMessage": "Supprimer {object}" - }, - "object.delete.content": { - "defaultMessage": "Êtes-vous sûr de vouloir supprimer {object} ?" - }, - "object.edit": { - "defaultMessage": "Modifier {object}" - }, - "object.empty": { - "defaultMessage": "Il n'y a aucun {objects}" - }, - "object.event.created": { - "defaultMessage": "{object} créé" - }, - "object.event.deleted": { - "defaultMessage": "{object} supprimé" - }, - "object.event.disabled": { - "defaultMessage": "{object} désactivé" - }, - "object.event.enabled": { - "defaultMessage": "{object} activé" - }, - "object.event.renewed": { - "defaultMessage": "{object} renouvelé" - }, - "object.event.updated": { - "defaultMessage": "{object} mis à jour" - }, - "offline": { - "defaultMessage": "Hors ligne" - }, - "online": { - "defaultMessage": "En ligne" - }, - "options": { - "defaultMessage": "Options" - }, - "password": { - "defaultMessage": "Mot de passe" - }, - "password.generate": { - "defaultMessage": "Générer un mot de passe aléatoire" - }, - "password.hide": { - "defaultMessage": "Masquer le mot de passe" - }, - "password.show": { - "defaultMessage": "Afficher le mot de passe" - }, - "permissions.hidden": { - "defaultMessage": "Masquer" - }, - "permissions.manage": { - "defaultMessage": "Gérer" - }, - "permissions.view": { - "defaultMessage": "Voir uniquement" - }, - "permissions.visibility.all": { - "defaultMessage": "Tous les éléments" - }, - "permissions.visibility.title": { - "defaultMessage": "Éléments visibles" - }, - "permissions.visibility.user": { - "defaultMessage": "Éléments créés uniquement" - }, - "proxy-host": { - "defaultMessage": "Hôte proxy" - }, - "proxy-host.forward-host": { - "defaultMessage": "Nom d'hôte de redirection / IP" - }, - "proxy-hosts": { - "defaultMessage": "Hôtes proxy" - }, - "proxy-hosts.count": { - "defaultMessage": "{count} {count, plural, one {Hôte proxy} other {Hôtes proxy}}" - }, - "public": { - "defaultMessage": "Publique" - }, - "redirection-host": { - "defaultMessage": "Hôte de redirection" - }, - "redirection-host.forward-domain": { - "defaultMessage": "Domaine de redirection" - }, - "redirection-host.forward-http-code": { - "defaultMessage": "Code HTTP" - }, - "redirection-hosts": { - "defaultMessage": "Hôtes de redirection" - }, - "redirection-hosts.count": { - "defaultMessage": "{count} {count, plural, one {Hôte de redirection} other {Hôtes de redirection}}" - }, - "role.admin": { - "defaultMessage": "Administrateur" - }, - "role.standard-user": { - "defaultMessage": "Utilisateur standard" - }, - "save": { - "defaultMessage": "Enregistrer" - }, - "setting": { - "defaultMessage": "Paramètre" - }, - "settings": { - "defaultMessage": "Paramètres" - }, - "settings.default-site": { - "defaultMessage": "Site par défaut" - }, - "settings.default-site.404": { - "defaultMessage": "Page 404" - }, - "settings.default-site.444": { - "defaultMessage": "Aucune réponse (444)" - }, - "settings.default-site.congratulations": { - "defaultMessage": "Page de félicitations" - }, - "settings.default-site.description": { - "defaultMessage": "ce qu'il faut afficher lorsqu'un hôte inconnu est détecté par Nginx" - }, - "settings.default-site.html": { - "defaultMessage": "HTML personnalisé" - }, - "settings.default-site.html.placeholder": { - "defaultMessage": "" - }, - "settings.default-site.redirect": { - "defaultMessage": "Redirection" - }, - "setup.preamble": { - "defaultMessage": "Commencez par créer votre compte administrateur." - }, - "setup.title": { - "defaultMessage": "Bienvenue !" - }, - "sign-in": { - "defaultMessage": "Se connecter" - }, - "ssl-certificate": { - "defaultMessage": "Certificat SSL" - }, - "stream": { - "defaultMessage": "Stream" - }, - "stream.forward-host": { - "defaultMessage": "Hôte destinataire" - }, - "stream.incoming-port": { - "defaultMessage": "Port d'entrée" - }, - "streams": { - "defaultMessage": "Streams" - }, - "streams.count": { - "defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}" - }, - "streams.tcp": { - "defaultMessage": "TCP" - }, - "streams.udp": { - "defaultMessage": "UDP" - }, - "test": { - "defaultMessage": "Test" - }, - "user": { - "defaultMessage": "Utilisateur" - }, - "user.change-password": { - "defaultMessage": "Modifier le mot de passe" - }, - "user.confirm-password": { - "defaultMessage": "Confirmer le mot de passe" - }, - "user.current-password": { - "defaultMessage": "Mot de passe actuel" - }, - "user.edit-profile": { - "defaultMessage": "Modifier le profil" - }, - "user.full-name": { - "defaultMessage": "Nom complet" - }, - "user.login-as": { - "defaultMessage": "Se connecter en tant que {name}" - }, - "user.logout": { - "defaultMessage": "Déconnexion" - }, - "user.new-password": { - "defaultMessage": "Nouveau mot de passe" - }, - "user.nickname": { - "defaultMessage": "Pseudonyme" - }, - "user.set-password": { - "defaultMessage": "Définir le mot de passe" - }, - "user.set-permissions": { - "defaultMessage": "Définir les autorisations pour {name}" - }, - "user.switch-dark": { - "defaultMessage": "Passer au mode Sombre" - }, - "user.switch-light": { - "defaultMessage": "Passer au mode Lumineux" - }, - "username": { - "defaultMessage": "Nom d'utilisateur" - }, - "users": { - "defaultMessage": "Utilisateurs" - } + "access-list": { + "defaultMessage": "Liste d'accès" + }, + "access-list.access-count": { + "defaultMessage": "{count} {count, plural, one {Règle} other {Règles}}" + }, + "access-list.auth-count": { + "defaultMessage": "{count} {count, plural, one {Utilisateur} other {Utilisateurs}}" + }, + "access-list.help-rules-last": { + "defaultMessage": "S'il existe au moins une règle, cette règle de refuser tout sera ajoutée en dernier." + }, + "access-list.help.rules-order": { + "defaultMessage": "Notez que les directives autoriser et refuser seront appliquées dans l'ordre où elles sont définies." + }, + "access-list.pass-auth": { + "defaultMessage": "Transmettre l'authentification au serveur en amont" + }, + "access-list.public": { + "defaultMessage": "Accessible au public" + }, + "access-list.public.subtitle": { + "defaultMessage": "Aucune authentification de base requise" + }, + "access-list.satisfy-any": { + "defaultMessage": "Valide n'importe quelle règle" + }, + "access-list.subtitle": { + "defaultMessage": "{utilisateurs} {utilisateurs, plural, one {Utilisateur} other {Utilisateurs}}, {règles} {règles, plural, one {Règle} other {Règles}} - Crée : {date}" + }, + "access-lists": { + "defaultMessage": "Listes d'accès" + }, + "action.add": { + "defaultMessage": "Ajouter" + }, + "action.add-location": { + "defaultMessage": "Ajouter localisation" + }, + "action.close": { + "defaultMessage": "Fermer" + }, + "action.delete": { + "defaultMessage": "Supprimer" + }, + "action.disable": { + "defaultMessage": "Désactiver" + }, + "action.download": { + "defaultMessage": "Télécharger" + }, + "action.edit": { + "defaultMessage": "Modifier" + }, + "action.enable": { + "defaultMessage": "Activer" + }, + "action.permissions": { + "defaultMessage": "Permissions" + }, + "action.renew": { + "defaultMessage": "Renouveler" + }, + "action.view-details": { + "defaultMessage": "Voir les Détails" + }, + "auditlogs": { + "defaultMessage": "Journaux d'audit" + }, + "cancel": { + "defaultMessage": "Annuler" + }, + "certificate": { + "defaultMessage": "Certificat" + }, + "certificate.custom-certificate": { + "defaultMessage": "Certificat" + }, + "certificate.custom-certificate-key": { + "defaultMessage": "Clé du Certificat" + }, + "certificate.custom-intermediate": { + "defaultMessage": "Certificat intermédiaire" + }, + "certificate.in-use": { + "defaultMessage": "Utilisé" + }, + "certificate.none.subtitle": { + "defaultMessage": "Aucun certificat assigné" + }, + "certificate.none.subtitle.for-http": { + "defaultMessage": "Cet hôte n'utilisera pas le HTTPS" + }, + "certificate.none.title": { + "defaultMessage": "Aucun" + }, + "certificate.not-in-use": { + "defaultMessage": "Non utilisé" + }, + "certificate.renew": { + "defaultMessage": "Renouveler Certificat" + }, + "certificates": { + "defaultMessage": "Certificats" + }, + "certificates.custom": { + "defaultMessage": "Certificat personnalisé" + }, + "certificates.custom.warning": { + "defaultMessage": "Les fichiers de clé protégés par une passphrase ne sont pas acceptés." + }, + "certificates.dns.credentials": { + "defaultMessage": "Contenu du fichier d'identifiants" + }, + "certificates.dns.credentials-note": { + "defaultMessage": "Ce plugin nécessite un fichier de configuration contenant un jeton d'API ou d'autres informations d'identification pour votre fournisseur." + }, + "certificates.dns.credentials-warning": { + "defaultMessage": "Ces données seront stockées en clair dans la base de données et dans un fichier !" + }, + "certificates.dns.propagation-seconds": { + "defaultMessage": "Propagation Seconds" + }, + "certificates.dns.propagation-seconds-note": { + "defaultMessage": "Laisser vide pour utiliser la valeur par défaut du plugin. Nombre de secondes à attendre pour la propagation DNS." + }, + "certificates.dns.provider": { + "defaultMessage": "Fournisseur DNS" + }, + "certificates.dns.warning": { + "defaultMessage": "Cette section requiert une certaine connaissance de Certbot et de ses plugins DNS. Veuillez consulter la documentation des plugins correspondants." + }, + "certificates.http.reachability-404": { + "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il ne semble pas s'agir de Nginx Proxy Manager. Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." + }, + "certificates.http.reachability-failed-to-check": { + "defaultMessage": "Impossible de vérifier l'accessibilité en raison d'une erreur de communication avec site24x7.com." + }, + "certificates.http.reachability-not-resolved": { + "defaultMessage": "Aucun serveur n'est disponible pour ce domaine. Veuillez vérifier que votre domaine existe et pointe vers l'adresse IP où votre instance NPM est exécutée. Si nécessaire, le port 80 est ouvert dans votre routeur." + }, + "certificates.http.reachability-ok": { + "defaultMessage": "Votre serveur est accessible et la création de certificats devrait être possible." + }, + "certificates.http.reachability-other": { + "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé un code d'état inattendu {code}. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." + }, + "certificates.http.reachability-wrong-data": { + "defaultMessage": "Un serveur a été trouvé sur ce domaine, mais il a renvoyé des données inattendues. S'agit-il du serveur NPM ? Veuillez vérifier que votre domaine pointe bien vers l'adresse IP où votre instance NPM est exécutée." + }, + "certificates.http.test-results": { + "defaultMessage": "Résultats du test" + }, + "certificates.http.warning": { + "defaultMessage": "Ces domaines doivent déjà être configurés pour pointer vers cette installation." + }, + "certificates.request.subtitle": { + "defaultMessage": "avec Let's Encrypt" + }, + "certificates.request.title": { + "defaultMessage": "Demander un nouveau certificat" + }, + "certificates.key-type": { + "defaultMessage": "Type de clé" + }, + "certificates.key-type-description": { + "defaultMessage": "RSA est largement répandu, ECDSA est plus rapide et plus sécurisé, mais peut ne pas être supporté par les systèmes plus anciens" + }, + "certificates.key-type-ecdsa": { + "defaultMessage": "ECDSA 256" + }, + "certificates.key-type-rsa": { + "defaultMessage": "RSA 2048" + }, + "column.access": { + "defaultMessage": "Accès" + }, + "column.authorization": { + "defaultMessage": "Autorisation" + }, + "column.authorizations": { + "defaultMessage": "Autorisations" + }, + "column.custom-locations": { + "defaultMessage": "Emplacement personnalisé" + }, + "column.destination": { + "defaultMessage": "Destination" + }, + "column.details": { + "defaultMessage": "Détails" + }, + "column.email": { + "defaultMessage": "eMail" + }, + "column.event": { + "defaultMessage": "Évènement" + }, + "column.expires": { + "defaultMessage": "Expire" + }, + "column.http-code": { + "defaultMessage": "Code HTTP" + }, + "column.incoming-port": { + "defaultMessage": "Port entrant" + }, + "column.name": { + "defaultMessage": "Nom" + }, + "column.protocol": { + "defaultMessage": "Protocole" + }, + "column.provider": { + "defaultMessage": "Fournisseur" + }, + "column.roles": { + "defaultMessage": "Rôles" + }, + "column.rules": { + "defaultMessage": "Règles" + }, + "column.satisfy": { + "defaultMessage": "Valide" + }, + "column.satisfy-all": { + "defaultMessage": "All" + }, + "column.satisfy-any": { + "defaultMessage": "Any" + }, + "column.scheme": { + "defaultMessage": "Schéma" + }, + "column.source": { + "defaultMessage": "Source" + }, + "column.ssl": { + "defaultMessage": "SSL" + }, + "column.status": { + "defaultMessage": "Statut" + }, + "created-on": { + "defaultMessage": "Créé : {date}" + }, + "dashboard": { + "defaultMessage": "Tableau de bord" + }, + "dead-host": { + "defaultMessage": "Hôte 404" + }, + "dead-hosts": { + "defaultMessage": "Hôtes 404" + }, + "dead-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Hôte 404} other {Hôtes 404}}" + }, + "disabled": { + "defaultMessage": "Désactivé" + }, + "domain-names": { + "defaultMessage": "Noms de domaine" + }, + "domain-names.max": { + "defaultMessage": "{count} noms de domaine au maximum" + }, + "domain-names.placeholder": { + "defaultMessage": "Commencez à écrire pour ajouter un domaine…" + }, + "domain-names.wildcards-not-permitted": { + "defaultMessage": "Les Wildcards ne sont pas permises dans ce cas" + }, + "domain-names.wildcards-not-supported": { + "defaultMessage": "Les Wildcards ne sont pas prises en charge par cette autorité de certification." + }, + "domains.force-ssl": { + "defaultMessage": "Forcer SSL" + }, + "domains.hsts-enabled": { + "defaultMessage": "HSTS activé" + }, + "domains.hsts-subdomains": { + "defaultMessage": "Sous-domaines HSTS" + }, + "domains.http2-support": { + "defaultMessage": "Prise en charge de HTTP/2" + }, + "domains.use-dns": { + "defaultMessage": "Utiliser le challenge DNS" + }, + "email-address": { + "defaultMessage": "Adresse eMail" + }, + "empty-search": { + "defaultMessage": "Aucun résultat trouvé" + }, + "empty-subtitle": { + "defaultMessage": "Pourquoi n'en créez-vous pas un ?" + }, + "enabled": { + "defaultMessage": "Activé" + }, + "error.access.at-least-one": { + "defaultMessage": "Une autorisation ou une règle d'accès est requise." + }, + "error.access.duplicate-usernames": { + "defaultMessage": "Les noms d'utilisateurs autorisés doivent être uniques" + }, + "error.invalid-auth": { + "defaultMessage": "Adresse eMail ou mot de passe invalide" + }, + "error.invalid-domain": { + "defaultMessage": "Domaine invalide : {domain}" + }, + "error.invalid-email": { + "defaultMessage": "Adresse eMail invalide" + }, + "error.max-character-length": { + "defaultMessage": "La longueur maximale est {max} caractère{max, plural, one {} other {s}}" + }, + "error.max-domains": { + "defaultMessage": "Trop de domaines, le maximum est {max}" + }, + "error.maximum": { + "defaultMessage": "Le maximum est {max}" + }, + "error.min-character-length": { + "defaultMessage": "La longueur minimale est {min} caractère{min, plural, one {} other {s}}" + }, + "error.minimum": { + "defaultMessage": "Le minimum est {min}" + }, + "error.passwords-must-match": { + "defaultMessage": "Les mots de passe doivent correspondre" + }, + "error.required": { + "defaultMessage": "Ceci est obligatoire" + }, + "expires.on": { + "defaultMessage": "Expire : {date}" + }, + "footer.github-fork": { + "defaultMessage": "Forkez-moi sur Github" + }, + "host.flags.block-exploits": { + "defaultMessage": "Bloquer les exploits courants" + }, + "host.flags.cache-assets": { + "defaultMessage": "Ressources du cache" + }, + "host.flags.preserve-path": { + "defaultMessage": "Préserver le chemin" + }, + "host.flags.protocols": { + "defaultMessage": "Protocoles" + }, + "host.flags.websockets-upgrade": { + "defaultMessage": "Prise en charge de Websockets" + }, + "host.forward-port": { + "defaultMessage": "Port de redirection" + }, + "host.forward-scheme": { + "defaultMessage": "Schéma" + }, + "hosts": { + "defaultMessage": "Hôtes" + }, + "http-only": { + "defaultMessage": "HTTP uniquement" + }, + "lets-encrypt": { + "defaultMessage": "Let's Encrypt" + }, + "lets-encrypt-via-dns": { + "defaultMessage": "Let's Encrypt via DNS" + }, + "lets-encrypt-via-http": { + "defaultMessage": "Let's Encrypt via HTTP" + }, + "loading": { + "defaultMessage": "Chargement…" + }, + "login.title": { + "defaultMessage": "Connectez-vous à votre compte" + }, + "nginx-config.label": { + "defaultMessage": "Configuration Nginx personnalisée" + }, + "nginx-config.placeholder": { + "defaultMessage": "# Mettez ici votre configuration Nginx personnalisé à vos risques et périls !" + }, + "no-permission-error": { + "defaultMessage": "Vous n'avez pas la permission de voir ce contenu." + }, + "notfound.action": { + "defaultMessage": "Ramenez-moi à l'accueil" + }, + "notfound.content": { + "defaultMessage": "Nous sommes désolés, mais la page que vous cherchez est introuvable" + }, + "notfound.title": { + "defaultMessage": "Oops… Vous avez découvert une page d'erreur" + }, + "notification.error": { + "defaultMessage": "Erreur" + }, + "notification.object-deleted": { + "defaultMessage": "{object} a été supprimé" + }, + "notification.object-disabled": { + "defaultMessage": "{object} a été désactivé" + }, + "notification.object-enabled": { + "defaultMessage": "{object} a été activé" + }, + "notification.object-renewed": { + "defaultMessage": "{object} a été renouvelé" + }, + "notification.object-saved": { + "defaultMessage": "{object} a été enregistré" + }, + "notification.success": { + "defaultMessage": "Réussi" + }, + "object.actions-title": { + "defaultMessage": "{object} #{id}" + }, + "object.add": { + "defaultMessage": "Ajouter {object}" + }, + "object.delete": { + "defaultMessage": "Supprimer {object}" + }, + "object.delete.content": { + "defaultMessage": "Êtes-vous sûr de vouloir supprimer {object} ?" + }, + "object.edit": { + "defaultMessage": "Modifier {object}" + }, + "object.empty": { + "defaultMessage": "Il n'y a aucun {objects}" + }, + "object.event.created": { + "defaultMessage": "{object} créé" + }, + "object.event.deleted": { + "defaultMessage": "{object} supprimé" + }, + "object.event.disabled": { + "defaultMessage": "{object} désactivé" + }, + "object.event.enabled": { + "defaultMessage": "{object} activé" + }, + "object.event.renewed": { + "defaultMessage": "{object} renouvelé" + }, + "object.event.updated": { + "defaultMessage": "{object} mis à jour" + }, + "offline": { + "defaultMessage": "Hors ligne" + }, + "online": { + "defaultMessage": "En ligne" + }, + "options": { + "defaultMessage": "Options" + }, + "password": { + "defaultMessage": "Mot de passe" + }, + "password.generate": { + "defaultMessage": "Générer un mot de passe aléatoire" + }, + "password.hide": { + "defaultMessage": "Masquer le mot de passe" + }, + "password.show": { + "defaultMessage": "Afficher le mot de passe" + }, + "permissions.hidden": { + "defaultMessage": "Masquer" + }, + "permissions.manage": { + "defaultMessage": "Gérer" + }, + "permissions.view": { + "defaultMessage": "Voir uniquement" + }, + "permissions.visibility.all": { + "defaultMessage": "Tous les éléments" + }, + "permissions.visibility.title": { + "defaultMessage": "Éléments visibles" + }, + "permissions.visibility.user": { + "defaultMessage": "Éléments créés uniquement" + }, + "proxy-host": { + "defaultMessage": "Hôte proxy" + }, + "proxy-host.forward-host": { + "defaultMessage": "Nom d'hôte de redirection / IP" + }, + "proxy-hosts": { + "defaultMessage": "Hôtes proxy" + }, + "proxy-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Hôte proxy} other {Hôtes proxy}}" + }, + "public": { + "defaultMessage": "Publique" + }, + "redirection-host": { + "defaultMessage": "Hôte de redirection" + }, + "redirection-host.forward-domain": { + "defaultMessage": "Domaine de redirection" + }, + "redirection-host.forward-http-code": { + "defaultMessage": "Code HTTP" + }, + "redirection-hosts": { + "defaultMessage": "Hôtes de redirection" + }, + "redirection-hosts.count": { + "defaultMessage": "{count} {count, plural, one {Hôte de redirection} other {Hôtes de redirection}}" + }, + "role.admin": { + "defaultMessage": "Administrateur" + }, + "role.standard-user": { + "defaultMessage": "Utilisateur standard" + }, + "save": { + "defaultMessage": "Enregistrer" + }, + "setting": { + "defaultMessage": "Paramètre" + }, + "settings": { + "defaultMessage": "Paramètres" + }, + "settings.default-site": { + "defaultMessage": "Site par défaut" + }, + "settings.default-site.404": { + "defaultMessage": "Page 404" + }, + "settings.default-site.444": { + "defaultMessage": "Aucune réponse (444)" + }, + "settings.default-site.congratulations": { + "defaultMessage": "Page de félicitations" + }, + "settings.default-site.description": { + "defaultMessage": "ce qu'il faut afficher lorsqu'un hôte inconnu est détecté par Nginx" + }, + "settings.default-site.html": { + "defaultMessage": "HTML personnalisé" + }, + "settings.default-site.html.placeholder": { + "defaultMessage": "" + }, + "settings.default-site.redirect": { + "defaultMessage": "Redirection" + }, + "setup.preamble": { + "defaultMessage": "Commencez par créer votre compte administrateur." + }, + "setup.title": { + "defaultMessage": "Bienvenue !" + }, + "sign-in": { + "defaultMessage": "Se connecter" + }, + "ssl-certificate": { + "defaultMessage": "Certificat SSL" + }, + "stream": { + "defaultMessage": "Stream" + }, + "stream.forward-host": { + "defaultMessage": "Hôte destinataire" + }, + "stream.incoming-port": { + "defaultMessage": "Port d'entrée" + }, + "streams": { + "defaultMessage": "Streams" + }, + "streams.count": { + "defaultMessage": "{count} {count, plural, one {Stream} other {Streams}}" + }, + "streams.tcp": { + "defaultMessage": "TCP" + }, + "streams.udp": { + "defaultMessage": "UDP" + }, + "test": { + "defaultMessage": "Test" + }, + "user": { + "defaultMessage": "Utilisateur" + }, + "user.change-password": { + "defaultMessage": "Modifier le mot de passe" + }, + "user.confirm-password": { + "defaultMessage": "Confirmer le mot de passe" + }, + "user.current-password": { + "defaultMessage": "Mot de passe actuel" + }, + "user.edit-profile": { + "defaultMessage": "Modifier le profil" + }, + "user.full-name": { + "defaultMessage": "Nom complet" + }, + "user.login-as": { + "defaultMessage": "Se connecter en tant que {name}" + }, + "user.logout": { + "defaultMessage": "Déconnexion" + }, + "user.new-password": { + "defaultMessage": "Nouveau mot de passe" + }, + "user.nickname": { + "defaultMessage": "Pseudonyme" + }, + "user.set-password": { + "defaultMessage": "Définir le mot de passe" + }, + "user.set-permissions": { + "defaultMessage": "Définir les autorisations pour {name}" + }, + "user.switch-dark": { + "defaultMessage": "Passer au mode Sombre" + }, + "user.switch-light": { + "defaultMessage": "Passer au mode Lumineux" + }, + "username": { + "defaultMessage": "Nom d'utilisateur" + }, + "users": { + "defaultMessage": "Utilisateurs" + } } From 4a9ad2baf7d7c44e3dee3ad97f03765e28d9c820 Mon Sep 17 00:00:00 2001 From: Nishant <5211104+roundone@users.noreply.github.com> Date: Fri, 6 Mar 2026 19:37:51 +0530 Subject: [PATCH 03/19] docs: add selfhosting.sh guide to third-party list --- docs/src/third-party/index.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/src/third-party/index.md b/docs/src/third-party/index.md index cd54b45b..dac77ee9 100644 --- a/docs/src/third-party/index.md +++ b/docs/src/third-party/index.md @@ -14,7 +14,8 @@ Known integrations: - [Proxmox Scripts](https://github.com/ej52/proxmox-scripts/tree/main/apps/nginx-proxy-manager) - [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/scripts?id=nginxproxymanager) - [nginxproxymanagerGraf](https://github.com/ma-karai/nginxproxymanagerGraf) +- [selfhosting.sh Nginx Proxy Manager Guide](https://selfhosting.sh/apps/nginx-proxy-manager/) - Complete Docker Compose setup guide with SSL configuration, access lists, and proxy host management. If you would like your integration of NPM listed, please open a -[Github issue](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) +[Github issue](https://github.com/NginxProxyManager/nginx-proxy-manager/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=) \ No newline at end of file From 9e4c92a066d8e5f537284635c1fdbbf0e20dd5f7 Mon Sep 17 00:00:00 2001 From: Terry Git Cracken Date: Sat, 14 Mar 2026 20:08:49 -0500 Subject: [PATCH 04/19] docs: add NPM Auth Gateway to third-party integrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add NPM Auth Gateway — a companion app that adds user-level access control with auto IP whitelisting on top of NPM's access list system. NPM remains authoritative. The gateway only reads/writes through NPM's REST API. If the gateway goes down, all existing IP whitelists persist in NPM. Running in production managing 90+ proxy hosts. --- docs/src/third-party/index.md | 1 + docs/src/third-party/npm-auth-gateway.md | 83 ++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 docs/src/third-party/npm-auth-gateway.md diff --git a/docs/src/third-party/index.md b/docs/src/third-party/index.md index cd54b45b..60768b5a 100644 --- a/docs/src/third-party/index.md +++ b/docs/src/third-party/index.md @@ -14,6 +14,7 @@ Known integrations: - [Proxmox Scripts](https://github.com/ej52/proxmox-scripts/tree/main/apps/nginx-proxy-manager) - [Proxmox VE Helper-Scripts](https://community-scripts.github.io/ProxmoxVE/scripts?id=nginxproxymanager) - [nginxproxymanagerGraf](https://github.com/ma-karai/nginxproxymanagerGraf) +- [NPM Auth Gateway](https://github.com/Mark0025/npm-auth-gateway) — User-level access control with auto IP whitelisting via auth providers. [Details](/third-party/npm-auth-gateway) If you would like your integration of NPM listed, please open a diff --git a/docs/src/third-party/npm-auth-gateway.md b/docs/src/third-party/npm-auth-gateway.md new file mode 100644 index 00000000..7dce9327 --- /dev/null +++ b/docs/src/third-party/npm-auth-gateway.md @@ -0,0 +1,83 @@ +--- +outline: deep +--- + +# NPM Auth Gateway + +User-level access control for Nginx Proxy Manager with auto IP whitelisting. + +**Repository:** [github.com/Mark0025/npm-auth-gateway](https://github.com/Mark0025/npm-auth-gateway) + +## What It Does + +NPM Auth Gateway is a companion app that adds user management on top of NPM's access list system. Instead of manually adding IPs to access lists, users log in through an auth provider and their IP is automatically whitelisted on the access lists they've been assigned to. + +NPM remains fully in control — the gateway only reads and writes through NPM's REST API. All access enforcement stays in NPM's nginx config. + +## The Problem It Solves + +- **Manual IP management** — every time a user needs access, an admin manually adds their IP to an access list +- **IP changes** — mobile users, VPNs, and travel mean IPs change constantly +- **No user visibility** — access lists contain IPs, but there's no record of who each IP belongs to +- **Scaling** — managing 10+ users across multiple access lists gets tedious + +## How It Works + +``` +Browser → NPM (SSL) → Auth Gateway → Auth Provider + ↓ + NPM REST API (:81) + auto-add IP to access lists +``` + +1. Admin creates a user by email +2. Admin assigns access — a table of proxy hosts with checkboxes showing which hosts each access list protects +3. User logs in → IP detected → automatically added to their assigned NPM access lists +4. User's IP changes → they log in again → new IP auto-added +5. Admin revokes access → user's IPs removed from all access lists + +## Key Features + +- **Auto IP whitelisting** on login +- **Per-host access control** with checkboxes (not abstract groups) +- **Admin/user roles** — admin sees everything, users see only their assigned hosts +- **Personalized dashboard** — users see their services as clickable cards +- **Login logging** with IP history per user +- **One-click revoke** — removes user's IPs from all access lists +- **Searchable tables** for proxy hosts and users +- **Survives gateway failure** — NPM keeps enforcing existing whitelists + +## Architecture + +| Responsibility | Who Handles It | +|---|---| +| SSL termination | **NPM** | +| Proxy host configuration | **NPM** | +| Access list enforcement | **NPM** | +| IP whitelisting | **NPM** | +| User identity | **Auth Provider** | +| User → access list mapping | **Gateway** | +| Auto IP whitelisting | **Gateway** | + +**No database required.** NPM stores all proxy/ACL config. User metadata lives in the auth provider. Zero state duplication. + +## NPM API Endpoints Used + +| Endpoint | Purpose | +|---|---| +| `POST /api/tokens` | Authentication | +| `GET /api/nginx/proxy-hosts` | List proxy hosts | +| `GET /api/nginx/access-lists` | List access lists | +| `PUT /api/nginx/access-lists/:id` | Update access list IPs | +| `POST /api/nginx/access-lists` | Create access list | +| `GET /api/nginx/certificates` | List SSL certificates | + +## Setup + +See the [repository README](https://github.com/Mark0025/npm-auth-gateway) for Docker deployment instructions. + +## Tech Stack + +Next.js / React / TypeScript / Docker + +The auth provider is swappable — the proof of concept uses Clerk, but any OIDC provider works (Auth0, Keycloak, Authentik, etc.). From 0242187db0ca1dc9ecfe647208d8b991e8cc68dc Mon Sep 17 00:00:00 2001 From: jc21 Date: Thu, 14 May 2026 14:16:00 +1000 Subject: [PATCH 05/19] Add message for available update in French locale --- frontend/src/locale/src/fr.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/locale/src/fr.json b/frontend/src/locale/src/fr.json index f3756127..5a2d86a5 100644 --- a/frontend/src/locale/src/fr.json +++ b/frontend/src/locale/src/fr.json @@ -605,6 +605,9 @@ "test": { "defaultMessage": "Test" }, + "update-available": { + "defaultMessage": "Mise à jour disponible : {latestVersion}" + }, "user": { "defaultMessage": "Utilisateur" }, From 52c32b473d32ac4951ce5c2d2b3ee0de38966f2c Mon Sep 17 00:00:00 2001 From: Matthew Kilpatrick <16194494+Matthew-Kilpatrick@users.noreply.github.com> Date: Fri, 15 May 2026 18:55:46 +0000 Subject: [PATCH 06/19] fix: omit "Access rules" directives if no rules configured When an access list was associated with a template which had users (items) but no rules (clients), a `deny all` directive was inserted to the config. This resulted in all requests, including those with valid credentials, being rejected due to the lack of any `allow` directive. This commit wraps the access rule configuration inside of an if block, so the `deny all;` directive is only present when at least one rule is configured. --- backend/templates/_access.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/templates/_access.conf b/backend/templates/_access.conf index 4f388545..81983f3a 100644 --- a/backend/templates/_access.conf +++ b/backend/templates/_access.conf @@ -10,11 +10,13 @@ {% endif %} + {% if access_list.clients.length > 0 %} # Access Rules: {{ access_list.clients | size }} total {% for client in access_list.clients %} {{client | nginxAccessRule}} {% endfor %} deny all; + {% endif %} # Access checks must... {% if access_list.satisfy_any == 1 or access_list.satisfy_any == true %} From 6008a666dd884b66779031c28d446d9dc4962d3d Mon Sep 17 00:00:00 2001 From: DOLBAEB Date: Sat, 16 May 2026 14:18:48 +0200 Subject: [PATCH 07/19] Adds host info to proxy host delete confirmation modal --- .../pages/Nginx/ProxyHosts/TableWrapper.tsx | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx b/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx index 68af43e1..5d6602e2 100644 --- a/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx +++ b/frontend/src/pages/Nginx/ProxyHosts/TableWrapper.tsx @@ -99,14 +99,27 @@ export default function TableWrapper() { isFiltered={!!search} isFetching={isFetching} onEdit={(id: number) => showProxyHostModal(id)} - onDelete={(id: number) => + onDelete={(id: number) => { + const host = data?.find((h) => h.id === id); showDeleteConfirmModal({ title: , onConfirm: () => handleDelete(id), invalidations: [["proxy-hosts"], ["proxy-host", id]], - children: , - }) - } + children: ( + <> + + {host?.domainNames?.length ? ( +
{host.domainNames.join(", ")}
+ ) : null} + {host?.forwardHost ? ( +
+ ({host.forwardScheme}://{host.forwardHost}:{host.forwardPort}) +
+ ) : null} + + ), + }); + }} onDisableToggle={handleDisableToggle} onNew={() => showProxyHostModal("new")} /> From ef0ec0b188c31699fd86aae0975b170a0d0dc5bc Mon Sep 17 00:00:00 2001 From: Reinaldo Ferro Date: Sun, 17 May 2026 22:16:48 +0200 Subject: [PATCH 08/19] Add Hostinger DNS plugin configuration --- backend/certbot/dns-plugins.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/certbot/dns-plugins.json b/backend/certbot/dns-plugins.json index 0588f985..7f474512 100644 --- a/backend/certbot/dns-plugins.json +++ b/backend/certbot/dns-plugins.json @@ -335,6 +335,14 @@ "package_name": "certbot-dns-hetzner-cloud", "version": "~=1.0.4" }, + "hostinger": { + "credentials": "dns_hostinger_api_token = 0123456789abcdef0123456789abcdef", + "dependencies": "", + "full_plugin_name": "dns-hostinger", + "name": "Hostinger.com", + "package_name": "certbot-dns-hostinger", + "version": "~=0.1.5" + }, "hostingnl": { "credentials": "dns_hostingnl_api_key = 0123456789abcdef0123456789abcdef", "dependencies": "", From 1e22574000d8893e66066ac689d8d8d8c444547d Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Mon, 18 May 2026 15:04:27 +1000 Subject: [PATCH 09/19] Fix nulls showing in certificate rows --- backend/models/certificate.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/backend/models/certificate.js b/backend/models/certificate.js index ad6e0a65..857c067e 100644 --- a/backend/models/certificate.js +++ b/backend/models/certificate.js @@ -15,6 +15,16 @@ Model.knex(db()); const boolFields = ["is_deleted"]; +const cleanDomainNames = (domainNames) => { + // Sort domain_names + if (typeof domainNames !== "undefined") { + const newDomainNames = domainNames.filter((name) => name !== null); + newDomainNames.sort(); + return newDomainNames; + } + return []; +}; + class Certificate extends Model { $beforeInsert() { this.created_on = now(); @@ -26,25 +36,17 @@ class Certificate extends Model { } // Default for domain_names - if (typeof this.domain_names === "undefined") { - this.domain_names = []; - } + this.domain_names = cleanDomainNames(this.domain_names); // Default for meta if (typeof this.meta === "undefined") { this.meta = {}; } - - this.domain_names.sort(); } $beforeUpdate() { this.modified_on = now(); - - // Sort domain_names - if (typeof this.domain_names !== "undefined") { - this.domain_names.sort(); - } + this.domain_names = cleanDomainNames(this.domain_names); } $parseDatabaseJson(json) { From 84886383a7c0f2b45b197b5fb78df321f445d4ff Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Mon, 18 May 2026 15:21:55 +1000 Subject: [PATCH 10/19] Fix certificates getting null domain names whgen no cn exists --- backend/internal/certificate.js | 2 +- backend/models/certificate.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 59c13bf6..e37100e6 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -614,7 +614,7 @@ const internalCertificate = { const certificate = await internalCertificate.update(access, { id: data.id, expires_on: moment(validations.certificate.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"), - domain_names: [validations.certificate.cn], + domain_names: validations.certificate.cn ? [validations.certificate.cn] : [], meta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later }); diff --git a/backend/models/certificate.js b/backend/models/certificate.js index 857c067e..9a349f30 100644 --- a/backend/models/certificate.js +++ b/backend/models/certificate.js @@ -18,7 +18,7 @@ const boolFields = ["is_deleted"]; const cleanDomainNames = (domainNames) => { // Sort domain_names if (typeof domainNames !== "undefined") { - const newDomainNames = domainNames.filter((name) => name !== null); + const newDomainNames = domainNames.filter((name) => name != null); newDomainNames.sort(); return newDomainNames; } From 7330d0441c68ce60f51bab31d188d5801dc2a1df Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Mon, 18 May 2026 15:34:29 +1000 Subject: [PATCH 11/19] Loose validation on certificate domain names --- backend/schema/common.json | 6 ++---- backend/schema/components/certificate-object.json | 10 +--------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/backend/schema/common.json b/backend/schema/common.json index 00b06e00..e56c9f70 100644 --- a/backend/schema/common.json +++ b/backend/schema/common.json @@ -77,14 +77,12 @@ "example": 3 }, "domain_names": { - "description": "Domain Names separated by a comma", + "description": "Domain Names array", "type": "array", - "minItems": 1, - "maxItems": 100, "uniqueItems": true, "items": { "type": "string", - "pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$" + "minLength": 1 }, "example": ["example.com", "www.example.com"] }, diff --git a/backend/schema/components/certificate-object.json b/backend/schema/components/certificate-object.json index 80cd92be..4ef666b4 100644 --- a/backend/schema/components/certificate-object.json +++ b/backend/schema/components/certificate-object.json @@ -25,15 +25,7 @@ "example": "My Custom Cert" }, "domain_names": { - "description": "Domain Names separated by a comma", - "type": "array", - "maxItems": 100, - "uniqueItems": true, - "items": { - "type": "string", - "pattern": "^[^&| @!#%^();:/\\\\}{=+?<>,~`'\"]+$" - }, - "example": ["example.com", "www.example.com"] + "$ref": "../common.json#/properties/domain_names" }, "expires_on": { "description": "Date and time of expiration", From 2bf9e9b21355b23cd5d89bfdb487e7bc02e457e0 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Mon, 18 May 2026 15:52:52 +1000 Subject: [PATCH 12/19] Support different cert info output in new version of debian --- backend/internal/certificate.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index e37100e6..a93bf857 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -686,10 +686,18 @@ const internalCertificate = { // Examples: // subject=CN = *.jc21.com // subject=CN = something.example.com + // CN=something.example.com const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim; const match = regex.exec(result); if (match && typeof match[1] !== "undefined") { certData.cn = match[1]; + } else { + // CN is likely formatted differently + const regex11 = /CN=(\S+)/gim; + const match11 = regex11.exec(result); + if (match11 && typeof match11[1] !== "undefined") { + certData.cn = match11[1]; + } } const result2 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-issuer", "-noout"]); From 8b256c33e9131d7d6d4c1f8cec32560217306599 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Mon, 18 May 2026 15:55:54 +1000 Subject: [PATCH 13/19] Sorted fr locale --- frontend/src/locale/src/fr.json | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/frontend/src/locale/src/fr.json b/frontend/src/locale/src/fr.json index cdceb79d..0911eedc 100644 --- a/frontend/src/locale/src/fr.json +++ b/frontend/src/locale/src/fr.json @@ -231,7 +231,7 @@ "defaultMessage": "Type de clé" }, "certificates.key-type-description": { - "defaultMessage": "RSA est largement compatible, ECDSA est plus rapide et plus sécurisé mais peut ne pas être pris en charge par les anciens systèmes" + "defaultMessage": "RSA est largement répandu, ECDSA est plus rapide et plus sécurisé, mais peut ne pas être supporté par les systèmes plus anciens" }, "certificates.key-type-ecdsa": { "defaultMessage": "ECDSA 256" @@ -245,18 +245,6 @@ "certificates.request.title": { "defaultMessage": "Demander un nouveau certificat" }, - "certificates.key-type": { - "defaultMessage": "Type de clé" - }, - "certificates.key-type-description": { - "defaultMessage": "RSA est largement répandu, ECDSA est plus rapide et plus sécurisé, mais peut ne pas être supporté par les systèmes plus anciens" - }, - "certificates.key-type-ecdsa": { - "defaultMessage": "ECDSA 256" - }, - "certificates.key-type-rsa": { - "defaultMessage": "RSA 2048" - }, "column.access": { "defaultMessage": "Accès" }, From 586dfd36a93150a65ace3b8596c11ecad3196ece Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Mon, 18 May 2026 16:12:02 +1000 Subject: [PATCH 14/19] Fix openssl3 formatting of subject cn --- backend/internal/certificate.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index a93bf857..82a47b92 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -683,21 +683,15 @@ const internalCertificate = { try { const result = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-subject", "-noout"]); + console.log("openssl result: ", result); // Examples: // subject=CN = *.jc21.com // subject=CN = something.example.com - // CN=something.example.com - const regex = /(?:subject=)?[^=]+=\s+(\S+)/gim; + // subject=CN=*.jc21.com + const regex = /(?:subject=)?[^=]+=\s*(\S+)/gim; const match = regex.exec(result); if (match && typeof match[1] !== "undefined") { - certData.cn = match[1]; - } else { - // CN is likely formatted differently - const regex11 = /CN=(\S+)/gim; - const match11 = regex11.exec(result); - if (match11 && typeof match11[1] !== "undefined") { - certData.cn = match11[1]; - } + certData.cn = match[1].trim(); } const result2 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-issuer", "-noout"]); From ee1f7ba551c245db25b35dc354f18f948e9629ca Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Mon, 18 May 2026 16:55:41 +1000 Subject: [PATCH 15/19] Fall back to error code 500 when not set in error object --- backend/app.js | 2 +- backend/internal/certificate.js | 317 +++++++++++++++++++++++--------- 2 files changed, 233 insertions(+), 86 deletions(-) diff --git a/backend/app.js b/backend/app.js index 3039bbb8..b1f04766 100644 --- a/backend/app.js +++ b/backend/app.js @@ -62,7 +62,7 @@ app.use("/", mainRoutes); app.use((err, req, res, _) => { const payload = { error: { - code: err.status, + code: err.status || 500, message: err.public ? err.message : "Internal Error", }, }; diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 82a47b92..0c2e546e 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -1,9 +1,9 @@ import fs from "node:fs"; import https from "node:https"; -import path from "path"; import { ZipArchive } from "archiver"; import _ from "lodash"; import moment from "moment"; +import path from "path"; import { ProxyAgent } from "proxy-agent"; import tempWrite from "temp-write"; import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" }; @@ -29,7 +29,11 @@ const omissions = () => { }; const internalCertificate = { - allowedSslFiles: ["certificate", "certificate_key", "intermediate_certificate"], + allowedSslFiles: [ + "certificate", + "certificate_key", + "intermediate_certificate", + ], intervalTimeout: 1000 * 60 * 60, // 1 hour interval: null, intervalProcessing: false, @@ -56,7 +60,10 @@ const internalCertificate = { ); const expirationThreshold = moment() - .add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1]) + .add( + internalCertificate.renewBeforeExpirationBy[0], + internalCertificate.renewBeforeExpirationBy[1], + ) .format("YYYY-MM-DD HH:mm:ss"); // Fetch all the letsencrypt certs from the db that will expire within the configured threshold @@ -137,12 +144,18 @@ const internalCertificate = { // 6. Re-instate previously disabled hosts // 1. Find out any hosts that are using any of the hostnames in this cert - const inUseResult = await internalHost.getHostsWithDomains(certificate.domain_names); + const inUseResult = await internalHost.getHostsWithDomains( + certificate.domain_names, + ); // 2. Disable them in nginx temporarily await internalCertificate.disableInUseHosts(inUseResult); - const user = await userModel.query().where("is_deleted", 0).andWhere("id", data.owner_user_id).first(); + const user = await userModel + .query() + .where("is_deleted", 0) + .andWhere("id", data.owner_user_id) + .first(); if (!user?.email) { throw new error.ValidationError( "A valid email address must be set on your user account to use Let's Encrypt", @@ -154,7 +167,10 @@ const internalCertificate = { try { await internalNginx.reload(); // 4. Request cert - await internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate, user.email); + await internalCertificate.requestLetsEncryptSslWithDnsChallenge( + certificate, + user.email, + ); await internalNginx.reload(); // 6. Re-instate previously disabled hosts await internalCertificate.enableInUseHosts(inUseResult); @@ -171,7 +187,10 @@ const internalCertificate = { await internalNginx.reload(); setTimeout(() => {}, 5000); // 4. Request cert - await internalCertificate.requestLetsEncryptSsl(certificate, user.email); + await internalCertificate.requestLetsEncryptSsl( + certificate, + user.email, + ); // 5. Remove LE config await internalNginx.deleteLetsEncryptRequestConfig(certificate); await internalNginx.reload(); @@ -195,7 +214,9 @@ const internalCertificate = { const savedRow = await certificateModel .query() .patchAndFetchById(certificate.id, { - expires_on: moment(certInfo.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"), + expires_on: moment(certInfo.dates.to, "X").format( + "YYYY-MM-DD HH:mm:ss", + ), }) .then(utils.omitRow(omissions())); @@ -204,7 +225,11 @@ const internalCertificate = { letsencrypt_certificate: certInfo, }); - await internalCertificate.addCreatedAuditLog(access, certificate.id, savedRow); + await internalCertificate.addCreatedAuditLog( + access, + certificate.id, + savedRow, + ); return savedRow; } catch (err) { @@ -222,7 +247,11 @@ const internalCertificate = { data.meta = _.assign({}, data.meta || {}, certificate.meta); // Add to audit log - await internalCertificate.addCreatedAuditLog(access, certificate.id, utils.omitRow(omissions())(data)); + await internalCertificate.addCreatedAuditLog( + access, + certificate.id, + utils.omitRow(omissions())(data), + ); return utils.omitRow(omissions())(certificate); }, @@ -321,7 +350,9 @@ const internalCertificate = { row.proxy_hosts = utils.omitRows(["is_deleted"])(row.proxy_hosts); } if (typeof row.redirection_hosts !== "undefined") { - row.redirection_hosts = utils.omitRows(["is_deleted"])(row.redirection_hosts); + row.redirection_hosts = utils.omitRows(["is_deleted"])( + row.redirection_hosts, + ); } if (typeof row.dead_hosts !== "undefined") { row.dead_hosts = utils.omitRows(["is_deleted"])(row.dead_hosts); @@ -344,7 +375,9 @@ const internalCertificate = { if (certificate.provider === "letsencrypt") { const zipDirectory = internalCertificate.getLiveCertPath(data.id); if (!fs.existsSync(zipDirectory)) { - throw new error.ItemNotFoundError(`Certificate ${certificate.nice_name} does not exists`); + throw new error.ItemNotFoundError( + `Certificate ${certificate.nice_name} does not exists`, + ); } const certFiles = fs @@ -361,7 +394,9 @@ const internalCertificate = { fileName: opName, }; } - throw new error.ValidationError("Only Let'sEncrypt certificates can be downloaded"); + throw new error.ValidationError( + "Only Let'sEncrypt certificates can be downloaded", + ); }, /** @@ -470,7 +505,10 @@ const internalCertificate = { * @returns {Promise} */ getCount: async (userId, visibility) => { - const query = certificateModel.query().count("id as count").where("is_deleted", 0); + const query = certificateModel + .query() + .count("id as count") + .where("is_deleted", 0); if (visibility !== "all") { query.andWhere("owner_user_id", userId); @@ -518,13 +556,17 @@ const internalCertificate = { }); }).then(() => { return new Promise((resolve, reject) => { - fs.writeFile(`${dir}/privkey.pem`, certificate.meta.certificate_key, (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); + fs.writeFile( + `${dir}/privkey.pem`, + certificate.meta.certificate_key, + (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }, + ); }); }); }, @@ -597,7 +639,9 @@ const internalCertificate = { upload: async (access, data) => { const row = await internalCertificate.get(access, { id: data.id }); if (row.provider !== "other") { - throw new error.ValidationError("Cannot upload certificates for this type of provider"); + throw new error.ValidationError( + "Cannot upload certificates for this type of provider", + ); } const validations = await internalCertificate.validate(data); @@ -613,8 +657,12 @@ const internalCertificate = { const certificate = await internalCertificate.update(access, { id: data.id, - expires_on: moment(validations.certificate.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"), - domain_names: validations.certificate.cn ? [validations.certificate.cn] : [], + expires_on: moment(validations.certificate.dates.to, "X").format( + "YYYY-MM-DD HH:mm:ss", + ), + domain_names: validations.certificate.cn + ? [validations.certificate.cn] + : [], meta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later }); @@ -638,7 +686,9 @@ const internalCertificate = { }, 10000); try { - const result = await utils.exec(`openssl pkey -in ${filepath} -check -noout 2>&1 `); + const result = await utils.exec( + `openssl pkey -in ${filepath} -check -noout 2>&1 `, + ); clearTimeout(failTimeout); if (!result.toLowerCase().includes("key is valid")) { throw new error.ValidationError(`Result Validation Error: ${result}`); @@ -648,7 +698,10 @@ const internalCertificate = { } catch (err) { clearTimeout(failTimeout); fs.unlinkSync(filepath); - throw new error.ValidationError(`Certificate Key is not valid (${err.message})`, err); + throw new error.ValidationError( + `Certificate Key is not valid (${err.message})`, + err, + ); } }, @@ -662,7 +715,10 @@ const internalCertificate = { getCertificateInfo: async (certificate, throwExpired) => { const filepath = await tempWrite(certificate); try { - const certData = await internalCertificate.getCertificateInfoFromFile(filepath, throwExpired); + const certData = await internalCertificate.getCertificateInfoFromFile( + filepath, + throwExpired, + ); fs.unlinkSync(filepath); return certData; } catch (err) { @@ -682,7 +738,13 @@ const internalCertificate = { const certData = {}; try { - const result = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-subject", "-noout"]); + const result = await utils.execFile("openssl", [ + "x509", + "-in", + certificateFile, + "-subject", + "-noout", + ]); console.log("openssl result: ", result); // Examples: // subject=CN = *.jc21.com @@ -694,7 +756,13 @@ const internalCertificate = { certData.cn = match[1].trim(); } - const result2 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-issuer", "-noout"]); + const result2 = await utils.execFile("openssl", [ + "x509", + "-in", + certificateFile, + "-issuer", + "-noout", + ]); // Examples: // issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3 // issuer=C = US, O = Let's Encrypt, CN = E5 @@ -705,7 +773,13 @@ const internalCertificate = { certData.issuer = match2[1]; } - const result3 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-dates", "-noout"]); + const result3 = await utils.execFile("openssl", [ + "x509", + "-in", + certificateFile, + "-dates", + "-noout", + ]); // notBefore=Jul 14 04:04:29 2018 GMT // notAfter=Oct 12 04:04:29 2018 GMT let validFrom = null; @@ -717,7 +791,10 @@ const internalCertificate = { const match = regex.exec(str.trim()); if (match && typeof match[2] !== "undefined") { - const date = Number.parseInt(moment(match[2], "MMM DD HH:mm:ss YYYY z").format("X"), 10); + const date = Number.parseInt( + moment(match[2], "MMM DD HH:mm:ss YYYY z").format("X"), + 10, + ); if (match[1].toLowerCase() === "notbefore") { validFrom = date; @@ -729,10 +806,15 @@ const internalCertificate = { }); if (!validFrom || !validTo) { - throw new error.ValidationError(`Could not determine dates from certificate: ${result}`); + throw new error.ValidationError( + `Could not determine dates from certificate: ${result}`, + ); } - if (throw_expired && validTo < Number.parseInt(moment().format("X"), 10)) { + if ( + throw_expired && + validTo < Number.parseInt(moment().format("X"), 10) + ) { throw new error.ValidationError("Certificate has expired"); } @@ -743,7 +825,10 @@ const internalCertificate = { return certData; } catch (err) { - throw new error.ValidationError(`Certificate is not valid (${err.message})`, err); + throw new error.ValidationError( + `Certificate is not valid (${err.message})`, + err, + ); } }, @@ -781,6 +866,7 @@ const internalCertificate = { const args = [ "certonly", + "-n", // non-interactive "--config", letsencryptConfig, "--work-dir", @@ -829,13 +915,18 @@ const internalCertificate = { const credentialsLocation = `/etc/letsencrypt/credentials/credentials-${certificate.id}`; fs.mkdirSync("/etc/letsencrypt/credentials", { recursive: true }); - fs.writeFileSync(credentialsLocation, certificate.meta.dns_provider_credentials, { mode: 0o600 }); + fs.writeFileSync( + credentialsLocation, + certificate.meta.dns_provider_credentials, + { mode: 0o600 }, + ); // Whether the plugin has a ---credentials argument const hasConfigArg = certificate.meta.dns_provider !== "route53"; const args = [ "certonly", + "-n", // non-interactive "--config", letsencryptConfig, "--work-dir", @@ -856,7 +947,10 @@ const internalCertificate = { ]; if (hasConfigArg) { - args.push(`--${dnsPlugin.full_plugin_name}-credentials`, credentialsLocation); + args.push( + `--${dnsPlugin.full_plugin_name}-credentials`, + credentialsLocation, + ); } if (certificate.meta.propagation_seconds !== undefined) { args.push( @@ -870,7 +964,10 @@ const internalCertificate = { args.push("--key-type", certificate.meta.key_type); } - const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); + const adds = internalCertificate.getAdditionalCertbotArgs( + certificate.id, + certificate.meta.dns_provider, + ); args.push(...adds.args); logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`); @@ -906,9 +1003,13 @@ const internalCertificate = { `${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`, ); - const updatedCertificate = await certificateModel.query().patchAndFetchById(certificate.id, { - expires_on: moment(certInfo.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"), - }); + const updatedCertificate = await certificateModel + .query() + .patchAndFetchById(certificate.id, { + expires_on: moment(certInfo.dates.to, "X").format( + "YYYY-MM-DD HH:mm:ss", + ), + }); // Add to audit log await internalAuditLog.add(access, { @@ -921,7 +1022,9 @@ const internalCertificate = { return updatedCertificate; } - throw new error.ValidationError("Only Let'sEncrypt certificates can be renewed"); + throw new error.ValidationError( + "Only Let'sEncrypt certificates can be renewed", + ); }, /** @@ -955,7 +1058,10 @@ const internalCertificate = { args.push("--key-type", certificate.meta.key_type); } - const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); + const adds = internalCertificate.getAdditionalCertbotArgs( + certificate.id, + certificate.meta.dns_provider, + ); args.push(...adds.args); logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`); @@ -1001,7 +1107,10 @@ const internalCertificate = { args.push("--key-type", certificate.meta.key_type); } - const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); + const adds = internalCertificate.getAdditionalCertbotArgs( + certificate.id, + certificate.meta.dns_provider, + ); args.push(...adds.args); logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`); @@ -1041,7 +1150,9 @@ const internalCertificate = { try { const result = await utils.execFile(certbotCommand, args, adds.opts); - await utils.exec(`rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`); + await utils.exec( + `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`, + ); logger.info(result); return result; } catch (err) { @@ -1058,7 +1169,10 @@ const internalCertificate = { */ hasLetsEncryptSslCerts: (certificate) => { const letsencryptPath = internalCertificate.getLiveCertPath(certificate.id); - return fs.existsSync(`${letsencryptPath}/fullchain.pem`) && fs.existsSync(`${letsencryptPath}/privkey.pem`); + return ( + fs.existsSync(`${letsencryptPath}/fullchain.pem`) && + fs.existsSync(`${letsencryptPath}/privkey.pem`) + ); }, /** @@ -1072,15 +1186,24 @@ const internalCertificate = { disableInUseHosts: async (inUseResult) => { if (inUseResult?.total_count) { if (inUseResult?.proxy_hosts.length) { - await internalNginx.bulkDeleteConfigs("proxy_host", inUseResult.proxy_hosts); + await internalNginx.bulkDeleteConfigs( + "proxy_host", + inUseResult.proxy_hosts, + ); } if (inUseResult?.redirection_hosts.length) { - await internalNginx.bulkDeleteConfigs("redirection_host", inUseResult.redirection_hosts); + await internalNginx.bulkDeleteConfigs( + "redirection_host", + inUseResult.redirection_hosts, + ); } if (inUseResult?.dead_hosts.length) { - await internalNginx.bulkDeleteConfigs("dead_host", inUseResult.dead_hosts); + await internalNginx.bulkDeleteConfigs( + "dead_host", + inUseResult.dead_hosts, + ); } } }, @@ -1096,15 +1219,24 @@ const internalCertificate = { enableInUseHosts: async (inUseResult) => { if (inUseResult.total_count) { if (inUseResult.proxy_hosts.length) { - await internalNginx.bulkGenerateConfigs("proxy_host", inUseResult.proxy_hosts); + await internalNginx.bulkGenerateConfigs( + "proxy_host", + inUseResult.proxy_hosts, + ); } if (inUseResult.redirection_hosts.length) { - await internalNginx.bulkGenerateConfigs("redirection_host", inUseResult.redirection_hosts); + await internalNginx.bulkGenerateConfigs( + "redirection_host", + inUseResult.redirection_hosts, + ); } if (inUseResult.dead_hosts.length) { - await internalNginx.bulkGenerateConfigs("dead_host", inUseResult.dead_hosts); + await internalNginx.bulkGenerateConfigs( + "dead_host", + inUseResult.dead_hosts, + ); } } }, @@ -1119,7 +1251,8 @@ const internalCertificate = { await access.can("certificates:list"); // Create a test challenge file - const testChallengeDir = "/data/letsencrypt-acme-challenge/.well-known/acme-challenge"; + const testChallengeDir = + "/data/letsencrypt-acme-challenge/.well-known/acme-challenge"; const testChallengeFile = `${testChallengeDir}/test-challenge`; fs.mkdirSync(testChallengeDir, { recursive: true }); fs.writeFileSync(testChallengeFile, "Success", { encoding: "utf8" }); @@ -1151,38 +1284,42 @@ const internalCertificate = { }; const result = await new Promise((resolve) => { - const req = https.request("https://www.site24x7.com/tools/restapi-tester", options, (res) => { - let responseBody = ""; + const req = https.request( + "https://www.site24x7.com/tools/restapi-tester", + options, + (res) => { + let responseBody = ""; - res.on("data", (chunk) => { - responseBody = responseBody + chunk; - }); + res.on("data", (chunk) => { + responseBody = responseBody + chunk; + }); - res.on("end", () => { - try { - const parsedBody = JSON.parse(`${responseBody}`); - if (res.statusCode !== 200) { - logger.warn( - `Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned: ${parsedBody.message}`, - ); + res.on("end", () => { + try { + const parsedBody = JSON.parse(`${responseBody}`); + if (res.statusCode !== 200) { + logger.warn( + `Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned: ${parsedBody.message}`, + ); + resolve(undefined); + } else { + resolve(parsedBody); + } + } catch (err) { + if (res.statusCode !== 200) { + logger.warn( + `Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned`, + ); + } else { + logger.warn( + `Failed to test HTTP challenge for domain ${domain} because response failed to be parsed: ${err.message}`, + ); + } resolve(undefined); - } else { - resolve(parsedBody); } - } catch (err) { - if (res.statusCode !== 200) { - logger.warn( - `Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned`, - ); - } else { - logger.warn( - `Failed to test HTTP challenge for domain ${domain} because response failed to be parsed: ${err.message}`, - ); - } - resolve(undefined); - } - }); - }); + }); + }, + ); // Make sure to write the request body. req.write(formBody); @@ -1203,7 +1340,10 @@ const internalCertificate = { ); return `other:${result.error.msg}`; } - if (`${result.responsecode}` === "200" && result.htmlresponse === "Success") { + if ( + `${result.responsecode}` === "200" && + result.htmlresponse === "Success" + ) { // Server exists and has responded with the correct data return "ok"; } @@ -1217,19 +1357,26 @@ const internalCertificate = { } if (`${result.responsecode}` === "404") { // Server exists but responded with a 404 - logger.info(`HTTP challenge test failed for domain ${domain} because code 404 was returned`); + logger.info( + `HTTP challenge test failed for domain ${domain} because code 404 was returned`, + ); return "404"; } if ( `${result.responsecode}` === "0" || - (typeof result.reason === "string" && result.reason.toLowerCase() === "host unavailable") + (typeof result.reason === "string" && + result.reason.toLowerCase() === "host unavailable") ) { // Server does not exist at domain - logger.info(`HTTP challenge test failed for domain ${domain} the host was not found`); + logger.info( + `HTTP challenge test failed for domain ${domain} the host was not found`, + ); return "no-host"; } // Other errors - logger.info(`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`); + logger.info( + `HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`, + ); return `other:${result.responsecode}`; }, From f53bf88f4dfec6732893dc97a544fc6b4967a044 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Tue, 19 May 2026 08:04:49 +1000 Subject: [PATCH 16/19] Update cypress docker version, generate custom certs each time --- backend/internal/certificate.js | 317 +++++------------- test/cypress/Dockerfile | 2 +- test/cypress/e2e/api/Certificates.cy.js | 1 + test/cypress/e2e/api/Ldap.cy.js | 2 +- test/cypress/e2e/api/OAuth.cy.js | 2 +- .../cypress/fixtures/test.example.com-key.pem | 28 -- test/cypress/fixtures/test.example.com.pem | 26 -- test/cypress/support/commands.mjs | 25 +- test/package.json | 1 + test/yarn.lock | 18 + 10 files changed, 133 insertions(+), 289 deletions(-) delete mode 100644 test/cypress/fixtures/test.example.com-key.pem delete mode 100644 test/cypress/fixtures/test.example.com.pem diff --git a/backend/internal/certificate.js b/backend/internal/certificate.js index 0c2e546e..6498422c 100644 --- a/backend/internal/certificate.js +++ b/backend/internal/certificate.js @@ -1,9 +1,9 @@ import fs from "node:fs"; import https from "node:https"; +import path from "path"; import { ZipArchive } from "archiver"; import _ from "lodash"; import moment from "moment"; -import path from "path"; import { ProxyAgent } from "proxy-agent"; import tempWrite from "temp-write"; import dnsPlugins from "../certbot/dns-plugins.json" with { type: "json" }; @@ -29,11 +29,7 @@ const omissions = () => { }; const internalCertificate = { - allowedSslFiles: [ - "certificate", - "certificate_key", - "intermediate_certificate", - ], + allowedSslFiles: ["certificate", "certificate_key", "intermediate_certificate"], intervalTimeout: 1000 * 60 * 60, // 1 hour interval: null, intervalProcessing: false, @@ -60,10 +56,7 @@ const internalCertificate = { ); const expirationThreshold = moment() - .add( - internalCertificate.renewBeforeExpirationBy[0], - internalCertificate.renewBeforeExpirationBy[1], - ) + .add(internalCertificate.renewBeforeExpirationBy[0], internalCertificate.renewBeforeExpirationBy[1]) .format("YYYY-MM-DD HH:mm:ss"); // Fetch all the letsencrypt certs from the db that will expire within the configured threshold @@ -144,18 +137,12 @@ const internalCertificate = { // 6. Re-instate previously disabled hosts // 1. Find out any hosts that are using any of the hostnames in this cert - const inUseResult = await internalHost.getHostsWithDomains( - certificate.domain_names, - ); + const inUseResult = await internalHost.getHostsWithDomains(certificate.domain_names); // 2. Disable them in nginx temporarily await internalCertificate.disableInUseHosts(inUseResult); - const user = await userModel - .query() - .where("is_deleted", 0) - .andWhere("id", data.owner_user_id) - .first(); + const user = await userModel.query().where("is_deleted", 0).andWhere("id", data.owner_user_id).first(); if (!user?.email) { throw new error.ValidationError( "A valid email address must be set on your user account to use Let's Encrypt", @@ -167,10 +154,7 @@ const internalCertificate = { try { await internalNginx.reload(); // 4. Request cert - await internalCertificate.requestLetsEncryptSslWithDnsChallenge( - certificate, - user.email, - ); + await internalCertificate.requestLetsEncryptSslWithDnsChallenge(certificate, user.email); await internalNginx.reload(); // 6. Re-instate previously disabled hosts await internalCertificate.enableInUseHosts(inUseResult); @@ -187,10 +171,7 @@ const internalCertificate = { await internalNginx.reload(); setTimeout(() => {}, 5000); // 4. Request cert - await internalCertificate.requestLetsEncryptSsl( - certificate, - user.email, - ); + await internalCertificate.requestLetsEncryptSsl(certificate, user.email); // 5. Remove LE config await internalNginx.deleteLetsEncryptRequestConfig(certificate); await internalNginx.reload(); @@ -214,9 +195,7 @@ const internalCertificate = { const savedRow = await certificateModel .query() .patchAndFetchById(certificate.id, { - expires_on: moment(certInfo.dates.to, "X").format( - "YYYY-MM-DD HH:mm:ss", - ), + expires_on: moment(certInfo.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"), }) .then(utils.omitRow(omissions())); @@ -225,11 +204,7 @@ const internalCertificate = { letsencrypt_certificate: certInfo, }); - await internalCertificate.addCreatedAuditLog( - access, - certificate.id, - savedRow, - ); + await internalCertificate.addCreatedAuditLog(access, certificate.id, savedRow); return savedRow; } catch (err) { @@ -247,11 +222,7 @@ const internalCertificate = { data.meta = _.assign({}, data.meta || {}, certificate.meta); // Add to audit log - await internalCertificate.addCreatedAuditLog( - access, - certificate.id, - utils.omitRow(omissions())(data), - ); + await internalCertificate.addCreatedAuditLog(access, certificate.id, utils.omitRow(omissions())(data)); return utils.omitRow(omissions())(certificate); }, @@ -350,9 +321,7 @@ const internalCertificate = { row.proxy_hosts = utils.omitRows(["is_deleted"])(row.proxy_hosts); } if (typeof row.redirection_hosts !== "undefined") { - row.redirection_hosts = utils.omitRows(["is_deleted"])( - row.redirection_hosts, - ); + row.redirection_hosts = utils.omitRows(["is_deleted"])(row.redirection_hosts); } if (typeof row.dead_hosts !== "undefined") { row.dead_hosts = utils.omitRows(["is_deleted"])(row.dead_hosts); @@ -375,9 +344,7 @@ const internalCertificate = { if (certificate.provider === "letsencrypt") { const zipDirectory = internalCertificate.getLiveCertPath(data.id); if (!fs.existsSync(zipDirectory)) { - throw new error.ItemNotFoundError( - `Certificate ${certificate.nice_name} does not exists`, - ); + throw new error.ItemNotFoundError(`Certificate ${certificate.nice_name} does not exists`); } const certFiles = fs @@ -394,9 +361,7 @@ const internalCertificate = { fileName: opName, }; } - throw new error.ValidationError( - "Only Let'sEncrypt certificates can be downloaded", - ); + throw new error.ValidationError("Only Let'sEncrypt certificates can be downloaded"); }, /** @@ -505,10 +470,7 @@ const internalCertificate = { * @returns {Promise} */ getCount: async (userId, visibility) => { - const query = certificateModel - .query() - .count("id as count") - .where("is_deleted", 0); + const query = certificateModel.query().count("id as count").where("is_deleted", 0); if (visibility !== "all") { query.andWhere("owner_user_id", userId); @@ -556,17 +518,13 @@ const internalCertificate = { }); }).then(() => { return new Promise((resolve, reject) => { - fs.writeFile( - `${dir}/privkey.pem`, - certificate.meta.certificate_key, - (err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }, - ); + fs.writeFile(`${dir}/privkey.pem`, certificate.meta.certificate_key, (err) => { + if (err) { + reject(err); + } else { + resolve(); + } + }); }); }); }, @@ -639,9 +597,7 @@ const internalCertificate = { upload: async (access, data) => { const row = await internalCertificate.get(access, { id: data.id }); if (row.provider !== "other") { - throw new error.ValidationError( - "Cannot upload certificates for this type of provider", - ); + throw new error.ValidationError("Cannot upload certificates for this type of provider"); } const validations = await internalCertificate.validate(data); @@ -657,12 +613,8 @@ const internalCertificate = { const certificate = await internalCertificate.update(access, { id: data.id, - expires_on: moment(validations.certificate.dates.to, "X").format( - "YYYY-MM-DD HH:mm:ss", - ), - domain_names: validations.certificate.cn - ? [validations.certificate.cn] - : [], + expires_on: moment(validations.certificate.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"), + domain_names: validations.certificate.cn ? [validations.certificate.cn] : [], meta: _.clone(row.meta), // Prevent the update method from changing this value that we'll use later }); @@ -686,9 +638,7 @@ const internalCertificate = { }, 10000); try { - const result = await utils.exec( - `openssl pkey -in ${filepath} -check -noout 2>&1 `, - ); + const result = await utils.exec(`openssl pkey -in ${filepath} -check -noout 2>&1 `); clearTimeout(failTimeout); if (!result.toLowerCase().includes("key is valid")) { throw new error.ValidationError(`Result Validation Error: ${result}`); @@ -698,10 +648,7 @@ const internalCertificate = { } catch (err) { clearTimeout(failTimeout); fs.unlinkSync(filepath); - throw new error.ValidationError( - `Certificate Key is not valid (${err.message})`, - err, - ); + throw new error.ValidationError(`Certificate Key is not valid (${err.message})`, err); } }, @@ -715,10 +662,7 @@ const internalCertificate = { getCertificateInfo: async (certificate, throwExpired) => { const filepath = await tempWrite(certificate); try { - const certData = await internalCertificate.getCertificateInfoFromFile( - filepath, - throwExpired, - ); + const certData = await internalCertificate.getCertificateInfoFromFile(filepath, throwExpired); fs.unlinkSync(filepath); return certData; } catch (err) { @@ -738,14 +682,8 @@ const internalCertificate = { const certData = {}; try { - const result = await utils.execFile("openssl", [ - "x509", - "-in", - certificateFile, - "-subject", - "-noout", - ]); - console.log("openssl result: ", result); + const result = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-subject", "-noout"]); + // Examples: // subject=CN = *.jc21.com // subject=CN = something.example.com @@ -756,13 +694,7 @@ const internalCertificate = { certData.cn = match[1].trim(); } - const result2 = await utils.execFile("openssl", [ - "x509", - "-in", - certificateFile, - "-issuer", - "-noout", - ]); + const result2 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-issuer", "-noout"]); // Examples: // issuer=C = US, O = Let's Encrypt, CN = Let's Encrypt Authority X3 // issuer=C = US, O = Let's Encrypt, CN = E5 @@ -773,13 +705,7 @@ const internalCertificate = { certData.issuer = match2[1]; } - const result3 = await utils.execFile("openssl", [ - "x509", - "-in", - certificateFile, - "-dates", - "-noout", - ]); + const result3 = await utils.execFile("openssl", ["x509", "-in", certificateFile, "-dates", "-noout"]); // notBefore=Jul 14 04:04:29 2018 GMT // notAfter=Oct 12 04:04:29 2018 GMT let validFrom = null; @@ -791,10 +717,7 @@ const internalCertificate = { const match = regex.exec(str.trim()); if (match && typeof match[2] !== "undefined") { - const date = Number.parseInt( - moment(match[2], "MMM DD HH:mm:ss YYYY z").format("X"), - 10, - ); + const date = Number.parseInt(moment(match[2], "MMM DD HH:mm:ss YYYY z").format("X"), 10); if (match[1].toLowerCase() === "notbefore") { validFrom = date; @@ -806,15 +729,10 @@ const internalCertificate = { }); if (!validFrom || !validTo) { - throw new error.ValidationError( - `Could not determine dates from certificate: ${result}`, - ); + throw new error.ValidationError(`Could not determine dates from certificate: ${result}`); } - if ( - throw_expired && - validTo < Number.parseInt(moment().format("X"), 10) - ) { + if (throw_expired && validTo < Number.parseInt(moment().format("X"), 10)) { throw new error.ValidationError("Certificate has expired"); } @@ -825,10 +743,7 @@ const internalCertificate = { return certData; } catch (err) { - throw new error.ValidationError( - `Certificate is not valid (${err.message})`, - err, - ); + throw new error.ValidationError(`Certificate is not valid (${err.message})`, err); } }, @@ -915,11 +830,7 @@ const internalCertificate = { const credentialsLocation = `/etc/letsencrypt/credentials/credentials-${certificate.id}`; fs.mkdirSync("/etc/letsencrypt/credentials", { recursive: true }); - fs.writeFileSync( - credentialsLocation, - certificate.meta.dns_provider_credentials, - { mode: 0o600 }, - ); + fs.writeFileSync(credentialsLocation, certificate.meta.dns_provider_credentials, { mode: 0o600 }); // Whether the plugin has a ---credentials argument const hasConfigArg = certificate.meta.dns_provider !== "route53"; @@ -947,10 +858,7 @@ const internalCertificate = { ]; if (hasConfigArg) { - args.push( - `--${dnsPlugin.full_plugin_name}-credentials`, - credentialsLocation, - ); + args.push(`--${dnsPlugin.full_plugin_name}-credentials`, credentialsLocation); } if (certificate.meta.propagation_seconds !== undefined) { args.push( @@ -964,10 +872,7 @@ const internalCertificate = { args.push("--key-type", certificate.meta.key_type); } - const adds = internalCertificate.getAdditionalCertbotArgs( - certificate.id, - certificate.meta.dns_provider, - ); + const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); args.push(...adds.args); logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`); @@ -1003,13 +908,9 @@ const internalCertificate = { `${internalCertificate.getLiveCertPath(certificate.id)}/fullchain.pem`, ); - const updatedCertificate = await certificateModel - .query() - .patchAndFetchById(certificate.id, { - expires_on: moment(certInfo.dates.to, "X").format( - "YYYY-MM-DD HH:mm:ss", - ), - }); + const updatedCertificate = await certificateModel.query().patchAndFetchById(certificate.id, { + expires_on: moment(certInfo.dates.to, "X").format("YYYY-MM-DD HH:mm:ss"), + }); // Add to audit log await internalAuditLog.add(access, { @@ -1022,9 +923,7 @@ const internalCertificate = { return updatedCertificate; } - throw new error.ValidationError( - "Only Let'sEncrypt certificates can be renewed", - ); + throw new error.ValidationError("Only Let'sEncrypt certificates can be renewed"); }, /** @@ -1058,10 +957,7 @@ const internalCertificate = { args.push("--key-type", certificate.meta.key_type); } - const adds = internalCertificate.getAdditionalCertbotArgs( - certificate.id, - certificate.meta.dns_provider, - ); + const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); args.push(...adds.args); logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`); @@ -1107,10 +1003,7 @@ const internalCertificate = { args.push("--key-type", certificate.meta.key_type); } - const adds = internalCertificate.getAdditionalCertbotArgs( - certificate.id, - certificate.meta.dns_provider, - ); + const adds = internalCertificate.getAdditionalCertbotArgs(certificate.id, certificate.meta.dns_provider); args.push(...adds.args); logger.info(`Command: ${certbotCommand} ${args ? args.join(" ") : ""}`); @@ -1150,9 +1043,7 @@ const internalCertificate = { try { const result = await utils.execFile(certbotCommand, args, adds.opts); - await utils.exec( - `rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`, - ); + await utils.exec(`rm -f '/etc/letsencrypt/credentials/credentials-${certificate.id}' || true`); logger.info(result); return result; } catch (err) { @@ -1169,10 +1060,7 @@ const internalCertificate = { */ hasLetsEncryptSslCerts: (certificate) => { const letsencryptPath = internalCertificate.getLiveCertPath(certificate.id); - return ( - fs.existsSync(`${letsencryptPath}/fullchain.pem`) && - fs.existsSync(`${letsencryptPath}/privkey.pem`) - ); + return fs.existsSync(`${letsencryptPath}/fullchain.pem`) && fs.existsSync(`${letsencryptPath}/privkey.pem`); }, /** @@ -1186,24 +1074,15 @@ const internalCertificate = { disableInUseHosts: async (inUseResult) => { if (inUseResult?.total_count) { if (inUseResult?.proxy_hosts.length) { - await internalNginx.bulkDeleteConfigs( - "proxy_host", - inUseResult.proxy_hosts, - ); + await internalNginx.bulkDeleteConfigs("proxy_host", inUseResult.proxy_hosts); } if (inUseResult?.redirection_hosts.length) { - await internalNginx.bulkDeleteConfigs( - "redirection_host", - inUseResult.redirection_hosts, - ); + await internalNginx.bulkDeleteConfigs("redirection_host", inUseResult.redirection_hosts); } if (inUseResult?.dead_hosts.length) { - await internalNginx.bulkDeleteConfigs( - "dead_host", - inUseResult.dead_hosts, - ); + await internalNginx.bulkDeleteConfigs("dead_host", inUseResult.dead_hosts); } } }, @@ -1219,24 +1098,15 @@ const internalCertificate = { enableInUseHosts: async (inUseResult) => { if (inUseResult.total_count) { if (inUseResult.proxy_hosts.length) { - await internalNginx.bulkGenerateConfigs( - "proxy_host", - inUseResult.proxy_hosts, - ); + await internalNginx.bulkGenerateConfigs("proxy_host", inUseResult.proxy_hosts); } if (inUseResult.redirection_hosts.length) { - await internalNginx.bulkGenerateConfigs( - "redirection_host", - inUseResult.redirection_hosts, - ); + await internalNginx.bulkGenerateConfigs("redirection_host", inUseResult.redirection_hosts); } if (inUseResult.dead_hosts.length) { - await internalNginx.bulkGenerateConfigs( - "dead_host", - inUseResult.dead_hosts, - ); + await internalNginx.bulkGenerateConfigs("dead_host", inUseResult.dead_hosts); } } }, @@ -1251,8 +1121,7 @@ const internalCertificate = { await access.can("certificates:list"); // Create a test challenge file - const testChallengeDir = - "/data/letsencrypt-acme-challenge/.well-known/acme-challenge"; + const testChallengeDir = "/data/letsencrypt-acme-challenge/.well-known/acme-challenge"; const testChallengeFile = `${testChallengeDir}/test-challenge`; fs.mkdirSync(testChallengeDir, { recursive: true }); fs.writeFileSync(testChallengeFile, "Success", { encoding: "utf8" }); @@ -1284,42 +1153,38 @@ const internalCertificate = { }; const result = await new Promise((resolve) => { - const req = https.request( - "https://www.site24x7.com/tools/restapi-tester", - options, - (res) => { - let responseBody = ""; + const req = https.request("https://www.site24x7.com/tools/restapi-tester", options, (res) => { + let responseBody = ""; - res.on("data", (chunk) => { - responseBody = responseBody + chunk; - }); + res.on("data", (chunk) => { + responseBody = responseBody + chunk; + }); - res.on("end", () => { - try { - const parsedBody = JSON.parse(`${responseBody}`); - if (res.statusCode !== 200) { - logger.warn( - `Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned: ${parsedBody.message}`, - ); - resolve(undefined); - } else { - resolve(parsedBody); - } - } catch (err) { - if (res.statusCode !== 200) { - logger.warn( - `Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned`, - ); - } else { - logger.warn( - `Failed to test HTTP challenge for domain ${domain} because response failed to be parsed: ${err.message}`, - ); - } + res.on("end", () => { + try { + const parsedBody = JSON.parse(`${responseBody}`); + if (res.statusCode !== 200) { + logger.warn( + `Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned: ${parsedBody.message}`, + ); resolve(undefined); + } else { + resolve(parsedBody); } - }); - }, - ); + } catch (err) { + if (res.statusCode !== 200) { + logger.warn( + `Failed to test HTTP challenge for domain ${domain} because HTTP status code ${res.statusCode} was returned`, + ); + } else { + logger.warn( + `Failed to test HTTP challenge for domain ${domain} because response failed to be parsed: ${err.message}`, + ); + } + resolve(undefined); + } + }); + }); // Make sure to write the request body. req.write(formBody); @@ -1340,10 +1205,7 @@ const internalCertificate = { ); return `other:${result.error.msg}`; } - if ( - `${result.responsecode}` === "200" && - result.htmlresponse === "Success" - ) { + if (`${result.responsecode}` === "200" && result.htmlresponse === "Success") { // Server exists and has responded with the correct data return "ok"; } @@ -1357,26 +1219,19 @@ const internalCertificate = { } if (`${result.responsecode}` === "404") { // Server exists but responded with a 404 - logger.info( - `HTTP challenge test failed for domain ${domain} because code 404 was returned`, - ); + logger.info(`HTTP challenge test failed for domain ${domain} because code 404 was returned`); return "404"; } if ( `${result.responsecode}` === "0" || - (typeof result.reason === "string" && - result.reason.toLowerCase() === "host unavailable") + (typeof result.reason === "string" && result.reason.toLowerCase() === "host unavailable") ) { // Server does not exist at domain - logger.info( - `HTTP challenge test failed for domain ${domain} the host was not found`, - ); + logger.info(`HTTP challenge test failed for domain ${domain} the host was not found`); return "no-host"; } // Other errors - logger.info( - `HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`, - ); + logger.info(`HTTP challenge test failed for domain ${domain} because code ${result.responsecode} was returned`); return `other:${result.responsecode}`; }, diff --git a/test/cypress/Dockerfile b/test/cypress/Dockerfile index bd821085..cf071baf 100644 --- a/test/cypress/Dockerfile +++ b/test/cypress/Dockerfile @@ -1,4 +1,4 @@ -FROM cypress/included:15.11.0 +FROM cypress/included:15.15.0 # Disable Cypress CLI colors ENV FORCE_COLOR=0 diff --git a/test/cypress/e2e/api/Certificates.cy.js b/test/cypress/e2e/api/Certificates.cy.js index 00da38b2..ce4200fb 100644 --- a/test/cypress/e2e/api/Certificates.cy.js +++ b/test/cypress/e2e/api/Certificates.cy.js @@ -5,6 +5,7 @@ describe('Certificates endpoints', () => { let certID; before(() => { + cy.createCustomCerts(); cy.resetUsers(); cy.getToken().then((tok) => { token = tok; diff --git a/test/cypress/e2e/api/Ldap.cy.js b/test/cypress/e2e/api/Ldap.cy.js index 64e17730..e1a99976 100644 --- a/test/cypress/e2e/api/Ldap.cy.js +++ b/test/cypress/e2e/api/Ldap.cy.js @@ -2,7 +2,7 @@ describe('LDAP with Authentik', () => { let _token; - if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') { + if (cy.env('skipStackCheck') === 'true' || cy.env('stack') === 'postgres') { before(() => { cy.resetUsers(); diff --git a/test/cypress/e2e/api/OAuth.cy.js b/test/cypress/e2e/api/OAuth.cy.js index c5c819f9..55e78f52 100644 --- a/test/cypress/e2e/api/OAuth.cy.js +++ b/test/cypress/e2e/api/OAuth.cy.js @@ -2,7 +2,7 @@ describe('OAuth with Authentik', () => { let _token; - if (Cypress.env('skipStackCheck') === 'true' || Cypress.env('stack') === 'postgres') { + if (cy.env('skipStackCheck') === 'true' || cy.env('stack') === 'postgres') { before(() => { cy.getToken().then((tok) => { diff --git a/test/cypress/fixtures/test.example.com-key.pem b/test/cypress/fixtures/test.example.com-key.pem deleted file mode 100644 index 307cdc30..00000000 --- a/test/cypress/fixtures/test.example.com-key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1n9j9C5Bes1nd -qACDckERauxXVNKCnUlUM1buGBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2w -rbmvZvLuPmXePOKbIKS+XXh+2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHge -Yz6Cv/Si2/LJPCh/CoBfM4hUQJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQ -oxRAHiOR9081Xn1WeoKr7kVBIa5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7Z -Eo+nS8Wr/4QWicatIWZXpVaEOPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79X -zGONeH1PAgMBAAECggEAANb3Wtwl07pCjRrMvc7WbC0xYIn82yu8/g2qtjkYUJcU -ia5lQbYN7RGCS85Oc/tkq48xQEG5JQWNH8b918jDEMTrFab0aUEyYcru1q9L8PL6 -YHaNgZSrMrDcHcS8h0QOXNRJT5jeGkiHJaTR0irvB526tqF3knbK9yW22KTfycUe -a0Z9voKn5xRk1DCbHi/nk2EpT7xnjeQeLFaTIRXbS68omkr4YGhwWm5OizoyEGZu -W0Zum5BkQyMr6kor3wdxOTG97ske2rcyvvHi+ErnwL0xBv0qY0Dhe8DpuXpDezqw -o72yY8h31Fu84i7sAj24YuE5Df8DozItFXQpkgbQ6QKBgQDPrufhvIFm2S/MzBdW -H8JxY7CJlJPyxOvc1NIl9RczQGAQR90kx52cgIcuIGEG6/wJ/xnGfMmW40F0DnQ+ -N+oLgB9SFxeLkRb7s9Z/8N3uIN8JJFYcerEOiRQeN2BXEEWJ7bUThNtsVrAcKoUh -ELsDmnHW/3V+GKwhd0vpk842+wKBgQDf4PGLG9PTE5tlAoyHFodJRd2RhTJQkwsU -MDNjLJ+KecLv+Nl+QiJhoflG1ccqtSFlBSCG067CDQ5LV0xm3mLJ7pfJoMgjcq31 -qjEmX4Ls91GuVOPtbwst3yFKjsHaSoKB5fBvWRcKFpBUezM7Qcw2JP3+dQT+bQIq -cMTkRWDSvQKBgQDOdCQFDjxg/lR7NQOZ1PaZe61aBz5P3pxNqa7ClvMaOsuEQ7w9 -vMYcdtRq8TsjA2JImbSI0TIg8gb2FQxPcYwTJKl+FICOeIwtaSg5hTtJZpnxX5LO -utTaC0DZjNkTk5RdOdWA8tihyUdGqKoxJY2TVmwGe2rUEDjFB++J4inkEwKBgB6V -g0nmtkxanFrzOzFlMXwgEEHF+Xaqb9QFNa/xs6XeNnREAapO7JV75Cr6H2hFMFe1 -mJjyqCgYUoCWX3iaHtLJRnEkBtNY4kzyQB6m46LtsnnnXO/dwKA2oDyoPfFNRoDq -YatEd3JIXNU9s2T/+x7WdOBjKhh72dTkbPFmTPDdAoGAU6rlPBevqOFdObYxdPq8 -EQWu44xqky3Mf5sBpOwtu6rqCYuziLiN7K4sjN5GD5mb1cEU+oS92ZiNcUQ7MFXk -8yTYZ7U0VcXyAcpYreWwE8thmb0BohJBr+Mp3wLTx32x0HKdO6vpUa0d35LUTUmM -RrKmPK/msHKK/sVHiL+NFqo= ------END PRIVATE KEY----- diff --git a/test/cypress/fixtures/test.example.com.pem b/test/cypress/fixtures/test.example.com.pem deleted file mode 100644 index 16340cdf..00000000 --- a/test/cypress/fixtures/test.example.com.pem +++ /dev/null @@ -1,26 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEYDCCAsigAwIBAgIRAPoSC0hvitb26ODMlsH6YbowDQYJKoZIhvcNAQELBQAw -gZExHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEzMDEGA1UECwwqamN1 -cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJub3cpMTowOAYDVQQD -DDFta2NlcnQgamN1cm5vd0BKYW1pZXMtTGFwdG9wLmxvY2FsIChKYW1pZSBDdXJu -b3cpMB4XDTI0MTAwOTA3MjIxN1oXDTI3MDEwOTA3MjIxN1owXjEnMCUGA1UEChMe -bWtjZXJ0IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTMwMQYDVQQLDCpqY3Vybm93 -QEphbWllcy1MYXB0b3AubG9jYWwgKEphbWllIEN1cm5vdykwggEiMA0GCSqGSIb3 -DQEBAQUAA4IBDwAwggEKAoIBAQC1n9j9C5Bes1ndqACDckERauxXVNKCnUlUM1bu -GBx1xc+j2e2Ar23wUJJuWBY18VfT8yqfqVDktO2wrbmvZvLuPmXePOKbIKS+XXh+ -2NG9L5bDG9rwGFCRXnbQj+GWCdMfzx14+CR1IHgeYz6Cv/Si2/LJPCh/CoBfM4hU -QJON3lxAWrWBpdbZnKYMrxuPBRfW9OuzTbCVXToQoxRAHiOR9081Xn1WeoKr7kVB -Ia5UphlvWXa12w1YmUwJu7YndnJGIavLWeNCVc7ZEo+nS8Wr/4QWicatIWZXpVaE -OPhRoeplQDxNWg5b/Q26rYoVd7PrCmRs7sVcH79XzGONeH1PAgMBAAGjZTBjMA4G -A1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBSB -/vfmBUd4W7CvyEMl7YpMVQs8vTAbBgNVHREEFDASghB0ZXN0LmV4YW1wbGUuY29t -MA0GCSqGSIb3DQEBCwUAA4IBgQASwON/jPAHzcARSenY0ZGY1m5OVTYoQ/JWH0oy -l8SyFCQFEXt7UHDD/eTtLT0vMyc190nP57P8lTnZGf7hSinZz1B1d6V4cmzxpk0s -VXZT+irL6bJVJoMBHRpllKAhGULIo33baTrWFKA0oBuWx4AevSWKcLW5j87kEawn -ATCuMQ1I3ifR1mSlB7X8fb+vF+571q0NGuB3a42j6rdtXJ6SmH4+9B4qO0sfHDNt -IImpLCH/tycDpcYrGSCn1QrekFG1bSEh+Bb9i8rqMDSDsYrTFPZTuOQ3EtjGni9u -m+rEP3OyJg+md8c+0LVP7/UU4QWWnw3/Wolo5kSCxE8vNTFqi4GhVbdLnUtcIdTV -XxuR6cKyW87Snj1a0nG76ZLclt/akxDhtzqeV60BO0p8pmiev8frp+E94wFNYCmp -1cr3CnMEGRaficLSDFC6EBENzlZW2BQT6OMIV+g0NBgSyQe39s2zcdEl5+SzDVuw -hp8bJUp/QN7pnOVCDbjTQ+HVMXw= ------END CERTIFICATE----- diff --git a/test/cypress/support/commands.mjs b/test/cypress/support/commands.mjs index 7d9224d0..3f12d0ef 100644 --- a/test/cypress/support/commands.mjs +++ b/test/cypress/support/commands.mjs @@ -10,6 +10,7 @@ // import 'cypress-wait-until'; +import { createCA, createCert } from 'mkcert'; Cypress.Commands.add('randomString', (length) => { let result = ''; @@ -49,7 +50,7 @@ Cypress.Commands.add("validateSwaggerFile", (url, savePath) => { */ Cypress.Commands.add('validateSwaggerSchema', (method, code, path, data) => { cy.task('validateSwaggerSchema', { - file: Cypress.env('swaggerBase'), + file: cy.env('swaggerBase'), endpoint: path, method: method, statusCode: code, @@ -151,3 +152,25 @@ Cypress.Commands.add('waitForCertificateStatus', (token, certID, expected, timeo interval: 5000 }); }); + +// Creates CA files for testing, if they already exist they will be deleted +// and recreated with the same content. This is to ensure that the files exist +// for testing and are in a known state. +Cypress.Commands.add('createCustomCerts', async () => { + const ca = await createCA({ + organization: "NPM CA", + countryCode: "AU", + state: "QLD", + locality: "Brisbane", + validity: 365 + }); + + const cert = await createCert({ + ca: { key: ca.key, cert: ca.cert }, + domains: ["test.example.com"], + validity: 365 + }); + + cy.writeFile(`${config.fixturesFolder}/test.example.com.pem`, cert.cert); + cy.writeFile(`${config.fixturesFolder}/test.example.com-key.pem`, cert.key); +}); diff --git a/test/package.json b/test/package.json index 555bb6c2..b243cc41 100644 --- a/test/package.json +++ b/test/package.json @@ -17,6 +17,7 @@ "eslint-plugin-cypress": "^6.4.1", "form-data": "^4.0.5", "lodash": "^4.18.1", + "mkcert": "^3.2.0", "mocha": "^11.7.5", "mocha-junit-reporter": "^2.2.1" }, diff --git a/test/yarn.lock b/test/yarn.lock index 57e4b473..0d3f0714 100644 --- a/test/yarn.lock +++ b/test/yarn.lock @@ -567,6 +567,11 @@ combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" +commander@^11.0.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" + integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== + commander@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" @@ -1561,6 +1566,14 @@ minizlib@^3.1.0: dependencies: minipass "^7.1.2" +mkcert@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/mkcert/-/mkcert-3.2.0.tgz#6c4d58bbb31dbf76efd24f197d454702b14020f4" + integrity sha512-026Eivq9RoOjOuLJGzbhGwXUAjBxRX11Z7Jbm4/7lqT/Av+XNy9SPrJte6+UpEt7i+W3e/HZYxQqlQcqXZWSzg== + dependencies: + commander "^11.0.0" + node-forge "^1.3.1" + mkdirp@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" @@ -1628,6 +1641,11 @@ node-fetch@^3.3.2: fetch-blob "^3.1.4" formdata-polyfill "^4.0.10" +node-forge@^1.3.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.4.0.tgz#1c7b7d8bdc2d078739f58287d589d903a11b2fc2" + integrity sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ== + npm-run-path@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" From 11955e5dff517a76379ee18c05449226e519cb1d Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Tue, 19 May 2026 08:51:52 +1000 Subject: [PATCH 17/19] Cypress improvements, move mkcert back to command line --- test/cypress/config/ci.mjs | 1 + test/cypress/config/dev.mjs | 1 + test/cypress/e2e/api/Ldap.cy.js | 65 ------------------- test/cypress/e2e/api/OAuth.cy.js | 97 ---------------------------- test/cypress/e2e/api/Streams.cy.js | 10 +-- test/cypress/plugins/index.mjs | 5 ++ test/cypress/support/commands.mjs | 45 ++++++------- test/cypress/support/task.mjs | 100 +++++++++++++++++++++++++++++ test/package.json | 1 - test/yarn.lock | 18 ------ 10 files changed, 133 insertions(+), 210 deletions(-) delete mode 100644 test/cypress/e2e/api/Ldap.cy.js delete mode 100644 test/cypress/e2e/api/OAuth.cy.js create mode 100644 test/cypress/support/task.mjs diff --git a/test/cypress/config/ci.mjs b/test/cypress/config/ci.mjs index 2597c194..ea203a89 100644 --- a/test/cypress/config/ci.mjs +++ b/test/cypress/config/ci.mjs @@ -2,6 +2,7 @@ import { defineConfig } from 'cypress'; import pluginSetup from '../plugins/index.mjs'; export default defineConfig({ + allowCypressEnv: false, requestTimeout: 30000, defaultCommandTimeout: 20000, reporter: "cypress-multi-reporters", diff --git a/test/cypress/config/dev.mjs b/test/cypress/config/dev.mjs index f32d973c..636c3095 100644 --- a/test/cypress/config/dev.mjs +++ b/test/cypress/config/dev.mjs @@ -2,6 +2,7 @@ import { defineConfig } from 'cypress'; import pluginSetup from '../plugins/index.mjs'; export default defineConfig({ + allowCypressEnv: false, requestTimeout: 30000, defaultCommandTimeout: 20000, reporter: "cypress-multi-reporters", diff --git a/test/cypress/e2e/api/Ldap.cy.js b/test/cypress/e2e/api/Ldap.cy.js deleted file mode 100644 index e1a99976..00000000 --- a/test/cypress/e2e/api/Ldap.cy.js +++ /dev/null @@ -1,65 +0,0 @@ -/// - -describe('LDAP with Authentik', () => { - let _token; - if (cy.env('skipStackCheck') === 'true' || cy.env('stack') === 'postgres') { - - before(() => { - cy.resetUsers(); - cy.getToken().then((tok) => { - _token = tok; - - // cy.task('backendApiPut', { - // token: token, - // path: '/api/settings/ldap-auth', - // data: { - // value: { - // host: 'authentik-ldap:3389', - // base_dn: 'ou=users,DC=ldap,DC=goauthentik,DC=io', - // user_dn: 'cn={{USERNAME}},ou=users,DC=ldap,DC=goauthentik,DC=io', - // email_property: 'mail', - // name_property: 'sn', - // self_filter: '(&(cn={{USERNAME}})(ak-active=TRUE))', - // auto_create_user: true - // } - // } - // }).then((data) => { - // cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); - // expect(data.result).to.have.property('id'); - // expect(data.result.id).to.be.greaterThan(0); - // }); - - // cy.task('backendApiPut', { - // token: token, - // path: '/api/settings/auth-methods', - // data: { - // value: [ - // 'local', - // 'ldap' - // ] - // } - // }).then((data) => { - // cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); - // expect(data.result).to.have.property('id'); - // expect(data.result.id).to.be.greaterThan(0); - // }); - }); - }); - - it.skip('Should log in with LDAP', () => { - // cy.task('backendApiPost', { - // token: token, - // path: '/api/auth', - // data: { - // // Authentik LDAP creds: - // type: 'ldap', - // identity: 'cypress', - // secret: 'fqXBfUYqHvYqiwBHWW7f' - // } - // }).then((data) => { - // cy.validateSwaggerSchema('post', 200, '/auth', data); - // expect(data.result).to.have.property('token'); - // }); - }); - } -}); diff --git a/test/cypress/e2e/api/OAuth.cy.js b/test/cypress/e2e/api/OAuth.cy.js deleted file mode 100644 index 55e78f52..00000000 --- a/test/cypress/e2e/api/OAuth.cy.js +++ /dev/null @@ -1,97 +0,0 @@ -/// - -describe('OAuth with Authentik', () => { - let _token; - if (cy.env('skipStackCheck') === 'true' || cy.env('stack') === 'postgres') { - - before(() => { - cy.getToken().then((tok) => { - _token = tok; - - // cy.task('backendApiPut', { - // token: token, - // path: '/api/settings/oauth-auth', - // data: { - // value: { - // client_id: '7iO2AvuUp9JxiSVkCcjiIbQn4mHmUMBj7yU8EjqU', - // client_secret: 'VUMZzaGTrmXJ8PLksyqzyZ6lrtz04VvejFhPMBP9hGZNCMrn2LLBanySs4ta7XGrDr05xexPyZT1XThaf4ubg00WqvHRVvlu4Naa1aMootNmSRx3VAk6RSslUJmGyHzq', - // authorization_url: 'http://authentik:9000/application/o/authorize/', - // resource_url: 'http://authentik:9000/application/o/userinfo/', - // token_url: 'http://authentik:9000/application/o/token/', - // logout_url: 'http://authentik:9000/application/o/npm/end-session/', - // identifier: 'preferred_username', - // scopes: [], - // auto_create_user: true - // } - // } - // }).then((data) => { - // cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); - // expect(data.result).to.have.property('id'); - // expect(data.result.id).to.be.greaterThan(0); - // }); - - // cy.task('backendApiPut', { - // token: token, - // path: '/api/settings/auth-methods', - // data: { - // value: [ - // 'local', - // 'oauth' - // ] - // } - // }).then((data) => { - // cy.validateSwaggerSchema('put', 200, '/settings/{name}', data); - // expect(data.result).to.have.property('id'); - // expect(data.result.id).to.be.greaterThan(0); - // }); - }); - }); - - it.skip('Should log in with OAuth', () => { - // cy.task('backendApiGet', { - // path: '/oauth/login?redirect_base=' + encodeURI(Cypress.config('baseUrl')), - // }).then((data) => { - // expect(data).to.have.property('result'); - - // cy.origin('http://authentik:9000', {args: data.result}, (url) => { - // cy.visit(url); - // cy.get('ak-flow-executor') - // .shadow() - // .find('ak-stage-identification') - // .shadow() - // .find('input[name="uidField"]', { visible: true }) - // .type('cypress'); - - // cy.get('ak-flow-executor') - // .shadow() - // .find('ak-stage-identification') - // .shadow() - // .find('button[type="submit"]', { visible: true }) - // .click(); - - // cy.get('ak-flow-executor') - // .shadow() - // .find('ak-stage-password') - // .shadow() - // .find('input[name="password"]', { visible: true }) - // .type('fqXBfUYqHvYqiwBHWW7f'); - - // cy.get('ak-flow-executor') - // .shadow() - // .find('ak-stage-password') - // .shadow() - // .find('button[type="submit"]', { visible: true }) - // .click(); - // }) - - // // we should be logged in - // cy.get('#root p.chakra-text') - // .first() - // .should('have.text', 'Nginx Proxy Manager'); - - // // logout: - // cy.clearLocalStorage(); - // }); - }); - } -}); diff --git a/test/cypress/e2e/api/Streams.cy.js b/test/cypress/e2e/api/Streams.cy.js index 10809f89..b2c97b44 100644 --- a/test/cypress/e2e/api/Streams.cy.js +++ b/test/cypress/e2e/api/Streams.cy.js @@ -23,11 +23,13 @@ describe('Streams', () => { }); // Create a custom cert pair - cy.exec('mkcert -cert-file=/test/cypress/fixtures/website1.pem -key-file=/test/cypress/fixtures/website1.key.pem website1.example.com').then((result) => { - expect(result.exitCode).to.eq(0); - // Install CA - cy.exec('mkcert -install').then((result) => { + cy.task('getFixturesFolder').then((fixturesFolder) => { + cy.exec(`mkcert -cert-file=${fixturesFolder}/website1.pem -key-file=${fixturesFolder}/website1.key.pem website1.example.com`).then((result) => { expect(result.exitCode).to.eq(0); + // Install CA + cy.exec('mkcert -install').then((result) => { + expect(result.exitCode).to.eq(0); + }); }); }); diff --git a/test/cypress/plugins/index.mjs b/test/cypress/plugins/index.mjs index 1058d633..dc09527d 100644 --- a/test/cypress/plugins/index.mjs +++ b/test/cypress/plugins/index.mjs @@ -22,6 +22,11 @@ export default (on, config) => { return null; }, }); + on('task', { + getFixturesFolder() { + return config.fixturesFolder + }, + }); return config; }; diff --git a/test/cypress/support/commands.mjs b/test/cypress/support/commands.mjs index 3f12d0ef..12db1994 100644 --- a/test/cypress/support/commands.mjs +++ b/test/cypress/support/commands.mjs @@ -10,7 +10,6 @@ // import 'cypress-wait-until'; -import { createCA, createCert } from 'mkcert'; Cypress.Commands.add('randomString', (length) => { let result = ''; @@ -49,14 +48,16 @@ Cypress.Commands.add("validateSwaggerFile", (url, savePath) => { * @param {*} data The API response data to check against the swagger schema */ Cypress.Commands.add('validateSwaggerSchema', (method, code, path, data) => { - cy.task('validateSwaggerSchema', { - file: cy.env('swaggerBase'), - endpoint: path, - method: method, - statusCode: code, - responseSchema: data, - verbose: true - }).should('equal', null); + cy.env(['swaggerBase']).then(({ swaggerBase }) => { + cy.task('validateSwaggerSchema', { + file: swaggerBase, + endpoint: path, + method: method, + statusCode: code, + responseSchema: data, + verbose: true + }).should('equal', null); + }); }); Cypress.Commands.add('createInitialUser', (defaultUser) => { @@ -156,21 +157,15 @@ Cypress.Commands.add('waitForCertificateStatus', (token, certID, expected, timeo // Creates CA files for testing, if they already exist they will be deleted // and recreated with the same content. This is to ensure that the files exist // for testing and are in a known state. -Cypress.Commands.add('createCustomCerts', async () => { - const ca = await createCA({ - organization: "NPM CA", - countryCode: "AU", - state: "QLD", - locality: "Brisbane", - validity: 365 +Cypress.Commands.add('createCustomCerts', () => { + cy.task('getFixturesFolder').then((fixturesFolder) => { + cy.exec(`mkcert -cert-file=${fixturesFolder}/test.example.com.pem -key-file=${fixturesFolder}/test.example.com-key.pem test.example.com`) + .then((result) => { + expect(result.exitCode).to.eq(0); + // Install CA + cy.exec('mkcert -install').then((result) => { + expect(result.exitCode).to.eq(0); + }); + }); }); - - const cert = await createCert({ - ca: { key: ca.key, cert: ca.cert }, - domains: ["test.example.com"], - validity: 365 - }); - - cy.writeFile(`${config.fixturesFolder}/test.example.com.pem`, cert.cert); - cy.writeFile(`${config.fixturesFolder}/test.example.com-key.pem`, cert.key); }); diff --git a/test/cypress/support/task.mjs b/test/cypress/support/task.mjs new file mode 100644 index 00000000..edc5015c --- /dev/null +++ b/test/cypress/support/task.mjs @@ -0,0 +1,100 @@ +import fs from "node:fs"; +import FormData from "form-data"; +import Client from "./client.mjs"; +import logger from "./logger.mjs"; + +export default (config) => { + logger("Client Ready using", config.baseUrl); + + return { + /** + * @param {object} options + * @param {string} options.path API path + * @param {string} [options.token] JWT + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiGet: (options) => { + const api = new Client(config); + api.setToken(options.token); + return api.request("get", options.path, options.returnOnError || false); + }, + + /** + * @param {object} options + * @param {string} options.token JWT + * @param {string} options.path API path + * @param {object} options.data + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiPost: (options) => { + const api = new Client(config); + api.setToken(options.token); + return api.request( + "post", + options.path, + options.returnOnError || false, + options.data, + ); + }, + + /** + * @param {object} options + * @param {string} options.token JWT + * @param {string} options.path API path + * @param {object} options.files + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiPostFiles: (options) => { + const api = new Client(config); + api.setToken(options.token); + + const form = new FormData(); + for (const [key, value] of Object.entries(options.files)) { + form.append( + key, + fs.createReadStream(`${config.fixturesFolder}/${value}`), + ); + } + return api.postForm(options.path, form, options.returnOnError || false); + }, + + /** + * @param {object} options + * @param {string} options.token JWT + * @param {string} options.path API path + * @param {object} options.data + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiPut: (options) => { + const api = new Client(config); + api.setToken(options.token); + return api.request( + "put", + options.path, + options.returnOnError || false, + options.data, + ); + }, + + /** + * @param {object} options + * @param {string} options.token JWT + * @param {string} options.path API path + * @param {bool} [options.returnOnError] If true, will return instead of throwing errors + * @returns {string} + */ + backendApiDelete: (options) => { + const api = new Client(config); + api.setToken(options.token); + return api.request( + "delete", + options.path, + options.returnOnError || false, + ); + }, + }; +}; diff --git a/test/package.json b/test/package.json index b243cc41..555bb6c2 100644 --- a/test/package.json +++ b/test/package.json @@ -17,7 +17,6 @@ "eslint-plugin-cypress": "^6.4.1", "form-data": "^4.0.5", "lodash": "^4.18.1", - "mkcert": "^3.2.0", "mocha": "^11.7.5", "mocha-junit-reporter": "^2.2.1" }, diff --git a/test/yarn.lock b/test/yarn.lock index 0d3f0714..57e4b473 100644 --- a/test/yarn.lock +++ b/test/yarn.lock @@ -567,11 +567,6 @@ combined-stream@^1.0.8, combined-stream@~1.0.6: dependencies: delayed-stream "~1.0.0" -commander@^11.0.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-11.1.0.tgz#62fdce76006a68e5c1ab3314dc92e800eb83d906" - integrity sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ== - commander@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" @@ -1566,14 +1561,6 @@ minizlib@^3.1.0: dependencies: minipass "^7.1.2" -mkcert@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/mkcert/-/mkcert-3.2.0.tgz#6c4d58bbb31dbf76efd24f197d454702b14020f4" - integrity sha512-026Eivq9RoOjOuLJGzbhGwXUAjBxRX11Z7Jbm4/7lqT/Av+XNy9SPrJte6+UpEt7i+W3e/HZYxQqlQcqXZWSzg== - dependencies: - commander "^11.0.0" - node-forge "^1.3.1" - mkdirp@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" @@ -1641,11 +1628,6 @@ node-fetch@^3.3.2: fetch-blob "^3.1.4" formdata-polyfill "^4.0.10" -node-forge@^1.3.1: - version "1.4.0" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.4.0.tgz#1c7b7d8bdc2d078739f58287d589d903a11b2fc2" - integrity sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ== - npm-run-path@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" From dff978a63c5b8998cd7dadbf01e84e55aa4e7164 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Wed, 20 May 2026 07:52:46 +1000 Subject: [PATCH 18/19] Tests: install mkcert before generating --- test/cypress/support/commands.mjs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/test/cypress/support/commands.mjs b/test/cypress/support/commands.mjs index 12db1994..5d753abc 100644 --- a/test/cypress/support/commands.mjs +++ b/test/cypress/support/commands.mjs @@ -159,13 +159,12 @@ Cypress.Commands.add('waitForCertificateStatus', (token, certID, expected, timeo // for testing and are in a known state. Cypress.Commands.add('createCustomCerts', () => { cy.task('getFixturesFolder').then((fixturesFolder) => { - cy.exec(`mkcert -cert-file=${fixturesFolder}/test.example.com.pem -key-file=${fixturesFolder}/test.example.com-key.pem test.example.com`) - .then((result) => { - expect(result.exitCode).to.eq(0); - // Install CA - cy.exec('mkcert -install').then((result) => { + cy.exec('mkcert -install').then((result) => { + expect(result.exitCode).to.eq(0); + cy.exec(`mkcert -cert-file=${fixturesFolder}/test.example.com.pem -key-file=${fixturesFolder}/test.example.com-key.pem test.example.com`) + .then((result) => { expect(result.exitCode).to.eq(0); }); - }); + }); }); }); From c354238c3524fdd0d99985dad52798f0289c9542 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Wed, 20 May 2026 08:05:24 +1000 Subject: [PATCH 19/19] Testing mkcert in test suite --- test/cypress/support/commands.mjs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/test/cypress/support/commands.mjs b/test/cypress/support/commands.mjs index 5d753abc..0001ded8 100644 --- a/test/cypress/support/commands.mjs +++ b/test/cypress/support/commands.mjs @@ -159,12 +159,8 @@ Cypress.Commands.add('waitForCertificateStatus', (token, certID, expected, timeo // for testing and are in a known state. Cypress.Commands.add('createCustomCerts', () => { cy.task('getFixturesFolder').then((fixturesFolder) => { - cy.exec('mkcert -install').then((result) => { - expect(result.exitCode).to.eq(0); - cy.exec(`mkcert -cert-file=${fixturesFolder}/test.example.com.pem -key-file=${fixturesFolder}/test.example.com-key.pem test.example.com`) - .then((result) => { - expect(result.exitCode).to.eq(0); - }); + cy.exec('mkcert -install', {failOnNonZeroExit: false}).then(() => { + cy.exec(`mkcert -cert-file=${fixturesFolder}/test.example.com.pem -key-file=${fixturesFolder}/test.example.com-key.pem test.example.com`, {failOnNonZeroExit: false}); }); }); });