Merge branch 'dev' into geoip

This commit is contained in:
Owen
2025-08-25 21:07:17 -07:00
23 changed files with 265 additions and 35 deletions

View File

@@ -1052,6 +1052,11 @@
"actionUpdateClient": "Update Client",
"actionListClients": "List Clients",
"actionGetClient": "Get Client",
"actionCreateSiteResource": "Create Site Resource",
"actionDeleteSiteResource": "Delete Site Resource",
"actionGetSiteResource": "Get Site Resource",
"actionListSiteResources": "List Site Resources",
"actionUpdateSiteResource": "Update Site Resource",
"noneSelected": "None selected",
"orgNotFound2": "No organizations found.",
"searchProgress": "Search...",

View File

@@ -1052,6 +1052,11 @@
"actionUpdateClient": "Update Client",
"actionListClients": "List Clients",
"actionGetClient": "Get Client",
"actionCreateSiteResource": "Create Site Resource",
"actionDeleteSiteResource": "Delete Site Resource",
"actionGetSiteResource": "Get Site Resource",
"actionListSiteResources": "List Site Resources",
"actionUpdateSiteResource": "Update Site Resource",
"noneSelected": "None selected",
"orgNotFound2": "No organizations found.",
"searchProgress": "Search...",

View File

@@ -1052,6 +1052,11 @@
"actionUpdateClient": "Kunde aktualisieren",
"actionListClients": "Kunden auflisten",
"actionGetClient": "Kunde holen",
"actionCreateSiteResource": "Site-Ressource erstellen",
"actionDeleteSiteResource": "Site-Ressource löschen",
"actionGetSiteResource": "Site-Ressource abrufen",
"actionListSiteResources": "Site-Ressourcen auflisten",
"actionUpdateSiteResource": "Site-Ressource aktualisieren",
"noneSelected": "Keine ausgewählt",
"orgNotFound2": "Keine Organisationen gefunden.",
"searchProgress": "Suche...",

View File

@@ -1054,6 +1054,11 @@
"actionUpdateClient": "Update Client",
"actionListClients": "List Clients",
"actionGetClient": "Get Client",
"actionCreateSiteResource": "Create Site Resource",
"actionDeleteSiteResource": "Delete Site Resource",
"actionGetSiteResource": "Get Site Resource",
"actionListSiteResources": "List Site Resources",
"actionUpdateSiteResource": "Update Site Resource",
"noneSelected": "None selected",
"orgNotFound2": "No organizations found.",
"searchProgress": "Search...",

View File

@@ -1052,6 +1052,11 @@
"actionUpdateClient": "Actualizar cliente",
"actionListClients": "Listar clientes",
"actionGetClient": "Obtener cliente",
"actionCreateSiteResource": "Crear Recurso del Sitio",
"actionDeleteSiteResource": "Eliminar recurso del sitio",
"actionGetSiteResource": "Obtener recurso del sitio",
"actionListSiteResources": "Listar recursos del sitio",
"actionUpdateSiteResource": "Actualizar recurso del sitio",
"noneSelected": "Ninguno seleccionado",
"orgNotFound2": "No se encontraron organizaciones.",
"searchProgress": "Buscar...",

View File

