Compare commits

...

19 Commits

Author SHA1 Message Date
Owen
40cd8cdec7 Merge branch 'dev' 2025-10-17 16:25:01 -07:00
Owen
6768672a44 Merge branch 'main' of github.com:fosrl/pangolin 2025-10-17 16:24:55 -07:00
Owen
240c5b005b Add more transactions support 2025-10-17 16:22:43 -07:00
Owen
c07abf8ff9 Pass through transaction 2025-10-17 14:05:17 -07:00
Owen
e5a436593f Delete all before migrating 2025-10-17 14:05:17 -07:00
Owen
bb6e093ac6 Priority needs to be def 2025-10-17 14:05:17 -07:00
Milo Schwartz
59a334ce24 Update README.md 2025-10-17 14:05:17 -07:00
Owen
d241dcfb27 Fix typo 2025-10-17 14:05:17 -07:00
Owen
af263e7913 Pass through transaction 2025-10-17 14:04:49 -07:00
Owen Schwartz
6610e7d405 Merge pull request #1673 from fosrl/dependabot/npm_and_yarn/prod-patch-updates-ac45ae572b
Bump the prod-patch-updates group across 1 directory with 2 updates
2025-10-17 14:02:36 -07:00
Owen Schwartz
c476e65cf2 Merge pull request #1677 from fosrl/dependabot/npm_and_yarn/dev-patch-updates-3f2a7d9f8f
Bump the dev-patch-updates group across 1 directory with 2 updates
2025-10-17 14:01:57 -07:00
Owen Schwartz
b69b2eeeb3 Merge pull request #1689 from barnabehvrd/patch-2
FR translation update
2025-10-17 13:57:09 -07:00
Barnabé Havard
89dab0917b Fixed (again ...) indentation issues 2025-10-17 22:42:07 +02:00
Barnabé Havard
73efdb95ae Fixed indentation issues 2025-10-17 22:36:08 +02:00
Barnabé Havard
1bcca88614 Updated several translation 2025-10-17 22:32:51 +02:00
Owen Schwartz
8387571c1d Merge pull request #1684 from Pallavikumarimdb/fix/make-priority-optional
Make priority optional in schema
2025-10-17 10:14:01 -07:00
Pallavi Kumari
1d017f60b4 make priority optional in schema 2025-10-17 19:51:32 +05:30
dependabot[bot]
81effda9e8 Bump the prod-patch-updates group across 1 directory with 2 updates
Bumps the prod-patch-updates group with 2 updates in the / directory: [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) and [next](https://github.com/vercel/next.js).


Updates `eslint-config-next` from 15.5.4 to 15.5.5
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v15.5.5/packages/eslint-config-next)

Updates `next` from 15.5.4 to 15.5.5
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.4...v15.5.5)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 15.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: next
  dependency-version: 15.5.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-17 01:22:32 +00:00
dependabot[bot]
9343906ab1 Bump the dev-patch-updates group across 1 directory with 2 updates
Bumps the dev-patch-updates group with 2 updates in the / directory: [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) and [esbuild](https://github.com/evanw/esbuild).


Updates `@types/react-dom` from 19.2.1 to 19.2.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `esbuild` from 0.25.10 to 0.25.11
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.25.10...v0.25.11)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
- dependency-name: esbuild
  dependency-version: 0.25.11
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: dev-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-17 01:20:39 +00:00
10 changed files with 807 additions and 242 deletions

View File