@@ -23,7 +23,7 @@
"inviteLoginUser": "Assurez-vous que vous êtes bien connecté en tant qu'utilisateur correct.",
"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.",
"inviteCreateUser": "Veuillez d'abord créer un compte.",
"goHome": "Retour à la maison",
"goHome": "Aller à laccueil",
"inviteLogInOtherUser": "Se connecter en tant qu'utilisateur différent",
"createAnAccount": "Créer un compte",
"inviteNotAccepted": "Invitation non acceptée",
@@ -34,16 +34,16 @@
"confirmPassword": "Confirmer le mot de passe",
"createAccount": "Créer un compte",
"viewSettings": "Afficher les paramètres",
"delete": "Supprimez",
"delete": "Supprimer",
"name": "Nom",
"online": "En ligne",
"offline": "Hors ligne",
"site": "Site",
"dataIn": "Données dans",
"dataOut": "Données épuisées",
"dataIn": "Données entrantes",
"dataOut": "Données sortantes",
"connectionType": "Type de connexion",
"tunnelType": "Type de tunnel",
"local": "Locale",
"local": "Local",
"edit": "Editer",
"siteConfirmDelete": "Confirmer la suppression du site",
"siteDelete": "Supprimer le site",
@@ -68,7 +68,7 @@
"toggle": "Activer/désactiver",
"dockerCompose": "Composition Docker",
"dockerRun": "Exécution Docker",
"siteLearnLocal": "Les sites locaux ne tunnel, en savoir plus",
"siteLearnLocal": "Les sites locaux ne tunnel plus, en savoir plus",
"siteConfirmCopy": "J'ai copié la configuration",
"searchSitesProgress": "Rechercher des sites...",
"siteAdd": "Ajouter un site",
@@ -94,9 +94,9 @@
"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",
"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. NE FONCTIONNE QUE SUR LES NŒUDS AUTO-HÉBERGÉS",
"siteLocalDescription": "Ressources locales seulement. Pas de tunneling.",
"siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. FONCTIONNE UNIQUEMENT SUR DES NŒUDS AUTONOMES",
"siteLocalDescriptionSaas": "Ressources locales uniquement. Pas de tunneling. NE FONCTIONNE QUE SUR LES NŒUDS AUTO-HÉBERGÉS",
"siteSeeAll": "Voir tous les sites",
"siteTunnelDescription": "Déterminez comment vous voulez vous connecter à votre site",
"siteNewtCredentials": "Identifiants Newt",
@@ -132,7 +132,7 @@
"expireIn": "Expire dans",
"neverExpire": "N'expire jamais",
"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 quune seule fois. Assurez-vous de le copier.",
"shareAccessHint": "N'importe qui avec ce lien peut accéder à la ressource. Partagez-le avec soin.",
"shareTokenUsage": "Voir Utilisation du jeton d'accès",
"createLink": "Créer un lien",
@@ -140,7 +140,7 @@
"resourceSearch": "Rechercher des ressources",
"openMenu": "Ouvrir le menu",
"resource": "Ressource",
"title": "Titre de la page",
"title": "Titre",
"created": "Créé",
"expires": "Expire",
"never": "Jamais",
@@ -196,7 +196,7 @@
"visibility": "Visibilité",
"enabled": "Activé",
"disabled": "Désactivé",
"general": "Généraux",
"general": "Général",
"generalSettings": "Paramètres généraux",
"proxy": "Proxy",
"internal": "Interne",
@@ -593,7 +593,7 @@
"newtId": "ID Newt",
"newtSecretKey": "Clé secrète Newt",
"architecture": "Architecture",
"sites": "Espaces",
"sites": "Sites",
"siteWgAnyClients": "Utilisez n'importe quel client WireGuard pour vous connecter. Vous devrez adresser vos ressources internes en utilisant l'IP du pair.",
"siteWgCompatibleAllClients": "Compatible avec tous les clients WireGuard",
"siteWgManualConfigurationRequired": "Configuration manuelle requise",
@@ -959,7 +959,7 @@
"supportKetOptionFull": "Support complet",
"forWholeServer": "Pour tout le serveur",
"lifetimePurchase": "Achat à vie",
"supporterStatus": "Statut de supporter",
"supporterStatus": "Statut de supporteur",
"buy": "Acheter",
"supportKeyOptionLimited": "Support limité",
"forFiveUsers": "Pour 5 utilisateurs ou moins",
@@ -1052,6 +1052,11 @@
"actionUpdateClient": "Mettre à jour le client",
"actionListClients": "Liste des clients",
"actionGetClient": "Obtenir le client",
"actionCreateSiteResource": "Créer une ressource de site",
"actionDeleteSiteResource": "Supprimer une ressource de site",
"actionGetSiteResource": "Obtenir une ressource de site",
"actionListSiteResources": "Lister les ressources de site",
"actionUpdateSiteResource": "Mettre à jour une ressource de site",
"noneSelected": "Aucune sélection",
"orgNotFound2": "Aucune organisation trouvée.",
"searchProgress": "Rechercher...",
@@ -1098,7 +1103,7 @@
"allowAll": "Tout autoriser",
"permissionsAllowAll": "Autoriser toutes les autorisations",
"githubUsernameRequired": "Le nom d'utilisateur GitHub est requis",
"supportKeyRequired": "La clé de supporter est requise",
"supportKeyRequired": "La clé de supporteur est requise",
"passwordRequirementsChars": "Le mot de passe doit comporter au moins 8 caractères",
"language": "Langue",
"verificationCodeRequired": "Le code est requis",
@@ -1110,14 +1115,14 @@
"orgErrorNoProvided": "Aucune organisation fournie",
"apiKeysErrorNoUpdate": "Pas de clé API à mettre à jour",
"sidebarOverview": "Aperçu",
"sidebarHome": "Domicile",
"sidebarSites": "Espaces",
"sidebarResources": "Ressource",
"sidebarHome": "Accueil",
"sidebarSites": "Sites",
"sidebarResources": "Ressources",
"sidebarAccessControl": "Contrôle d'accès",
"sidebarUsers": "Utilisateurs",
"sidebarInvitations": "Invitations",
"sidebarRoles": "Rôles",
"sidebarShareableLinks": "Liens partagables",
"sidebarShareableLinks": "Liens partageables",
"sidebarApiKeys": "Clés API",
"sidebarSettings": "Réglages",
"sidebarAllUsers": "Tous les utilisateurs",

View File

@@ -1052,6 +1052,11 @@
"actionUpdateClient": "Aggiorna Client",
"actionListClients": "Elenco Clienti",
"actionGetClient": "Ottieni Client",
"actionCreateSiteResource": "Crea Risorsa del Sito",
"actionDeleteSiteResource": "Elimina Risorsa del Sito",
"actionGetSiteResource": "Ottieni Risorsa del Sito",
"actionListSiteResources": "Elenca Risorse del Sito",
"actionUpdateSiteResource": "Aggiorna Risorsa del Sito",
"noneSelected": "Nessuna selezione",
"orgNotFound2": "Nessuna organizzazione trovata.",
"searchProgress": "Ricerca...",

View File

@@ -1052,6 +1052,11 @@
"actionUpdateClient": "클라이언트 업데이트",
"actionListClients": "클라이언트 목록",
"actionGetClient": "클라이언트 가져오기",
"actionCreateSiteResource": "사이트 리소스 생성",
"actionDeleteSiteResource": "사이트 리소스 삭제",
"actionGetSiteResource": "사이트 리소스 가져오기",
"actionListSiteResources": "사이트 리소스 목록",
"actionUpdateSiteResource": "사이트 리소스 업데이트",
"noneSelected": "선택된 항목 없음",
"orgNotFound2": "조직이 없습니다.",
"searchProgress": "검색...",

View File

@@ -1052,6 +1052,11 @@
"actionUpdateClient": "Oppdater klient",
"actionListClients": "List klienter",
"actionGetClient": "Hent klient",
"actionCreateSiteResource": "Opprett stedsressurs",
"actionDeleteSiteResource": "Slett Stedsressurs",
"actionGetSiteResource": "Hent Stedsressurs",
"actionListSiteResources": "List opp Stedsressurser",
"actionUpdateSiteResource": "Oppdater Stedsressurs",
"noneSelected": "Ingen valgt",
"orgNotFound2": "Ingen organisasjoner funnet.",
"searchProgress": "Søker...",

View File

@@ -1052,6 +1052,11 @@
"actionUpdateClient": "Klant bijwerken",
"actionListClients": "Lijst klanten",
"actionGetClient": "Client ophalen",
"actionCreateSiteResource": "Sitebron maken",
"actionDeleteSiteResource": "Document verwijderen van site",
"actionGetSiteResource": "Bron van site ophalen",
"actionListSiteResources": "Bronnen van site weergeven",
"actionUpdateSiteResource": "Document bijwerken van site",
"noneSelected": "Niet geselecteerd",
"orgNotFound2": "Geen organisaties gevonden.",
"searchProgress": "Zoeken...",

View File

@@ -1052,6 +1052,11 @@
"actionUpdateClient": "Aktualizuj klienta",
"actionListClients": "Lista klientów",
"actionGetClient": "Pobierz klienta",
"actionCreateSiteResource": "Utwórz zasób witryny",
"actionDeleteSiteResource": "Usuń zasób strony",
"actionGetSiteResource": "Pobierz zasób strony",
"actionListSiteResources": "Lista zasobów strony",
"actionUpdateSiteResource": "Aktualizuj zasób strony",
"noneSelected": "Nie wybrano",
"orgNotFound2": "Nie znaleziono organizacji.",
"searchProgress": "Szukaj...",

View File

@@ -1052,6 +1052,11 @@
"actionUpdateClient": "Atualizar Cliente",
"actionListClients": "Listar Clientes",
"actionGetClient": "Obter Cliente",
"actionCreateSiteResource": "Criar Recurso do Site",
"actionDeleteSiteResource": "Eliminar Recurso do Site",
"actionGetSiteResource": "Obter Recurso do Site",
"actionListSiteResources": "Listar Recursos do Site",
"actionUpdateSiteResource": "Atualizar Recurso do Site",
"noneSelected": "Nenhum selecionado",
"orgNotFound2": "Nenhuma organização encontrada.",
"searchProgress": "Pesquisar...",