@@ -6,45 +6,45 @@
"setupOrgName": "Nom de l'organisation", "setupOrgName": "Nom de l'organisation",
"orgDisplayName": "Ceci est le nom d'affichage de votre organisation.", "orgDisplayName": "Ceci est le nom d'affichage de votre organisation.",
"orgId": "ID de l'organisation", "orgId": "ID de l'organisation",
"setupIdentifierMessage": "Ceci est l'identifiant unique pour votre organisation. Il est séparé du nom affiché.", "setupIdentifierMessage": "Ceci est l'identifiant de votre organisation. Il est différent du nom affiché.",
"setupErrorIdentifier": "L'ID de l'organisation est déjà pris. Veuillez en choisir un autre.", "setupErrorIdentifier": "Cet identifiant est déjà pris. Veuillez en choisir un autre.",
"componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.", "componentsErrorNoMemberCreate": "Vous n'êtes actuellement membre d'aucune organisation. Créez une organisation pour commencer.",
"componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.", "componentsErrorNoMember": "Vous n'êtes actuellement membre d'aucune organisation.",
"welcome": "Bienvenue à Pangolin", "welcome": "Bienvenue sur Pangolin",
"welcomeTo": "Bienvenue chez", "welcomeTo": "Bienvenue chez",
"componentsCreateOrg": "Créer une organisation", "componentsCreateOrg": "Créer une organisation",
"componentsMember": "Vous êtes membre de {count, plural, =0 {aucune organisation} one {une organisation} other {# organisations}}.", "componentsMember": "Vous {count, plural, =0 {n'} other {}}êtes membre {count, plural, =0 {d'aucune organisation} one {d'une organisation} other {de # organisations}}.",
"componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.", "componentsInvalidKey": "Clés de licence invalides ou expirées détectées. Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
"dismiss": "Refuser", "dismiss": "Refuser",
"componentsLicenseViolation": "Violation de licence : Ce serveur utilise des sites {usedSites} qui dépassent la limite autorisée des sites {maxSites} . Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.", "componentsLicenseViolation": "Violation de licence : Ce serveur utilise des sites {usedSites} qui dépassent la limite autorisée des sites {maxSites} . Suivez les conditions de licence pour continuer à utiliser toutes les fonctionnalités.",
"componentsSupporterMessage": "Merci de soutenir Pangolin en tant que {tier}!", "componentsSupporterMessage": "Merci de soutenir Pangolin en tant que {tier}!",
"inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder n'ait pas été acceptée ou n'est plus valide.", "inviteErrorNotValid": "Nous sommes désolés, mais il semble que l'invitation via laquelle vous essayez d'accéder n'ait pas été acceptée ou n'est plus valide.",
"inviteErrorUser": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder ne soit pas pour cet utilisateur.", "inviteErrorUser": "Nous sommes désolés, mais il semble que l'invitation via laquelle vous essayez d'accéder ne soit pas pour cet utilisateur.",
"inviteLoginUser": "Assurez-vous que vous êtes bien connecté en tant qu'utilisateur correct.", "inviteLoginUser": "Assurez-vous d'etre bien connecté au bon compte.",
"inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation que vous essayez d'accéder ne soit pas pour un utilisateur qui existe.", "inviteErrorNoUser": "Nous sommes désolés, mais il semble que l'invitation via laquelle vous essayez d'accéder ne soit pas pour un utilisateur qui existe.",
"inviteCreateUser": "Veuillez d'abord créer un compte.", "inviteCreateUser": "Vous n'avez aucun compte, veuillez en créer un.",
"goHome": "Retour à la maison", "goHome": "Retour à l'accueil",
"inviteLogInOtherUser": "Se connecter en tant qu'utilisateur différent", "inviteLogInOtherUser": "Se connecter en tant qu'utilisateur différent",
"createAnAccount": "Créer un compte", "createAnAccount": "Créer un compte",
"inviteNotAccepted": "Invitation non acceptée", "inviteNotAccepted": "Invitation non acceptée",
"authCreateAccount": "Créez un compte pour commencer", "authCreateAccount": "Créez un compte pour commencer",
"authNoAccount": "Vous n'avez pas de compte ?", "authNoAccount": "Vous n'avez pas de compte ?",
"email": "Courriel", "email": "Adresse email",
"password": "Mot de passe", "password": "Mot de passe",
"confirmPassword": "Confirmer le mot de passe", "confirmPassword": "Confirmer le mot de passe",
"createAccount": "Créer un compte", "createAccount": "Créer un compte",
"viewSettings": "Afficher les paramètres", "viewSettings": "Afficher les paramètres",
"delete": "Supprimez", "delete": "Supprimer",
"name": "Nom", "name": "Nom",
"online": "En ligne", "online": "En ligne",
"offline": "Hors ligne", "offline": "Hors ligne",
"site": "Site", "site": "Site",
"dataIn": "Données dans", "dataIn": "Données reçues",
"dataOut": "Données épuisées", "dataOut": "Données émises",
"connectionType": "Type de connexion", "connectionType": "Type de connexion",
"tunnelType": "Type de tunnel", "tunnelType": "Type de tunnel",
"local": "Locale", "local": "Locale",
"edit": "Editer", "edit": "Modifier",
"siteConfirmDelete": "Confirmer la suppression du site", "siteConfirmDelete": "Confirmer la suppression du site",
"siteDelete": "Supprimer le site", "siteDelete": "Supprimer le site",
"siteMessageRemove": "Une fois supprimé, le site ne sera plus accessible. Toutes les ressources et cibles associées au site seront également supprimées.", "siteMessageRemove": "Une fois supprimé, le site ne sera plus accessible. Toutes les ressources et cibles associées au site seront également supprimées.",
@@ -64,11 +64,11 @@
"siteLearnNewt": "Apprenez à installer Newt sur votre système", "siteLearnNewt": "Apprenez à installer Newt sur votre système",
"siteSeeConfigOnce": "Vous ne pourrez voir la configuration qu'une seule fois.", "siteSeeConfigOnce": "Vous ne pourrez voir la configuration qu'une seule fois.",
"siteLoadWGConfig": "Chargement de la configuration WireGuard...", "siteLoadWGConfig": "Chargement de la configuration WireGuard...",
"siteDocker": "Développer les détails du déploiement Docker", "siteDocker": "Afficher les détails du déploiement Docker",
"toggle": "Activer/désactiver", "toggle": "Activer/désactiver",
"dockerCompose": "Composition Docker", "dockerCompose": "Docker Compose",
"dockerRun": "Exécution Docker", "dockerRun": "Docker Run",
"siteLearnLocal": "Les sites locaux ne tunnel, en savoir plus", "siteLearnLocal": "Les sites locaux ne permettent pas d'utiliser les tunnel, en savoir plus",
"siteConfirmCopy": "J'ai copié la configuration", "siteConfirmCopy": "J'ai copié la configuration",
"searchSitesProgress": "Rechercher des sites...", "searchSitesProgress": "Rechercher des sites...",
"siteAdd": "Ajouter un site", "siteAdd": "Ajouter un site",
@@ -79,9 +79,9 @@
"operatingSystem": "Système d'exploitation", "operatingSystem": "Système d'exploitation",
"commands": "Commandes", "commands": "Commandes",
"recommended": "Recommandé", "recommended": "Recommandé",
"siteNewtDescription": "Pour une meilleure expérience d'utilisateur, utilisez Newt. Il utilise WireGuard sous le capot et vous permet d'adresser vos ressources privées par leur adresse LAN sur votre réseau privé à partir du tableau de bord Pangolin.", "siteNewtDescription": "Pour une meilleure expérience d'utilisateur, utilisez Newt. Newt se base sur WireGuard et vous permet d'adresser vos ressources privées par leur adresse LAN sur votre réseau privé à partir du tableau de bord Pangolin.",
"siteRunsInDocker": "Exécute dans Docker", "siteRunsInDocker": "S'exécute dans Docker",
"siteRunsInShell": "Exécute en shell sur macOS, Linux et Windows", "siteRunsInShell": "S'exécute en shell sur macOS, Linux et Windows",
"siteErrorDelete": "Erreur lors de la suppression du site", "siteErrorDelete": "Erreur lors de la suppression du site",
"siteErrorUpdate": "Impossible de mettre à jour le site", "siteErrorUpdate": "Impossible de mettre à jour le site",
"siteErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour du site.", "siteErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour du site.",
@@ -89,18 +89,18 @@
"siteUpdatedDescription": "Le site a été mis à jour.", "siteUpdatedDescription": "Le site a été mis à jour.",
"siteGeneralDescription": "Configurer les paramètres généraux de ce site", "siteGeneralDescription": "Configurer les paramètres généraux de ce site",
"siteSettingDescription": "Configurer les paramètres de votre site", "siteSettingDescription": "Configurer les paramètres de votre site",
"siteSetting": "Réglages {siteName}", "siteSetting": "Réglages de {siteName}",
"siteNewtTunnel": "Tunnel Newt (Recommandé)", "siteNewtTunnel": "Tunnel Newt (Recommandé)",
"siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans votre réseau. Pas de configuration supplémentaire.", "siteNewtTunnelDescription": "La façon la plus simple de créer un point d'entrée dans votre réseau. Pas de configuration supplémentaire.",
"siteWg": "WireGuard basique", "siteWg": "WireGuard basique",
"siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.", "siteWgDescription": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise.",
"siteWgDescriptionSaas": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES", "siteWgDescriptionSaas": "Utilisez n'importe quel client WireGuard pour établir un tunnel. Configuration NAT manuelle requise. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES",
"siteLocalDescription": "Ressources locales seulement. Pas de tunneling.", "siteLocalDescription": "Ressources locales seulement. Pas de tunneling.",
"siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.", "siteLocalDescriptionSaas": "Ressources locales seulement. Pas de tunneling. Seulement disponible sur les noeuds distants",
"siteSeeAll": "Voir tous les sites", "siteSeeAll": "Voir tous les sites",
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site", "siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site",
"siteNewtCredentials": "Identifiants Newt", "siteNewtCredentials": "Identifiants Newt",
"siteNewtCredentialsDescription": "C'est ainsi que Newt s'authentifiera avec le serveur", "siteNewtCredentialsDescription": "C'est comme cela que Newt s'authentifiera avec le serveur",
"siteCredentialsSave": "Enregistrez vos identifiants", "siteCredentialsSave": "Enregistrez vos identifiants",
"siteCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de le copier dans un endroit sécurisé.", "siteCredentialsSaveDescription": "Vous ne pourrez voir cela qu'une seule fois. Assurez-vous de le copier dans un endroit sécurisé.",
"siteInfo": "Informations sur le site", "siteInfo": "Informations sur le site",
@@ -112,7 +112,7 @@
"shareErrorDelete": "Impossible de supprimer le lien", "shareErrorDelete": "Impossible de supprimer le lien",
"shareErrorDeleteMessage": "Une erreur s'est produite lors de la suppression du lien", "shareErrorDeleteMessage": "Une erreur s'est produite lors de la suppression du lien",
"shareDeleted": "Lien supprimé", "shareDeleted": "Lien supprimé",
"shareDeletedDescription": "Le lien a été supprimé", "shareDeletedDescription": "Le lien de partage a été supprimé",
"shareTokenDescription": "Votre jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.", "shareTokenDescription": "Votre jeton d'accès peut être passé de deux façons : en tant que paramètre de requête ou dans les en-têtes de la requête. Elles doivent être transmises par le client à chaque demande d'accès authentifié.",
"accessToken": "Jeton d'accès", "accessToken": "Jeton d'accès",
"usageExamples": "Exemples d'utilisation", "usageExamples": "Exemples d'utilisation",
@@ -134,7 +134,7 @@
"shareExpireDescription": "Le temps d'expiration est combien de temps le lien sera utilisable et fournira un accès à la ressource. Après cette période, le lien ne fonctionnera plus et les utilisateurs qui ont utilisé ce lien perdront l'accès à la ressource.", "shareExpireDescription": "Le temps d'expiration est combien de temps le lien sera utilisable et fournira un accès à la ressource. Après cette période, le lien ne fonctionnera plus et les utilisateurs qui ont utilisé ce lien perdront l'accès à la ressource.",
"shareSeeOnce": "Vous ne pourrez voir ce lien. Assurez-vous de le copier.", "shareSeeOnce": "Vous ne pourrez voir ce lien. Assurez-vous de le copier.",
"shareAccessHint": "N'importe qui avec ce lien peut accéder à la ressource. Partagez-le avec soin.", "shareAccessHint": "N'importe qui avec ce lien peut accéder à la ressource. Partagez-le avec soin.",
"shareTokenUsage": "Voir Utilisation du jeton d'accès", "shareTokenUsage": "Voir l'utilisation du jeton d'accès",
"createLink": "Créer un lien", "createLink": "Créer un lien",
"resourcesNotFound": "Aucune ressource trouvée", "resourcesNotFound": "Aucune ressource trouvée",
"resourceSearch": "Rechercher des ressources", "resourceSearch": "Rechercher des ressources",
@@ -1234,7 +1234,7 @@
"billing": "Facturation", "billing": "Facturation",
"orgBillingDescription": "Gérez vos informations de facturation et vos abonnements", "orgBillingDescription": "Gérez vos informations de facturation et vos abonnements",
"github": "GitHub", "github": "GitHub",
"pangolinHosted": "Pangolin Hébergement", "pangolinHosted": "Hebergé par Pangolin",
"fossorial": "Fossorial", "fossorial": "Fossorial",
"completeAccountSetup": "Complétez la configuration du compte", "completeAccountSetup": "Complétez la configuration du compte",
"completeAccountSetupDescription": "Définissez votre mot de passe pour commencer", "completeAccountSetupDescription": "Définissez votre mot de passe pour commencer",
@@ -1316,7 +1316,7 @@
"billingRemoteExitNodesInfo": "Vous êtes facturé pour chaque nœud géré dans votre organisation. La facturation est calculée quotidiennement en fonction du nombre de nœuds gérés actifs dans votre organisation.", "billingRemoteExitNodesInfo": "Vous êtes facturé pour chaque nœud géré dans votre organisation. La facturation est calculée quotidiennement en fonction du nombre de nœuds gérés actifs dans votre organisation.",
"domainNotFound": "Domaine introuvable", "domainNotFound": "Domaine introuvable",
"domainNotFoundDescription": "Cette ressource est désactivée car le domaine n'existe plus dans notre système. Veuillez définir un nouveau domaine pour cette ressource.", "domainNotFoundDescription": "Cette ressource est désactivée car le domaine n'existe plus dans notre système. Veuillez définir un nouveau domaine pour cette ressource.",
"failed": "Échec", "failed": "Erreur",
"createNewOrgDescription": "Créer une nouvelle organisation", "createNewOrgDescription": "Créer une nouvelle organisation",
"organization": "Organisation", "organization": "Organisation",
"port": "Port", "port": "Port",
@@ -1370,7 +1370,7 @@
"createDomainARecords": "Enregistrements A", "createDomainARecords": "Enregistrements A",
"createDomainRecordNumber": "Enregistrement {number}", "createDomainRecordNumber": "Enregistrement {number}",
"createDomainTxtRecords": "Enregistrements TXT", "createDomainTxtRecords": "Enregistrements TXT",
"createDomainSaveTheseRecords": "Enregistrez ces enregistrements", "createDomainSaveTheseRecords": "Sauvegardez ces enregistrements",
"createDomainSaveTheseRecordsDescription": "Assurez-vous de sauvegarder ces enregistrements DNS car vous ne les reverrez pas.", "createDomainSaveTheseRecordsDescription": "Assurez-vous de sauvegarder ces enregistrements DNS car vous ne les reverrez pas.",
"createDomainDnsPropagation": "Propagation DNS", "createDomainDnsPropagation": "Propagation DNS",
"createDomainDnsPropagationDescription": "Les modifications DNS peuvent mettre du temps à se propager sur internet. Cela peut prendre de quelques minutes à 48 heures selon votre fournisseur DNS et les réglages TTL.", "createDomainDnsPropagationDescription": "Les modifications DNS peuvent mettre du temps à se propager sur internet. Cela peut prendre de quelques minutes à 48 heures selon votre fournisseur DNS et les réglages TTL.",
@@ -1445,7 +1445,7 @@
"IntervalSeconds": "Intervalle sain", "IntervalSeconds": "Intervalle sain",
"timeoutSeconds": "Délai", "timeoutSeconds": "Délai",
"timeIsInSeconds": "Le temps est exprimé en secondes", "timeIsInSeconds": "Le temps est exprimé en secondes",
"retryAttempts": "Tentatives de réessai", "retryAttempts": "Tentatives",
"expectedResponseCodes": "Codes de réponse attendus", "expectedResponseCodes": "Codes de réponse attendus",
"expectedResponseCodesDescription": "Code de statut HTTP indiquant un état de santé satisfaisant. Si non renseigné, 200-300 est considéré comme satisfaisant.", "expectedResponseCodesDescription": "Code de statut HTTP indiquant un état de santé satisfaisant. Si non renseigné, 200-300 est considéré comme satisfaisant.",
"customHeaders": "En-têtes personnalisés", "customHeaders": "En-têtes personnalisés",
@@ -1760,9 +1760,9 @@
"enterpriseEdition": "Enterprise Edition", "enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed", "unlicensed": "Unlicensed",
"beta": "Beta", "beta": "Beta",
"manageClients": "Manage Clients", "manageClients": "Gérer les clients",
"manageClientsDescription": "Clients are devices that can connect to your sites", "manageClientsDescription": "Clients are devices that can connect to your sites",
"licenseTableValidUntil": "Valid Until", "licenseTableValidUntil": "Valide jusqu'au",
"saasLicenseKeysSettingsTitle": "Enterprise Licenses", "saasLicenseKeysSettingsTitle": "Enterprise Licenses",
"saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances", "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
"sidebarEnterpriseLicenses": "Licenses", "sidebarEnterpriseLicenses": "Licenses",
@@ -1771,8 +1771,8 @@
"validation": { "validation": {
"emailRequired": "Please enter a valid email address", "emailRequired": "Please enter a valid email address",
"useCaseTypeRequired": "Please select a use case type", "useCaseTypeRequired": "Please select a use case type",
"firstNameRequired": "First name is required", "firstNameRequired": "Le prénom est requis",
"lastNameRequired": "Last name is required", "lastNameRequired": "Le nom est requis",
"primaryUseRequired": "Please describe your primary use", "primaryUseRequired": "Please describe your primary use",
"jobTitleRequiredBusiness": "Job title is required for business use", "jobTitleRequiredBusiness": "Job title is required for business use",
"industryRequiredBusiness": "Industry is required for business use", "industryRequiredBusiness": "Industry is required for business use",
@@ -1786,7 +1786,7 @@
}, },
"useCaseOptions": { "useCaseOptions": {
"personal": { "personal": {
"title": "Personal Use", "title": "Utilisation personelle",
"description": "For individual, non-commercial use such as learning, personal projects, or experimentation." "description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
}, },
"business": { "business": {
@@ -1824,27 +1824,27 @@
}, },
"form": { "form": {
"useCaseQuestion": "Are you using Pangolin for personal or business use?", "useCaseQuestion": "Are you using Pangolin for personal or business use?",
"firstName": "First Name", "firstName": "Prénom",
"lastName": "Last Name", "lastName": "Nom",
"jobTitle": "Job Title", "jobTitle": "profession",
"primaryUseQuestion": "What do you primarily plan to use Pangolin for?", "primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
"industryQuestion": "What is your industry?", "industryQuestion": "What is your industry?",
"prospectiveUsersQuestion": "How many prospective users do you expect to have?", "prospectiveUsersQuestion": "How many prospective users do you expect to have?",
"prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?", "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
"companyName": "Company name", "companyName": "Entreprise",
"countryOfResidence": "Country of residence", "countryOfResidence": "Pays de résidence",
"stateProvinceRegion": "State / Province / Region", "stateProvinceRegion": "État / Province / Région",
"postalZipCode": "Postal / ZIP Code", "postalZipCode": "Code postal",
"companyWebsite": "Company website", "companyWebsite": "Site de l'entreprise",
"companyPhoneNumber": "Company phone number", "companyPhoneNumber": "Numéro de téléphone professionnel",
"country": "Country", "country": "Pays",
"phoneNumberOptional": "Phone number (optional)", "phoneNumberOptional": "Numéro de téléphone (optionnel)",
"complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license." "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
}, },
"buttons": { "buttons": {
"close": "Close", "close": "Fermer",
"previous": "Previous", "previous": "Précédent",
"next": "Next", "next": "Suivant",
"generateLicenseKey": "Generate License Key" "generateLicenseKey": "Generate License Key"
}, },
"toasts": { "toasts": {
@@ -1860,16 +1860,16 @@
}, },
"priority": "Priorité", "priority": "Priorité",
"priorityDescription": "Les routes de haute priorité sont évaluées en premier. La priorité = 100 signifie l'ordre automatique (décision du système). Utilisez un autre nombre pour imposer la priorité manuelle.", "priorityDescription": "Les routes de haute priorité sont évaluées en premier. La priorité = 100 signifie l'ordre automatique (décision du système). Utilisez un autre nombre pour imposer la priorité manuelle.",
"instanceName": "Instance Name", "instanceName": "Nom de l'instance",
"pathMatchModalTitle": "Configure Path Matching", "pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.", "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type", "pathMatchType": "Match Type",
"pathMatchPrefix": "Prefix", "pathMatchPrefix": "Préfix",
"pathMatchExact": "Exact", "pathMatchExact": "Exact",
"pathMatchRegex": "Regex", "pathMatchRegex": "Regex",
"pathMatchValue": "Path Value", "pathMatchValue": "Path Value",
"clear": "Clear", "clear": "Clear",
"saveChanges": "Save Changes", "saveChanges": "Sauvegarder",
"pathMatchRegexPlaceholder": "^/api/.*", "pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path", "pathMatchDefaultPlaceholder": "/path",
"pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.", "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
@@ -1889,7 +1889,7 @@
"pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly", "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
"pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement", "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
"pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix", "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
"pathRewritePrefix": "Prefix", "pathRewritePrefix": "Préfix",
"pathRewriteExact": "Exact", "pathRewriteExact": "Exact",
"pathRewriteRegex": "Regex", "pathRewriteRegex": "Regex",
"pathRewriteStrip": "Strip", "pathRewriteStrip": "Strip",

873
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -77,7 +77,7 @@
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"drizzle-orm": "0.44.6", "drizzle-orm": "0.44.6",
"eslint": "9.37.0", "eslint": "9.37.0",
"eslint-config-next": "15.5.4", "eslint-config-next": "15.5.5",
"express": "5.1.0", "express": "5.1.0",
"express-rate-limit": "8.1.0", "express-rate-limit": "8.1.0",
"glob": "11.0.3", "glob": "11.0.3",
@@ -92,7 +92,7 @@
"lucide-react": "^0.545.0", "lucide-react": "^0.545.0",
"maxmind": "5.0.0", "maxmind": "5.0.0",
"moment": "2.30.1", "moment": "2.30.1",
"next": "15.5.4", "next": "15.5.5",
"next-intl": "^4.3.12", "next-intl": "^4.3.12",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"node-cache": "5.1.2", "node-cache": "5.1.2",
@@ -143,13 +143,13 @@
"@types/nodemailer": "7.0.2", "@types/nodemailer": "7.0.2",
"@types/pg": "8.15.5", "@types/pg": "8.15.5",
"@types/react": "19.2.2", "@types/react": "19.2.2",
"@types/react-dom": "19.2.1", "@types/react-dom": "19.2.2",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@types/swagger-ui-express": "^4.1.8", "@types/swagger-ui-express": "^4.1.8",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@types/yargs": "17.0.33", "@types/yargs": "17.0.33",
"drizzle-kit": "0.31.5", "drizzle-kit": "0.31.5",
"esbuild": "0.25.10", "esbuild": "0.25.11",
"esbuild-node-externals": "1.18.0", "esbuild-node-externals": "1.18.0",
"postcss": "^8", "postcss": "^8",
"react-email": "4.3.0", "react-email": "4.3.0",

View File

@@ -126,7 +126,7 @@ export const targets = pgTable("targets", {
pathMatchType: text("pathMatchType"), // exact, prefix, regex pathMatchType: text("pathMatchType"), // exact, prefix, regex
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
priority: integer("priority").notNull().default(100) priority: integer("priority").default(100)
}); });
export const targetHealthCheck = pgTable("targetHealthCheck", { export const targetHealthCheck = pgTable("targetHealthCheck", {

View File

@@ -138,7 +138,7 @@ export const targets = sqliteTable("targets", {
pathMatchType: text("pathMatchType"), // exact, prefix, regex pathMatchType: text("pathMatchType"), // exact, prefix, regex
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
priority: integer("priority").notNull().default(100) priority: integer("priority").default(100)
}); });
export const targetHealthCheck = sqliteTable("targetHealthCheck", { export const targetHealthCheck = sqliteTable("targetHealthCheck", {

View File

@@ -612,7 +612,8 @@ export class UsageService {
public async getUsage( public async getUsage(
orgId: string, orgId: string,
featureId: FeatureId featureId: FeatureId,
trx: Transaction | typeof db = db
): Promise<Usage | null> { ): Promise<Usage | null> {
if (noop()) { if (noop()) {
return null; return null;
@@ -621,7 +622,7 @@ export class UsageService {
const usageId = `${orgId}-${featureId}`; const usageId = `${orgId}-${featureId}`;
try { try {
const [result] = await db const [result] = await trx
.select() .select()
.from(usage) .from(usage)
.where(eq(usage.usageId, usageId)) .where(eq(usage.usageId, usageId))
@@ -635,7 +636,7 @@ export class UsageService {
const meterId = getFeatureMeterId(featureId); const meterId = getFeatureMeterId(featureId);
try { try {
const [newUsage] = await db const [newUsage] = await trx
.insert(usage) .insert(usage)
.values({ .values({
usageId, usageId,
@@ -652,7 +653,7 @@ export class UsageService {
return newUsage; return newUsage;
} else { } else {
// Record was created by another process, fetch it // Record was created by another process, fetch it
const [existingUsage] = await db const [existingUsage] = await trx
.select() .select()
.from(usage) .from(usage)
.where(eq(usage.usageId, usageId)) .where(eq(usage.usageId, usageId))
@@ -665,7 +666,7 @@ export class UsageService {
`Insert failed for ${orgId}/${featureId}, attempting to fetch existing record:`, `Insert failed for ${orgId}/${featureId}, attempting to fetch existing record:`,
insertError insertError
); );
const [existingUsage] = await db const [existingUsage] = await trx
.select() .select()
.from(usage) .from(usage)
.where(eq(usage.usageId, usageId)) .where(eq(usage.usageId, usageId))
@@ -812,7 +813,8 @@ export class UsageService {
orgId: string, orgId: string,
kickSites = false, kickSites = false,
featureId?: FeatureId, featureId?: FeatureId,
usage?: Usage usage?: Usage,
trx: Transaction | typeof db = db
): Promise<boolean> { ): Promise<boolean> {
if (noop()) { if (noop()) {
return false; return false;
@@ -825,7 +827,7 @@ export class UsageService {
let orgLimits: Limit[] = []; let orgLimits: Limit[] = [];
if (featureId) { if (featureId) {
// Get all limits set for this organization // Get all limits set for this organization
orgLimits = await db orgLimits = await trx
.select() .select()
.from(limits) .from(limits)
.where( .where(
@@ -836,7 +838,7 @@ export class UsageService {
); );
} else { } else {
// Get all limits set for this organization // Get all limits set for this organization
orgLimits = await db orgLimits = await trx
.select() .select()
.from(limits) .from(limits)
.where(eq(limits.orgId, orgId)); .where(eq(limits.orgId, orgId));
@@ -855,7 +857,8 @@ export class UsageService {
} else { } else {
currentUsage = await this.getUsage( currentUsage = await this.getUsage(
orgId, orgId,
limit.featureId as FeatureId limit.featureId as FeatureId,
trx
); );
} }
@@ -890,7 +893,7 @@ export class UsageService {
); );
// Get all sites for this organization // Get all sites for this organization
const orgSites = await db const orgSites = await trx
.select() .select()
.from(sites) .from(sites)
.where(eq(sites.orgId, orgId)); .where(eq(sites.orgId, orgId));
@@ -902,7 +905,7 @@ export class UsageService {
// Send termination messages to newt sites // Send termination messages to newt sites
for (const site of orgSites) { for (const site of orgSites) {
if (site.type === "newt") { if (site.type === "newt") {
const [newt] = await db const [newt] = await trx
.select() .select()
.from(newts) .from(newts)
.where(eq(newts.siteId, site.siteId)) .where(eq(newts.siteId, site.siteId))
@@ -917,7 +920,7 @@ export class UsageService {
}; };
// Don't await to prevent blocking // Don't await to prevent blocking
sendToClient(newt.newtId, payload).catch( await sendToClient(newt.newtId, payload).catch(
(error: any) => { (error: any) => {
logger.error( logger.error(
`Failed to send termination message to newt ${newt.newtId}:`, `Failed to send termination message to newt ${newt.newtId}:`,

View File

@@ -1,4 +1,4 @@
import { db, exitNodes } from "@server/db"; import { db, exitNodes, Transaction } from "@server/db";
import logger from "@server/logger"; import logger from "@server/logger";
import { ExitNodePingResult } from "@server/routers/newt"; import { ExitNodePingResult } from "@server/routers/newt";
import { eq } from "drizzle-orm"; import { eq } from "drizzle-orm";
@@ -59,7 +59,11 @@ export function selectBestExitNode(
return pingResults[0]; return pingResults[0];
} }
export async function checkExitNodeOrg(exitNodeId: number, orgId: string) { export async function checkExitNodeOrg(
exitNodeId: number,
orgId: string,
trx?: Transaction | typeof db
): Promise<boolean> {
return false; return false;
} }

View File

@@ -18,7 +18,8 @@ import {
resources, resources,
targets, targets,
sites, sites,
targetHealthCheck targetHealthCheck,
Transaction
} from "@server/db"; } from "@server/db";
import logger from "@server/logger"; import logger from "@server/logger";
import { ExitNodePingResult } from "@server/routers/newt"; import { ExitNodePingResult } from "@server/routers/newt";
@@ -333,8 +334,8 @@ export function selectBestExitNode(
return fallbackNode; return fallbackNode;
} }
export async function checkExitNodeOrg(exitNodeId: number, orgId: string) { export async function checkExitNodeOrg(exitNodeId: number, orgId: string, trx: Transaction | typeof db = db) {
const [exitNodeOrg] = await db const [exitNodeOrg] = await trx
.select() .select()
.from(exitNodeOrgs) .from(exitNodeOrgs)
.where( .where(

View File

@@ -238,7 +238,7 @@ export async function getTraefikConfig(
} }
// get the valid certs for these domains // get the valid certs for these domains
validCerts = await getValidCertificatesForDomains(domains, true); // we are caching here because this is called often validCerts = await getValidCertificatesForDomains(domains, true); // we are caching here because this is called often
logger.debug(`Valid certs for domains: ${JSON.stringify(validCerts)}`); // logger.debug(`Valid certs for domains: ${JSON.stringify(validCerts)}`);
} }
const config_output: any = { const config_output: any = {

View File

@@ -98,7 +98,8 @@ export async function updateSiteBandwidth(
if ( if (
await checkExitNodeOrg( await checkExitNodeOrg(
exitNodeId, exitNodeId,
updatedSite.orgId updatedSite.orgId,
trx
) )
) { ) {
// not allowed // not allowed
@@ -148,7 +149,8 @@ export async function updateSiteBandwidth(
orgId, orgId,
true, true,
FeatureId.EGRESS_DATA_MB, FeatureId.EGRESS_DATA_MB,
bandwidthUsage bandwidthUsage,
trx
) )
.catch((error: any) => { .catch((error: any) => {
logger.error( logger.error(
@@ -174,7 +176,8 @@ export async function updateSiteBandwidth(
orgId, orgId,
true, true,
FeatureId.SITE_UPTIME, FeatureId.SITE_UPTIME,
uptimeUsage uptimeUsage,
trx
) )
.catch((error: any) => { .catch((error: any) => {
logger.error( logger.error(
@@ -242,7 +245,8 @@ export async function updateSiteBandwidth(
if ( if (
await checkExitNodeOrg( await checkExitNodeOrg(
exitNodeId, exitNodeId,
updatedSite.orgId updatedSite.orgId,
trx
) )
) { ) {
// not allowed // not allowed