View File

@@ -1052,6 +1052,11 @@
"actionUpdateClient": "Обновить Клиента",
"actionListClients": "Список Клиентов",
"actionGetClient": "Получить Клиента",
"actionCreateSiteResource": "Создать ресурс сайта",
"actionDeleteSiteResource": "Удалить ресурс сайта ",
"actionGetSiteResource": "Получить ресурс сайта",
"actionListSiteResources": "Список ресурсов сайта",
"actionUpdateSiteResource": "Обновить ресурс сайта",
"noneSelected": "Ничего не выбрано",
"orgNotFound2": "Организации не найдены.",
"searchProgress": "Поиск...",

View File

@@ -1052,6 +1052,11 @@
"actionUpdateClient": "Müşteri Güncelle",
"actionListClients": "Müşterileri Listele",
"actionGetClient": "Müşteriyi Al",
"actionCreateSiteResource": "Site Kaynağı Oluştur",
"actionDeleteSiteResource": "Site Kaynağını Sil",
"actionGetSiteResource": "Site Kaynağını Al",
"actionListSiteResources": "Site Kaynaklarını Listele",
"actionUpdateSiteResource": "Site Kaynağını Güncelle",
"noneSelected": "Hiçbiri seçili değil",
"orgNotFound2": "Hiçbir organizasyon bulunamadı.",
"searchProgress": "Ara...",

View File

@@ -1052,6 +1052,11 @@
"actionUpdateClient": "更新客户端",
"actionListClients": "列出客户端",
"actionGetClient": "获取客户端",
"actionCreateSiteResource": "创建站点资源",
"actionDeleteSiteResource": "删除站点资源",
"actionGetSiteResource": "获取站点资源",
"actionListSiteResources": "列出站点资源",
"actionUpdateSiteResource": "更新站点资源",
"noneSelected": "未选择",
"orgNotFound2": "未找到组织。",
"searchProgress": "搜索中...",

View File

@@ -11,3 +11,4 @@ export * from "./verifyAccessTokenAccess";
export * from "./verifyApiKeyIsRoot";
export * from "./verifyApiKeyApiKeyAccess";
export * from "./verifyApiKeyClientAccess";
export * from "./verifyApiKeySiteResourceAccess";

View File

@@ -0,0 +1,97 @@
import { Request, Response, NextFunction } from "express";
import { db } from "@server/db";
import { siteResources, apiKeyOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
export async function verifyApiKeySiteResourceAccess(
req: Request,
res: Response,
next: NextFunction
) {
try {
const apiKey = req.apiKey;
const siteResourceId = parseInt(req.params.siteResourceId);
const siteId = parseInt(req.params.siteId);
const orgId = req.params.orgId;
if (!apiKey) {
return next(
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
);
}
if (!siteResourceId || !siteId || !orgId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Missing required parameters"
)
);
}
if (apiKey.isRoot) {
// Root keys can access any resource in any org
return next();
}
// Check if the site resource exists and belongs to the specified site and org
const [siteResource] = await db
.select()
.from(siteResources)
.where(and(
eq(siteResources.siteResourceId, siteResourceId),
eq(siteResources.siteId, siteId),
eq(siteResources.orgId, orgId)
))
.limit(1);
if (!siteResource) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
"Site resource not found"
)
);
}
// Verify that the API key has access to the organization
if (!req.apiKeyOrg) {
const apiKeyOrgRes = await db
.select()
.from(apiKeyOrg)
.where(
and(
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
eq(apiKeyOrg.orgId, orgId)
)
)
.limit(1);
if (apiKeyOrgRes.length === 0) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"Key does not have access to this organization"
)
);
}
req.apiKeyOrg = apiKeyOrgRes[0];
}
// Attach the siteResource to the request for use in the next middleware/route
// @ts-ignore - Extending Request type
req.siteResource = siteResource;
return next();
} catch (error) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying site resource access"
)
);
}
}

View File

@@ -9,6 +9,7 @@ import * as client from "./client";
import * as accessToken from "./accessToken";
import * as apiKeys from "./apiKeys";
import * as idp from "./idp";
import * as siteResource from "./siteResource";
import {
verifyApiKey,
verifyApiKeyOrgAccess,
@@ -22,7 +23,8 @@ import {
verifyApiKeyAccessTokenAccess,
verifyApiKeyIsRoot,
verifyApiKeyClientAccess,
verifyClientsEnabled
verifyClientsEnabled,
verifyApiKeySiteResourceAccess
} from "@server/middlewares";
import HttpCode from "@server/types/HttpCode";
import { Router } from "express";
@@ -128,6 +130,69 @@ authenticated.delete(
site.deleteSite
);
authenticated.get(
"/org/:orgId/user-resources",
verifyApiKeyOrgAccess,
resource.getUserResources
);
// Site Resource endpoints
authenticated.put(
"/org/:orgId/site/:siteId/resource",
verifyApiKeyOrgAccess,
verifyApiKeySiteAccess,
verifyApiKeyHasAction(ActionsEnum.createSiteResource),
siteResource.createSiteResource
);
authenticated.get(
"/org/:orgId/site/:siteId/resources",
verifyApiKeyOrgAccess,
verifyApiKeySiteAccess,
verifyApiKeyHasAction(ActionsEnum.listSiteResources),
siteResource.listSiteResources
);
authenticated.get(
"/org/:orgId/site-resources",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listSiteResources),
siteResource.listAllSiteResourcesByOrg
);
authenticated.get(
"/org/:orgId/site/:siteId/resource/:siteResourceId",
verifyApiKeyOrgAccess,
verifyApiKeySiteAccess,
verifyApiKeySiteResourceAccess,
verifyApiKeyHasAction(ActionsEnum.getSiteResource),
siteResource.getSiteResource
);
authenticated.post(
"/org/:orgId/site/:siteId/resource/:siteResourceId",
verifyApiKeyOrgAccess,
verifyApiKeySiteAccess,
verifyApiKeySiteResourceAccess,
verifyApiKeyHasAction(ActionsEnum.updateSiteResource),
siteResource.updateSiteResource
);
authenticated.delete(
"/org/:orgId/site/:siteId/resource/:siteResourceId",
verifyApiKeyOrgAccess,
verifyApiKeySiteAccess,
verifyApiKeySiteResourceAccess,
verifyApiKeyHasAction(ActionsEnum.deleteSiteResource),
siteResource.deleteSiteResource
);
authenticated.put(
"/org/:orgId/resource",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.createResource),
resource.createResource
);
authenticated.put(
"/org/:orgId/site/:siteId/resource",
verifyApiKeyOrgAccess,

View File

@@ -60,7 +60,7 @@ export type ListRolesResponse = {
registry.registerPath({
method: "get",
path: "/orgs/{orgId}/roles",
path: "/org/{orgId}/roles",
description: "List roles.",
tags: [OpenAPITags.Org, OpenAPITags.Role],
request: {

View File

@@ -58,7 +58,7 @@ export default async function migration() {
await db.execute(sql`ALTER TABLE "clientSites" ADD COLUMN "endpoint" varchar;`);
await db.execute(sql`ALTER TABLE "exitNodes" ADD COLUMN "online" integer DEFAULT false NOT NULL;`);
await db.execute(sql`ALTER TABLE "exitNodes" ADD COLUMN "online" boolean DEFAULT false NOT NULL;`);
await db.execute(sql`ALTER TABLE "exitNodes" ADD COLUMN "lastPing" integer;`);

View File

@@ -777,15 +777,6 @@ export default function Page() {
</SettingsContainer>
<div className="flex justify-end space-x-2 mt-8">
<Button
type="button"
variant="outline"
onClick={() => {
router.push(`/${orgId}/settings/access/users`);
}}
>
{t("cancel")}
</Button>
{userType && dataLoaded && (
<Button
type={inviteLink ? "button" : "submit"}

View File

@@ -51,7 +51,12 @@ function getActionsCategories(root: boolean) {
[t('actionSetResourcePassword')]: "setResourcePassword",
[t('actionSetResourcePincode')]: "setResourcePincode",
[t('actionSetResourceEmailWhitelist')]: "setResourceWhitelist",
[t('actionGetResourceEmailWhitelist')]: "getResourceWhitelist"
[t('actionGetResourceEmailWhitelist')]: "getResourceWhitelist",
[t('actionCreateSiteResource')]: "createSiteResource",
[t('actionDeleteSiteResource')]: "deleteSiteResource",
[t('actionGetSiteResource')]: "getSiteResource",
[t('actionListSiteResources')]: "listSiteResources",
[t('actionUpdateSiteResource')]: "updateSiteResource"
},
Target: {

4
t
View File

@@ -1,4 +0,0 @@
docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest --push .
docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:1.9.0 --push .
docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest --push .
docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-1.9.0 --push .