From 46ed27a218569f784b3cd4869c4453cac44a6ab5 Mon Sep 17 00:00:00 2001 From: Julien Breton Date: Mon, 1 Dec 2025 01:18:09 +0100 Subject: [PATCH] Fix: Extend Basic Auth compatibility with browsers --- messages/bg-BG.json | 2 + messages/cs-CZ.json | 2 + messages/de-DE.json | 2 + messages/en-US.json | 2 + messages/es-ES.json | 2 + messages/fr-FR.json | 36 +-- messages/it-IT.json | 2 + messages/ko-KR.json | 2 + messages/nb-NO.json | 2 + messages/nl-NL.json | 2 + messages/pl-PL.json | 2 + messages/pt-PT.json | 2 + messages/ru-RU.json | 2 + messages/tr-TR.json | 2 + messages/zh-CN.json | 2 + server/db/pg/schema/schema.ts | 9 + server/db/queries/verifySessionQueries.ts | 14 +- server/db/sqlite/schema/schema.ts | 233 +++++++++--------- server/lib/blueprints/proxyResources.ts | 79 ++++-- server/lib/blueprints/types.ts | 3 +- server/private/routers/hybrid.ts | 12 +- server/routers/badger/logRequestAudit.ts | 2 +- server/routers/badger/verifySession.ts | 62 ++++- .../routers/resource/getResourceAuthInfo.ts | 111 +++++---- server/routers/resource/listResources.ts | 10 +- .../routers/resource/setResourceHeaderAuth.ts | 43 ++-- .../[niceId]/authentication/page.tsx | 3 +- src/components/SetResourceHeaderAuthForm.tsx | 93 ++++--- src/components/SetResourcePasswordForm.tsx | 20 +- src/components/SetResourcePincodeForm.tsx | 20 +- src/components/SwitchInput.tsx | 49 +++- 31 files changed, 527 insertions(+), 300 deletions(-) diff --git a/messages/bg-BG.json b/messages/bg-BG.json index 7c31feb5..eb6e748a 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Създайте своя организация, сайт и ресурси", "setupNewOrg": "Нова организация", "setupCreateOrg": "Създаване на организация", diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index ff1a6900..61d9722e 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Vytvořte si organizaci, lokalitu a služby", "setupNewOrg": "Nová organizace", "setupCreateOrg": "Vytvořit organizaci", diff --git a/messages/de-DE.json b/messages/de-DE.json index 15a56f2d..0486aa11 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Erstelle eine Organisation, einen Standort und Ressourcen", "setupNewOrg": "Neue Organisation", "setupCreateOrg": "Organisation erstellen", diff --git a/messages/en-US.json b/messages/en-US.json index cf066c3d..dc09f0c2 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Create your organization, site, and resources", "setupNewOrg": "New Organization", "setupCreateOrg": "Create Organization", diff --git a/messages/es-ES.json b/messages/es-ES.json index 1b33c928..fb97b367 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Crea tu organización, sitio y recursos", "setupNewOrg": "Nueva organización", "setupCreateOrg": "Crear organización", diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 276fa9bd..a523a751 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Activez cette option pour forcer une réponse 401 lorsqu'un jeton d'authentification est manquant. Cette option est nécessaire pour les navigateurs et certaines bibliothèques HTTP qui n'envoient pas d'informations d'identification sans challenge du serveur.", + "headerAuthCompatibility": "Compatibilité étendue", "setupCreate": "Créez votre organisation, vos nœuds et vos ressources", "setupNewOrg": "Nouvelle organisation", "setupCreateOrg": "Créer une organisation", @@ -1834,23 +1836,23 @@ "rewritePathDescription": "Réécrivez éventuellement le chemin avant de le transmettre à la cible.", "continueToApplication": "Continuer vers l'application", "checkingInvite": "Vérification de l'invitation", - "setResourceHeaderAuth": "Définir l\\'authentification d\\'en-tête de la ressource", - "resourceHeaderAuthRemove": "Supprimer l'authentification de l'en-tête", - "resourceHeaderAuthRemoveDescription": "Authentification de l'en-tête supprimée avec succès.", - "resourceErrorHeaderAuthRemove": "Échec de la suppression de l'authentification de l'en-tête", - "resourceErrorHeaderAuthRemoveDescription": "Impossible de supprimer l'authentification de l'en-tête de la ressource.", - "resourceHeaderAuthProtectionEnabled": "Authentification de l'en-tête activée", - "resourceHeaderAuthProtectionDisabled": "L'authentification de l'en-tête est désactivée", - "headerAuthRemove": "Supprimer l'authentification de l'en-tête", - "headerAuthAdd": "Ajouter l'authentification de l'en-tête", - "resourceErrorHeaderAuthSetup": "Impossible de définir l'authentification de l'en-tête", - "resourceErrorHeaderAuthSetupDescription": "Impossible de définir l'authentification de l'en-tête pour la ressource.", - "resourceHeaderAuthSetup": "Authentification de l'en-tête définie avec succès", - "resourceHeaderAuthSetupDescription": "L'authentification de l'en-tête a été définie avec succès.", - "resourceHeaderAuthSetupTitle": "Authentification de l'en-tête", - "resourceHeaderAuthSetupTitleDescription": "Définissez les identifiants d'authentification de base (nom d'utilisateur et mot de passe) pour protéger cette ressource avec l'authentification de l'en-tête HTTP. Accédez-y en utilisant le format https://username:password@resource.example.com", - "resourceHeaderAuthSubmit": "Authentification de l'en-tête", - "actionSetResourceHeaderAuth": "Authentification de l'en-tête", + "setResourceHeaderAuth": "Définir l\\'authentification via en-tête de la ressource", + "resourceHeaderAuthRemove": "Supprimer l'authentification via en-tête", + "resourceHeaderAuthRemoveDescription": "En-tête d'authentification supprimée avec succès.", + "resourceErrorHeaderAuthRemove": "Échec de la suppression de l'authentification via en-tête", + "resourceErrorHeaderAuthRemoveDescription": "Impossible de supprimer l'authentification via en-tête sur la ressource.", + "resourceHeaderAuthProtectionEnabled": "Authentification par en-tête Activée", + "resourceHeaderAuthProtectionDisabled": "Authentification par en-tête Désactivée", + "headerAuthRemove": "Supprimer l'authentification via en-tête", + "headerAuthAdd": "Ajouter une en-tête d'authentification", + "resourceErrorHeaderAuthSetup": "Impossible de définir l'authentification via en-tête", + "resourceErrorHeaderAuthSetupDescription": "Impossible de définir l'authentification via en-tête pour la ressource.", + "resourceHeaderAuthSetup": "Authentification via en-tête définie avec succès", + "resourceHeaderAuthSetupDescription": "L'authentification via en-tête a été définie avec succès.", + "resourceHeaderAuthSetupTitle": "Authentification via en-tête", + "resourceHeaderAuthSetupTitleDescription": "Définissez les identifiants d'authentification de base (nom d'utilisateur et mot de passe) pour protéger cette ressource avec l'authentification via en-tête HTTP. Accédez-y en utilisant le format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Activer la protection via en-tête", + "actionSetResourceHeaderAuth": "Authentification via en-tête", "enterpriseEdition": "Édition Entreprise", "unlicensed": "Sans licence", "beta": "Bêta", diff --git a/messages/it-IT.json b/messages/it-IT.json index 08f1e405..798ead37 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Crea la tua organizzazione, sito e risorse", "setupNewOrg": "Nuova Organizzazione", "setupCreateOrg": "Crea Organizzazione", diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 2ce7f7e6..75e57281 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "조직, 사이트 및 리소스를 생성하십시오.", "setupNewOrg": "새 조직", "setupCreateOrg": "조직 생성", diff --git a/messages/nb-NO.json b/messages/nb-NO.json index c2f8f791..06539ff2 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Lag din organisasjon, område og dine ressurser", "setupNewOrg": "Ny Organisasjon", "setupCreateOrg": "Opprett organisasjon", diff --git a/messages/nl-NL.json b/messages/nl-NL.json index 73ba2acc..ea093df7 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Maak uw organisatie, site en bronnen aan", "setupNewOrg": "Nieuwe organisatie", "setupCreateOrg": "Nieuwe organisatie aanmaken", diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 6c401dfa..4c84a1f7 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Utwórz swoją organizację, witrynę i zasoby", "setupNewOrg": "Nowa organizacja", "setupCreateOrg": "Utwórz organizację", diff --git a/messages/pt-PT.json b/messages/pt-PT.json index ed9a4551..bd70cb0a 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Crie sua organização, site e recursos", "setupNewOrg": "Nova organização", "setupCreateOrg": "Criar Organização", diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 23c521c1..8b008900 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Создайте свою организацию, сайт и ресурсы", "setupNewOrg": "Новая организация", "setupCreateOrg": "Создать организацию", diff --git a/messages/tr-TR.json b/messages/tr-TR.json index 7fc8c5ff..0e34a40f 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Organizasyonunuzu, sitenizi ve kaynaklarınızı oluşturun", "setupNewOrg": "Yeni Organizasyon", "setupCreateOrg": "Organizasyon Oluştur", diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 6b13a912..dc48aaa7 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "创建您的第一个组织、网站和资源", "setupNewOrg": "新建组织", "setupCreateOrg": "创建组织", diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index ffbe820c..7a6af1e1 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -419,6 +419,14 @@ export const resourceHeaderAuth = pgTable("resourceHeaderAuth", { headerAuthHash: varchar("headerAuthHash").notNull() }); +export const resourceHeaderAuthExtendedCompatibility = pgTable("resourceHeaderAuthExtendedCompatibility", { + headerAuthExtendedCompatibilityId: serial("headerAuthExtendedCompatibilityId").primaryKey(), + resourceId: integer("resourceId") + .notNull() + .references(() => resources.resourceId, { onDelete: "cascade" }), + extendedCompatibilityIsActivated: boolean("extendedCompatibilityIsActivated").notNull().default(false), +}); + export const resourceAccessToken = pgTable("resourceAccessToken", { accessTokenId: varchar("accessTokenId").primaryKey(), orgId: varchar("orgId") @@ -781,6 +789,7 @@ export type ResourceSession = InferSelectModel; export type ResourcePincode = InferSelectModel; export type ResourcePassword = InferSelectModel; export type ResourceHeaderAuth = InferSelectModel; +export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel; export type ResourceOtp = InferSelectModel; export type ResourceAccessToken = InferSelectModel; export type ResourceWhitelist = InferSelectModel; diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts index 85bd7cc7..381185e5 100644 --- a/server/db/queries/verifySessionQueries.ts +++ b/server/db/queries/verifySessionQueries.ts @@ -1,4 +1,6 @@ -import { db, loginPage, LoginPage, loginPageOrg, Org, orgs } from "@server/db"; +import { + db, loginPage, LoginPage, loginPageOrg, Org, orgs, +} from "@server/db"; import { Resource, ResourcePassword, @@ -14,7 +16,9 @@ import { sessions, userOrgs, userResources, - users + users, + ResourceHeaderAuthExtendedCompatibility, + resourceHeaderAuthExtendedCompatibility } from "@server/db"; import { and, eq } from "drizzle-orm"; @@ -23,6 +27,7 @@ export type ResourceWithAuth = { pincode: ResourcePincode | null; password: ResourcePassword | null; headerAuth: ResourceHeaderAuth | null; + headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null org: Org; }; @@ -52,6 +57,10 @@ export async function getResourceByDomain( resourceHeaderAuth, eq(resourceHeaderAuth.resourceId, resources.resourceId) ) + .leftJoin( + resourceHeaderAuthExtendedCompatibility, + eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId) + ) .innerJoin( orgs, eq(orgs.orgId, resources.orgId) @@ -68,6 +77,7 @@ export async function getResourceByDomain( pincode: result.resourcePincode, password: result.resourcePassword, headerAuth: result.resourceHeaderAuth, + headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility, org: result.orgs }; } diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 13453d2e..11cb7a9b 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -1,32 +1,32 @@ -import { randomUUID } from "crypto"; -import { InferSelectModel } from "drizzle-orm"; -import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; -import { boolean } from "yargs"; +import {randomUUID} from "crypto"; +import {InferSelectModel} from "drizzle-orm"; +import {sqliteTable, text, integer, index} from "drizzle-orm/sqlite-core"; +import {boolean} from "yargs"; export const domains = sqliteTable("domains", { domainId: text("domainId").primaryKey(), baseDomain: text("baseDomain").notNull(), - configManaged: integer("configManaged", { mode: "boolean" }) + configManaged: integer("configManaged", {mode: "boolean"}) .notNull() .default(false), type: text("type"), // "ns", "cname", "wildcard" - verified: integer("verified", { mode: "boolean" }).notNull().default(false), - failed: integer("failed", { mode: "boolean" }).notNull().default(false), + verified: integer("verified", {mode: "boolean"}).notNull().default(false), + failed: integer("failed", {mode: "boolean"}).notNull().default(false), tries: integer("tries").notNull().default(0), certResolver: text("certResolver"), - preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" }) + preferWildcardCert: integer("preferWildcardCert", {mode: "boolean"}) }); export const dnsRecords = sqliteTable("dnsRecords", { - id: integer("id").primaryKey({ autoIncrement: true }), + id: integer("id").primaryKey({autoIncrement: true}), domainId: text("domainId") .notNull() - .references(() => domains.domainId, { onDelete: "cascade" }), + .references(() => domains.domainId, {onDelete: "cascade"}), recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT" baseDomain: text("baseDomain"), - value: text("value").notNull(), - verified: integer("verified", { mode: "boolean" }).notNull().default(false), + value: text("value").notNull(), + verified: integer("verified", {mode: "boolean"}).notNull().default(false), }); @@ -35,7 +35,7 @@ export const orgs = sqliteTable("orgs", { name: text("name").notNull(), subnet: text("subnet"), createdAt: text("createdAt"), - requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }), + requireTwoFactor: integer("requireTwoFactor", {mode: "boolean"}), maxSessionLengthHours: integer("maxSessionLengthHours"), // hours passwordExpiryDays: integer("passwordExpiryDays"), // days settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever @@ -52,23 +52,23 @@ export const orgs = sqliteTable("orgs", { export const userDomains = sqliteTable("userDomains", { userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), domainId: text("domainId") .notNull() - .references(() => domains.domainId, { onDelete: "cascade" }) + .references(() => domains.domainId, {onDelete: "cascade"}) }); export const orgDomains = sqliteTable("orgDomains", { orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), + .references(() => orgs.orgId, {onDelete: "cascade"}), domainId: text("domainId") .notNull() - .references(() => domains.domainId, { onDelete: "cascade" }) + .references(() => domains.domainId, {onDelete: "cascade"}) }); export const sites = sqliteTable("sites", { - siteId: integer("siteId").primaryKey({ autoIncrement: true }), + siteId: integer("siteId").primaryKey({autoIncrement: true}), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -85,7 +85,7 @@ export const sites = sqliteTable("sites", { megabytesOut: integer("bytesOut").default(0), lastBandwidthUpdate: text("lastBandwidthUpdate"), type: text("type").notNull(), // "newt" or "wireguard" - online: integer("online", { mode: "boolean" }).notNull().default(false), + online: integer("online", {mode: "boolean"}).notNull().default(false), // exit node stuff that is how to connect to the site when it has a wg server address: text("address"), // this is the address of the wireguard interface in newt @@ -93,15 +93,15 @@ export const sites = sqliteTable("sites", { publicKey: text("publicKey"), // TODO: Fix typo in publicKey lastHolePunch: integer("lastHolePunch"), listenPort: integer("listenPort"), - dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" }) + dockerSocketEnabled: integer("dockerSocketEnabled", {mode: "boolean"}) .notNull() .default(true), remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access }); export const resources = sqliteTable("resources", { - resourceId: integer("resourceId").primaryKey({ autoIncrement: true }), - resourceGuid: text("resourceGuid", { length: 36 }) + resourceId: integer("resourceId").primaryKey({autoIncrement: true}), + resourceGuid: text("resourceGuid", {length: 36}) .unique() .notNull() .$defaultFn(() => randomUUID()), @@ -117,38 +117,38 @@ export const resources = sqliteTable("resources", { domainId: text("domainId").references(() => domains.domainId, { onDelete: "set null" }), - ssl: integer("ssl", { mode: "boolean" }).notNull().default(false), - blockAccess: integer("blockAccess", { mode: "boolean" }) + ssl: integer("ssl", {mode: "boolean"}).notNull().default(false), + blockAccess: integer("blockAccess", {mode: "boolean"}) .notNull() .default(false), - sso: integer("sso", { mode: "boolean" }).notNull().default(true), - http: integer("http", { mode: "boolean" }).notNull().default(true), + sso: integer("sso", {mode: "boolean"}).notNull().default(true), + http: integer("http", {mode: "boolean"}).notNull().default(true), protocol: text("protocol").notNull(), proxyPort: integer("proxyPort"), - emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" }) + emailWhitelistEnabled: integer("emailWhitelistEnabled", {mode: "boolean"}) .notNull() .default(false), - applyRules: integer("applyRules", { mode: "boolean" }) + applyRules: integer("applyRules", {mode: "boolean"}) .notNull() .default(false), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), - stickySession: integer("stickySession", { mode: "boolean" }) + enabled: integer("enabled", {mode: "boolean"}).notNull().default(true), + stickySession: integer("stickySession", {mode: "boolean"}) .notNull() .default(false), tlsServerName: text("tlsServerName"), setHostHeader: text("setHostHeader"), - enableProxy: integer("enableProxy", { mode: "boolean" }).default(true), + enableProxy: integer("enableProxy", {mode: "boolean"}).default(true), skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { onDelete: "set null" }), headers: text("headers"), // comma-separated list of headers to add to the request - proxyProtocol: integer("proxyProtocol", { mode: "boolean" }).notNull().default(false), + proxyProtocol: integer("proxyProtocol", {mode: "boolean"}).notNull().default(false), proxyProtocolVersion: integer("proxyProtocolVersion").default(1) }); export const targets = sqliteTable("targets", { - targetId: integer("targetId").primaryKey({ autoIncrement: true }), + targetId: integer("targetId").primaryKey({autoIncrement: true}), resourceId: integer("resourceId") .references(() => resources.resourceId, { onDelete: "cascade" @@ -163,7 +163,7 @@ export const targets = sqliteTable("targets", { method: text("method"), port: integer("port").notNull(), internalPort: integer("internalPort"), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + enabled: integer("enabled", {mode: "boolean"}).notNull().default(true), path: text("path"), pathMatchType: text("pathMatchType"), // exact, prefix, regex rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target @@ -177,8 +177,8 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", { }), targetId: integer("targetId") .notNull() - .references(() => targets.targetId, { onDelete: "cascade" }), - hcEnabled: integer("hcEnabled", { mode: "boolean" }) + .references(() => targets.targetId, {onDelete: "cascade"}), + hcEnabled: integer("hcEnabled", {mode: "boolean"}) .notNull() .default(false), hcPath: text("hcPath"), @@ -199,7 +199,7 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", { }); export const exitNodes = sqliteTable("exitNodes", { - exitNodeId: integer("exitNodeId").primaryKey({ autoIncrement: true }), + exitNodeId: integer("exitNodeId").primaryKey({autoIncrement: true}), name: text("name").notNull(), address: text("address").notNull(), // this is the address of the wireguard interface in gerbil endpoint: text("endpoint").notNull(), // this is how to reach gerbil externally - gets put into the wireguard config @@ -207,7 +207,7 @@ export const exitNodes = sqliteTable("exitNodes", { listenPort: integer("listenPort").notNull(), reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control maxConnections: integer("maxConnections"), - online: integer("online", { mode: "boolean" }).notNull().default(false), + online: integer("online", {mode: "boolean"}).notNull().default(false), lastPing: integer("lastPing"), type: text("type").default("gerbil"), // gerbil, remoteExitNode region: text("region") @@ -220,17 +220,17 @@ export const siteResources = sqliteTable("siteResources", { }), siteId: integer("siteId") .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }), + .references(() => sites.siteId, {onDelete: "cascade"}), orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), + .references(() => orgs.orgId, {onDelete: "cascade"}), niceId: text("niceId").notNull(), name: text("name").notNull(), protocol: text("protocol").notNull(), proxyPort: integer("proxyPort").notNull(), destinationPort: integer("destinationPort").notNull(), destinationIp: text("destinationIp").notNull(), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true) + enabled: integer("enabled", {mode: "boolean"}).notNull().default(true) }); export const users = sqliteTable("user", { @@ -243,20 +243,20 @@ export const users = sqliteTable("user", { onDelete: "cascade" }), passwordHash: text("passwordHash"), - twoFactorEnabled: integer("twoFactorEnabled", { mode: "boolean" }) + twoFactorEnabled: integer("twoFactorEnabled", {mode: "boolean"}) .notNull() .default(false), twoFactorSetupRequested: integer("twoFactorSetupRequested", { mode: "boolean" }).default(false), twoFactorSecret: text("twoFactorSecret"), - emailVerified: integer("emailVerified", { mode: "boolean" }) + emailVerified: integer("emailVerified", {mode: "boolean"}) .notNull() .default(false), dateCreated: text("dateCreated").notNull(), termsAcceptedTimestamp: text("termsAcceptedTimestamp"), termsVersion: text("termsVersion"), - serverAdmin: integer("serverAdmin", { mode: "boolean" }) + serverAdmin: integer("serverAdmin", {mode: "boolean"}) .notNull() .default(false), lastPasswordChange: integer("lastPasswordChange") @@ -290,7 +290,7 @@ export const webauthnChallenge = sqliteTable("webauthnChallenge", { export const setupTokens = sqliteTable("setupTokens", { tokenId: text("tokenId").primaryKey(), token: text("token").notNull(), - used: integer("used", { mode: "boolean" }).notNull().default(false), + used: integer("used", {mode: "boolean"}).notNull().default(false), dateCreated: text("dateCreated").notNull(), dateUsed: text("dateUsed") }); @@ -306,7 +306,7 @@ export const newts = sqliteTable("newt", { }); export const clients = sqliteTable("clients", { - clientId: integer("id").primaryKey({ autoIncrement: true }), + clientId: integer("id").primaryKey({autoIncrement: true}), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -323,7 +323,7 @@ export const clients = sqliteTable("clients", { lastBandwidthUpdate: text("lastBandwidthUpdate"), lastPing: integer("lastPing"), type: text("type").notNull(), // "olm" - online: integer("online", { mode: "boolean" }).notNull().default(false), + online: integer("online", {mode: "boolean"}).notNull().default(false), // endpoint: text("endpoint"), lastHolePunch: integer("lastHolePunch") }); @@ -331,11 +331,11 @@ export const clients = sqliteTable("clients", { export const clientSites = sqliteTable("clientSites", { clientId: integer("clientId") .notNull() - .references(() => clients.clientId, { onDelete: "cascade" }), + .references(() => clients.clientId, {onDelete: "cascade"}), siteId: integer("siteId") .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }), - isRelayed: integer("isRelayed", { mode: "boolean" }) + .references(() => sites.siteId, {onDelete: "cascade"}), + isRelayed: integer("isRelayed", {mode: "boolean"}) .notNull() .default(false), endpoint: text("endpoint") @@ -352,10 +352,10 @@ export const olms = sqliteTable("olms", { }); export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", { - codeId: integer("id").primaryKey({ autoIncrement: true }), + codeId: integer("id").primaryKey({autoIncrement: true}), userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), codeHash: text("codeHash").notNull() }); @@ -363,7 +363,7 @@ export const sessions = sqliteTable("session", { sessionId: text("id").primaryKey(), userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), expiresAt: integer("expiresAt").notNull(), issuedAt: integer("issuedAt") }); @@ -372,7 +372,7 @@ export const newtSessions = sqliteTable("newtSession", { sessionId: text("id").primaryKey(), newtId: text("newtId") .notNull() - .references(() => newts.newtId, { onDelete: "cascade" }), + .references(() => newts.newtId, {onDelete: "cascade"}), expiresAt: integer("expiresAt").notNull() }); @@ -380,14 +380,14 @@ export const olmSessions = sqliteTable("clientSession", { sessionId: text("id").primaryKey(), olmId: text("olmId") .notNull() - .references(() => olms.olmId, { onDelete: "cascade" }), + .references(() => olms.olmId, {onDelete: "cascade"}), expiresAt: integer("expiresAt").notNull() }); export const userOrgs = sqliteTable("userOrgs", { userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -396,28 +396,28 @@ export const userOrgs = sqliteTable("userOrgs", { roleId: integer("roleId") .notNull() .references(() => roles.roleId), - isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false), + isOwner: integer("isOwner", {mode: "boolean"}).notNull().default(false), autoProvisioned: integer("autoProvisioned", { mode: "boolean" }).default(false) }); export const emailVerificationCodes = sqliteTable("emailVerificationCodes", { - codeId: integer("id").primaryKey({ autoIncrement: true }), + codeId: integer("id").primaryKey({autoIncrement: true}), userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), email: text("email").notNull(), code: text("code").notNull(), expiresAt: integer("expiresAt").notNull() }); export const passwordResetTokens = sqliteTable("passwordResetTokens", { - tokenId: integer("id").primaryKey({ autoIncrement: true }), + tokenId: integer("id").primaryKey({autoIncrement: true}), email: text("email").notNull(), userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), tokenHash: text("tokenHash").notNull(), expiresAt: integer("expiresAt").notNull() }); @@ -429,13 +429,13 @@ export const actions = sqliteTable("actions", { }); export const roles = sqliteTable("roles", { - roleId: integer("roleId").primaryKey({ autoIncrement: true }), + roleId: integer("roleId").primaryKey({autoIncrement: true}), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" }) .notNull(), - isAdmin: integer("isAdmin", { mode: "boolean" }), + isAdmin: integer("isAdmin", {mode: "boolean"}), name: text("name").notNull(), description: text("description") }); @@ -443,92 +443,92 @@ export const roles = sqliteTable("roles", { export const roleActions = sqliteTable("roleActions", { roleId: integer("roleId") .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }), + .references(() => roles.roleId, {onDelete: "cascade"}), actionId: text("actionId") .notNull() - .references(() => actions.actionId, { onDelete: "cascade" }), + .references(() => actions.actionId, {onDelete: "cascade"}), orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }) + .references(() => orgs.orgId, {onDelete: "cascade"}) }); export const userActions = sqliteTable("userActions", { userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), actionId: text("actionId") .notNull() - .references(() => actions.actionId, { onDelete: "cascade" }), + .references(() => actions.actionId, {onDelete: "cascade"}), orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }) + .references(() => orgs.orgId, {onDelete: "cascade"}) }); export const roleSites = sqliteTable("roleSites", { roleId: integer("roleId") .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }), + .references(() => roles.roleId, {onDelete: "cascade"}), siteId: integer("siteId") .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }) + .references(() => sites.siteId, {onDelete: "cascade"}) }); export const userSites = sqliteTable("userSites", { userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), siteId: integer("siteId") .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }) + .references(() => sites.siteId, {onDelete: "cascade"}) }); export const userClients = sqliteTable("userClients", { userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), clientId: integer("clientId") .notNull() - .references(() => clients.clientId, { onDelete: "cascade" }) + .references(() => clients.clientId, {onDelete: "cascade"}) }); export const roleClients = sqliteTable("roleClients", { roleId: integer("roleId") .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }), + .references(() => roles.roleId, {onDelete: "cascade"}), clientId: integer("clientId") .notNull() - .references(() => clients.clientId, { onDelete: "cascade" }) + .references(() => clients.clientId, {onDelete: "cascade"}) }); export const roleResources = sqliteTable("roleResources", { roleId: integer("roleId") .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }), + .references(() => roles.roleId, {onDelete: "cascade"}), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) + .references(() => resources.resourceId, {onDelete: "cascade"}) }); export const userResources = sqliteTable("userResources", { userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) + .references(() => resources.resourceId, {onDelete: "cascade"}) }); export const userInvites = sqliteTable("userInvites", { inviteId: text("inviteId").primaryKey(), orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), + .references(() => orgs.orgId, {onDelete: "cascade"}), email: text("email").notNull(), expiresAt: integer("expiresAt").notNull(), tokenHash: text("token").notNull(), roleId: integer("roleId") .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }) + .references(() => roles.roleId, {onDelete: "cascade"}) }); export const resourcePincode = sqliteTable("resourcePincode", { @@ -537,7 +537,7 @@ export const resourcePincode = sqliteTable("resourcePincode", { }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), + .references(() => resources.resourceId, {onDelete: "cascade"}), pincodeHash: text("pincodeHash").notNull(), digitLength: integer("digitLength").notNull() }); @@ -548,7 +548,7 @@ export const resourcePassword = sqliteTable("resourcePassword", { }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), + .references(() => resources.resourceId, {onDelete: "cascade"}), passwordHash: text("passwordHash").notNull() }); @@ -558,18 +558,28 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", { }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), + .references(() => resources.resourceId, {onDelete: "cascade"}), headerAuthHash: text("headerAuthHash").notNull() }); +export const resourceHeaderAuthExtendedCompatibility = sqliteTable("resourceHeaderAuthExtendedCompatibility", { + headerAuthExtendedCompatibilityId: integer("headerAuthExtendedCompatibilityId").primaryKey({ + autoIncrement: true + }), + resourceId: integer("resourceId") + .notNull() + .references(() => resources.resourceId, {onDelete: "cascade"}), + extendedCompatibilityIsActivated: integer("extendedCompatibilityIsActivated", {mode: "boolean"}).notNull() +}); + export const resourceAccessToken = sqliteTable("resourceAccessToken", { accessTokenId: text("accessTokenId").primaryKey(), orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), + .references(() => orgs.orgId, {onDelete: "cascade"}), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), + .references(() => resources.resourceId, {onDelete: "cascade"}), tokenHash: text("tokenHash").notNull(), sessionLength: integer("sessionLength").notNull(), expiresAt: integer("expiresAt"), @@ -582,13 +592,13 @@ export const resourceSessions = sqliteTable("resourceSessions", { sessionId: text("id").primaryKey(), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), + .references(() => resources.resourceId, {onDelete: "cascade"}), expiresAt: integer("expiresAt").notNull(), sessionLength: integer("sessionLength").notNull(), - doNotExtend: integer("doNotExtend", { mode: "boolean" }) + doNotExtend: integer("doNotExtend", {mode: "boolean"}) .notNull() .default(false), - isRequestToken: integer("isRequestToken", { mode: "boolean" }), + isRequestToken: integer("isRequestToken", {mode: "boolean"}), userSessionId: text("userSessionId").references(() => sessions.sessionId, { onDelete: "cascade" }), @@ -620,11 +630,11 @@ export const resourceSessions = sqliteTable("resourceSessions", { }); export const resourceWhitelist = sqliteTable("resourceWhitelist", { - whitelistId: integer("id").primaryKey({ autoIncrement: true }), + whitelistId: integer("id").primaryKey({autoIncrement: true}), email: text("email").notNull(), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) + .references(() => resources.resourceId, {onDelete: "cascade"}) }); export const resourceOtp = sqliteTable("resourceOtp", { @@ -633,7 +643,7 @@ export const resourceOtp = sqliteTable("resourceOtp", { }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), + .references(() => resources.resourceId, {onDelete: "cascade"}), email: text("email").notNull(), otpHash: text("otpHash").notNull(), expiresAt: integer("expiresAt").notNull() @@ -645,11 +655,11 @@ export const versionMigrations = sqliteTable("versionMigrations", { }); export const resourceRules = sqliteTable("resourceRules", { - ruleId: integer("ruleId").primaryKey({ autoIncrement: true }), + ruleId: integer("ruleId").primaryKey({autoIncrement: true}), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + .references(() => resources.resourceId, {onDelete: "cascade"}), + enabled: integer("enabled", {mode: "boolean"}).notNull().default(true), priority: integer("priority").notNull(), action: text("action").notNull(), // ACCEPT, DROP, PASS match: text("match").notNull(), // CIDR, PATH, IP @@ -657,17 +667,17 @@ export const resourceRules = sqliteTable("resourceRules", { }); export const supporterKey = sqliteTable("supporterKey", { - keyId: integer("keyId").primaryKey({ autoIncrement: true }), + keyId: integer("keyId").primaryKey({autoIncrement: true}), key: text("key").notNull(), githubUsername: text("githubUsername").notNull(), phrase: text("phrase"), tier: text("tier"), - valid: integer("valid", { mode: "boolean" }).notNull().default(false) + valid: integer("valid", {mode: "boolean"}).notNull().default(false) }); // Identity Providers export const idp = sqliteTable("idp", { - idpId: integer("idpId").primaryKey({ autoIncrement: true }), + idpId: integer("idpId").primaryKey({autoIncrement: true}), name: text("name").notNull(), type: text("type").notNull(), defaultRoleMapping: text("defaultRoleMapping"), @@ -687,7 +697,7 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", { variant: text("variant").notNull().default("oidc"), idpId: integer("idpId") .notNull() - .references(() => idp.idpId, { onDelete: "cascade" }), + .references(() => idp.idpId, {onDelete: "cascade"}), clientId: text("clientId").notNull(), clientSecret: text("clientSecret").notNull(), authUrl: text("authUrl").notNull(), @@ -715,22 +725,22 @@ export const apiKeys = sqliteTable("apiKeys", { apiKeyHash: text("apiKeyHash").notNull(), lastChars: text("lastChars").notNull(), createdAt: text("dateCreated").notNull(), - isRoot: integer("isRoot", { mode: "boolean" }).notNull().default(false) + isRoot: integer("isRoot", {mode: "boolean"}).notNull().default(false) }); export const apiKeyActions = sqliteTable("apiKeyActions", { apiKeyId: text("apiKeyId") .notNull() - .references(() => apiKeys.apiKeyId, { onDelete: "cascade" }), + .references(() => apiKeys.apiKeyId, {onDelete: "cascade"}), actionId: text("actionId") .notNull() - .references(() => actions.actionId, { onDelete: "cascade" }) + .references(() => actions.actionId, {onDelete: "cascade"}) }); export const apiKeyOrg = sqliteTable("apiKeyOrg", { apiKeyId: text("apiKeyId") .notNull() - .references(() => apiKeys.apiKeyId, { onDelete: "cascade" }), + .references(() => apiKeys.apiKeyId, {onDelete: "cascade"}), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -741,10 +751,10 @@ export const apiKeyOrg = sqliteTable("apiKeyOrg", { export const idpOrg = sqliteTable("idpOrg", { idpId: integer("idpId") .notNull() - .references(() => idp.idpId, { onDelete: "cascade" }), + .references(() => idp.idpId, {onDelete: "cascade"}), orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), + .references(() => orgs.orgId, {onDelete: "cascade"}), roleMapping: text("roleMapping"), orgMapping: text("orgMapping") }); @@ -762,19 +772,19 @@ export const blueprints = sqliteTable("blueprints", { name: text("name").notNull(), source: text("source").notNull(), createdAt: integer("createdAt").notNull(), - succeeded: integer("succeeded", { mode: "boolean" }).notNull(), + succeeded: integer("succeeded", {mode: "boolean"}).notNull(), contents: text("contents").notNull(), message: text("message") }); export const requestAuditLog = sqliteTable( "requestAuditLog", { - id: integer("id").primaryKey({ autoIncrement: true }), + id: integer("id").primaryKey({autoIncrement: true}), timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds orgId: text("orgId").references(() => orgs.orgId, { onDelete: "cascade" }), - action: integer("action", { mode: "boolean" }).notNull(), + action: integer("action", {mode: "boolean"}).notNull(), reason: integer("reason").notNull(), actorType: text("actorType"), actor: text("actor"), @@ -791,7 +801,7 @@ export const requestAuditLog = sqliteTable( host: text("host"), path: text("path"), method: text("method"), - tls: integer("tls", { mode: "boolean" }) + tls: integer("tls", {mode: "boolean"}) }, (table) => [ index("idx_requestAuditLog_timestamp").on(table.timestamp), @@ -832,6 +842,7 @@ export type ResourceSession = InferSelectModel; export type ResourcePincode = InferSelectModel; export type ResourcePassword = InferSelectModel; export type ResourceHeaderAuth = InferSelectModel; +export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel; export type ResourceOtp = InferSelectModel; export type ResourceAccessToken = InferSelectModel; export type ResourceWhitelist = InferSelectModel; diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index d85befed..f6a40438 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -2,7 +2,7 @@ import { domains, orgDomains, Resource, - resourceHeaderAuth, + resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility, resourcePincode, resourceRules, resourceWhitelist, @@ -16,8 +16,8 @@ import { userResources, users } from "@server/db"; -import { resources, targets, sites } from "@server/db"; -import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm"; +import {resources, targets, sites} from "@server/db"; +import {eq, and, asc, or, ne, count, isNotNull} from "drizzle-orm"; import { Config, ConfigSchema, @@ -25,12 +25,12 @@ import { TargetData } from "./types"; import logger from "@server/logger"; -import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; -import { pickPort } from "@server/routers/target/helpers"; -import { resourcePassword } from "@server/db"; -import { hashPassword } from "@server/auth/password"; -import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; -import { get } from "http"; +import {createCertificate} from "#dynamic/routers/certificates/createCertificate"; +import {pickPort} from "@server/routers/target/helpers"; +import {resourcePassword} from "@server/db"; +import {hashPassword} from "@server/auth/password"; +import {isValidCIDR, isValidIP, isValidUrlGlobPattern} from "../validators"; +import {get} from "http"; export type ProxyResourcesResults = { proxyResource: Resource; @@ -63,7 +63,7 @@ export async function updateProxyResources( if (targetSiteId) { // Look up site by niceId [site] = await trx - .select({ siteId: sites.siteId }) + .select({siteId: sites.siteId}) .from(sites) .where( and( @@ -75,7 +75,7 @@ export async function updateProxyResources( } else if (siteId) { // Use the provided siteId directly, but verify it belongs to the org [site] = await trx - .select({ siteId: sites.siteId }) + .select({siteId: sites.siteId}) .from(sites) .where( and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)) @@ -93,7 +93,7 @@ export async function updateProxyResources( let internalPortToCreate; if (!targetData["internal-port"]) { - const { internalPort, targetIps } = await pickPort( + const {internalPort, targetIps} = await pickPort( site.siteId!, trx ); @@ -226,7 +226,7 @@ export async function updateProxyResources( tlsServerName: resourceData["tls-server-name"] || null, emailWhitelistEnabled: resourceData.auth?.[ "whitelist-users" - ] + ] ? resourceData.auth["whitelist-users"].length > 0 : false, headers: headers || null, @@ -285,21 +285,39 @@ export async function updateProxyResources( existingResource.resourceId ) ); + + await trx + .delete(resourceHeaderAuthExtendedCompatibility) + .where( + eq( + resourceHeaderAuthExtendedCompatibility.resourceId, + existingResource.resourceId + ) + ); + if (resourceData.auth?.["basic-auth"]) { const headerAuthUser = resourceData.auth?.["basic-auth"]?.user; const headerAuthPassword = resourceData.auth?.["basic-auth"]?.password; - if (headerAuthUser && headerAuthPassword) { + const headerAuthExtendedCompatibility = + resourceData.auth?.["basic-auth"]?.extendedCompatibility; + if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) { const headerAuthHash = await hashPassword( Buffer.from( `${headerAuthUser}:${headerAuthPassword}` ).toString("base64") ); - await trx.insert(resourceHeaderAuth).values({ - resourceId: existingResource.resourceId, - headerAuthHash - }); + await Promise.all([ + trx.insert(resourceHeaderAuth).values({ + resourceId: existingResource.resourceId, + headerAuthHash + }), + trx.insert(resourceHeaderAuthExtendedCompatibility).values({ + resourceId: existingResource.resourceId, + extendedCompatibilityIsActivated: headerAuthExtendedCompatibility + }) + ]); } } @@ -360,7 +378,7 @@ export async function updateProxyResources( if (targetSiteId) { // Look up site by niceId [site] = await trx - .select({ siteId: sites.siteId }) + .select({siteId: sites.siteId}) .from(sites) .where( and( @@ -372,7 +390,7 @@ export async function updateProxyResources( } else if (siteId) { // Use the provided siteId directly, but verify it belongs to the org [site] = await trx - .select({ siteId: sites.siteId }) + .select({siteId: sites.siteId}) .from(sites) .where( and( @@ -417,7 +435,7 @@ export async function updateProxyResources( if (checkIfTargetChanged(existingTarget, updatedTarget)) { let internalPortToUpdate; if (!targetData["internal-port"]) { - const { internalPort, targetIps } = await pickPort( + const {internalPort, targetIps} = await pickPort( site.siteId!, trx ); @@ -646,18 +664,25 @@ export async function updateProxyResources( const headerAuthUser = resourceData.auth?.["basic-auth"]?.user; const headerAuthPassword = resourceData.auth?.["basic-auth"]?.password; + const headerAuthExtendedCompatibility = resourceData.auth?.["basic-auth"]?.extendedCompatibility; - if (headerAuthUser && headerAuthPassword) { + if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) { const headerAuthHash = await hashPassword( Buffer.from( `${headerAuthUser}:${headerAuthPassword}` ).toString("base64") ); - await trx.insert(resourceHeaderAuth).values({ - resourceId: newResource.resourceId, - headerAuthHash - }); + await Promise.all([ + trx.insert(resourceHeaderAuth).values({ + resourceId: newResource.resourceId, + headerAuthHash + }), + trx.insert(resourceHeaderAuthExtendedCompatibility).values({ + resourceId: newResource.resourceId, + extendedCompatibilityIsActivated: headerAuthExtendedCompatibility + }), + ]); } } @@ -1004,7 +1029,7 @@ async function getDomain( trx: Transaction ) { const [fullDomainExists] = await trx - .select({ resourceId: resources.resourceId }) + .select({resourceId: resources.resourceId}) .from(resources) .where( and( diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index a5ee5700..67129140 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -47,7 +47,8 @@ export const AuthSchema = z.object({ password: z.string().min(1).optional(), "basic-auth": z.object({ user: z.string().min(1), - password: z.string().min(1) + password: z.string().min(1), + extendedCompatibility: z.boolean().default(false) }).optional(), "sso-enabled": z.boolean().optional().default(false), "sso-roles": z diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index f78fb592..1e373b73 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -36,8 +36,10 @@ import { LoginPage, resourceHeaderAuth, ResourceHeaderAuth, + resourceHeaderAuthExtendedCompatibility, + ResourceHeaderAuthExtendedCompatibility, orgs, - requestAuditLog + requestAuditLog, } from "@server/db"; import { resources, @@ -188,6 +190,7 @@ export type ResourceWithAuth = { pincode: ResourcePincode | null; password: ResourcePassword | null; headerAuth: ResourceHeaderAuth | null; + headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; }; export type UserSessionWithUser = { @@ -511,6 +514,10 @@ hybridRouter.get( resourceHeaderAuth, eq(resourceHeaderAuth.resourceId, resources.resourceId) ) + .leftJoin( + resourceHeaderAuthExtendedCompatibility, + eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId) + ) .where(eq(resources.fullDomain, domain)) .limit(1); @@ -543,7 +550,8 @@ hybridRouter.get( resource: result.resources, pincode: result.resourcePincode, password: result.resourcePassword, - headerAuth: result.resourceHeaderAuth + headerAuth: result.resourceHeaderAuth, + headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility }; return response(res, { diff --git a/server/routers/badger/logRequestAudit.ts b/server/routers/badger/logRequestAudit.ts index 6beb52c6..c3b2f512 100644 --- a/server/routers/badger/logRequestAudit.ts +++ b/server/routers/badger/logRequestAudit.ts @@ -9,7 +9,7 @@ Reasons: 100 - Allowed by Rule 101 - Allowed No Auth 102 - Valid Access Token -103 - Valid header auth +103 - Valid Header Auth (HTTP Basic Auth) 104 - Valid Pincode 105 - Valid Password 106 - Valid email diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index d7fe9190..86eaadcd 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -13,7 +13,7 @@ import { LoginPage, Org, Resource, - ResourceHeaderAuth, + ResourceHeaderAuth, ResourceHeaderAuthExtendedCompatibility, ResourcePassword, ResourcePincode, ResourceRule, @@ -65,6 +65,7 @@ type BasicUserData = { export type VerifyUserResponse = { valid: boolean; + headerAuthChallenged?: boolean; redirectUrl?: string; userData?: BasicUserData; }; @@ -142,6 +143,7 @@ export async function verifyResourceSession( pincode: ResourcePincode | null; password: ResourcePassword | null; headerAuth: ResourceHeaderAuth | null; + headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; org: Org; } | undefined = cache.get(resourceCacheKey); @@ -171,7 +173,7 @@ export async function verifyResourceSession( cache.set(resourceCacheKey, resourceData, 5); } - const { resource, pincode, password, headerAuth } = resourceData; + const { resource, pincode, password, headerAuth, headerAuthExtendedCompatibility } = resourceData; if (!resource) { logger.debug(`Resource not found ${cleanHost}`); @@ -450,7 +452,8 @@ export async function verifyResourceSession( !sso && !pincode && !password && - !resource.emailWhitelistEnabled + !resource.emailWhitelistEnabled && + !headerAuthExtendedCompatibility?.extendedCompatibilityIsActivated ) { logRequestAudit( { @@ -465,13 +468,15 @@ export async function verifyResourceSession( return notAllowed(res); } - } else if (headerAuth) { + } + else if (headerAuth) { // if there are no other auth methods we need to return unauthorized if nothing is provided if ( !sso && !pincode && !password && - !resource.emailWhitelistEnabled + !resource.emailWhitelistEnabled && + !headerAuthExtendedCompatibility?.extendedCompatibilityIsActivated ) { logRequestAudit( { @@ -557,7 +562,7 @@ export async function verifyResourceSession( } if (resourceSession) { - // only run this check if not SSO sesion; SSO session length is checked later + // only run this check if not SSO session; SSO session length is checked later const accessPolicy = await enforceResourceSessionLength( resourceSession, resourceData.org @@ -701,6 +706,11 @@ export async function verifyResourceSession( } } + // If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge + if (headerAuthExtendedCompatibility && headerAuthExtendedCompatibility.extendedCompatibilityIsActivated && !clientHeaderAuth){ + return headerAuthChallenged(res, redirectPath, resource.orgId); + } + logger.debug("No more auth to check, resource not allowed"); if (config.getRawConfig().app.log_failed_attempts) { @@ -833,6 +843,46 @@ function allowed(res: Response, userData?: BasicUserData) { return response(res, data); } +async function headerAuthChallenged( + res: Response, + redirectPath?: string, + orgId?: string +) { + let loginPage: LoginPage | null = null; + if (orgId) { + const { tier } = await getOrgTierData(orgId); // returns null in oss + if (tier === TierId.STANDARD) { + loginPage = await getOrgLoginPage(orgId); + } + } + + let redirectUrl: string | undefined = undefined; + if (redirectPath) { + let endpoint: string; + + if (loginPage && loginPage.domainId && loginPage.fullDomain) { + const secure = config + .getRawConfig() + .app.dashboard_url?.startsWith("https"); + const method = secure ? "https" : "http"; + endpoint = `${method}://${loginPage.fullDomain}`; + } else { + endpoint = config.getRawConfig().app.dashboard_url!; + } + redirectUrl = `${endpoint}${redirectPath}`; + } + + const data = { + data: { headerAuthChallenged: true, valid: false, redirectUrl }, + success: true, + error: false, + message: "Access denied", + status: HttpCode.OK + }; + logger.debug(JSON.stringify(data)); + return response(res, data); +} + async function isUserAllowedToAccessResource( userSessionId: string, resource: Resource, diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index 60f8e586..5602a909 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -1,23 +1,23 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; +import {Request, Response, NextFunction} from "express"; +import {z} from "zod"; import { db, - resourceHeaderAuth, + resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility, resourcePassword, resourcePincode, resources } from "@server/db"; -import { eq } from "drizzle-orm"; +import {eq} from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { fromError } from "zod-validation-error"; +import {fromError} from "zod-validation-error"; import logger from "@server/logger"; -import { build } from "@server/build"; +import {build} from "@server/build"; const getResourceAuthInfoSchema = z.strictObject({ - resourceGuid: z.string() - }); + resourceGuid: z.string() +}); export type GetResourceAuthInfoResponse = { resourceId: number; @@ -27,6 +27,7 @@ export type GetResourceAuthInfoResponse = { password: boolean; pincode: boolean; headerAuth: boolean; + headerAuthExtendedCompatibility: boolean; sso: boolean; blockAccess: boolean; url: string; @@ -51,54 +52,68 @@ export async function getResourceAuthInfo( ); } - const { resourceGuid } = parsedParams.data; + const {resourceGuid} = parsedParams.data; const isGuidInteger = /^\d+$/.test(resourceGuid); const [result] = isGuidInteger && build === "saas" ? await db - .select() - .from(resources) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) - ) + .select() + .from(resources) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId) + ) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourceId, resources.resourceId) + ) - .leftJoin( - resourceHeaderAuth, - eq( - resourceHeaderAuth.resourceId, - resources.resourceId - ) - ) - .where(eq(resources.resourceId, Number(resourceGuid))) - .limit(1) + .leftJoin( + resourceHeaderAuth, + eq( + resourceHeaderAuth.resourceId, + resources.resourceId + ) + ) + .leftJoin( + resourceHeaderAuthExtendedCompatibility, + eq( + resourceHeaderAuthExtendedCompatibility.resourceId, + resources.resourceId + ) + ) + .where(eq(resources.resourceId, Number(resourceGuid))) + .limit(1) : await db - .select() - .from(resources) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) - ) + .select() + .from(resources) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId) + ) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourceId, resources.resourceId) + ) - .leftJoin( - resourceHeaderAuth, - eq( - resourceHeaderAuth.resourceId, - resources.resourceId - ) - ) - .where(eq(resources.resourceGuid, resourceGuid)) - .limit(1); + .leftJoin( + resourceHeaderAuth, + eq( + resourceHeaderAuth.resourceId, + resources.resourceId + ) + ) + .leftJoin( + resourceHeaderAuthExtendedCompatibility, + eq( + resourceHeaderAuthExtendedCompatibility.resourceId, + resources.resourceId + ) + ) + .where(eq(resources.resourceGuid, resourceGuid)) + .limit(1); const resource = result?.resources; if (!resource) { @@ -110,6 +125,7 @@ export async function getResourceAuthInfo( const pincode = result?.resourcePincode; const password = result?.resourcePassword; const headerAuth = result?.resourceHeaderAuth; + const headerAuthExtendedCompatibility = result?.resourceHeaderAuthExtendedCompatibility; const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; @@ -122,6 +138,7 @@ export async function getResourceAuthInfo( password: password !== null, pincode: pincode !== null, headerAuth: headerAuth !== null, + headerAuthExtendedCompatibility: headerAuthExtendedCompatibility !== null, sso: resource.sso, blockAccess: resource.blockAccess, url, diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index a72dd763..6d95ec59 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, resourceHeaderAuth } from "@server/db"; +import {db, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility} from "@server/db"; import { resources, userResources, @@ -67,7 +67,7 @@ type JoinedRow = { hcEnabled: boolean | null; }; -// grouped by resource with targets[]) +// grouped by resource with targets[]) export type ResourceWithTargets = { resourceId: number; name: string; @@ -111,7 +111,7 @@ function queryResources(accessibleResourceIds: number[], orgId: string) { domainId: resources.domainId, niceId: resources.niceId, headerAuthId: resourceHeaderAuth.headerAuthId, - + headerAuthExtendedCompatibilityId: resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId, targetId: targets.targetId, targetIp: targets.ip, targetPort: targets.port, @@ -133,6 +133,10 @@ function queryResources(accessibleResourceIds: number[], orgId: string) { resourceHeaderAuth, eq(resourceHeaderAuth.resourceId, resources.resourceId) ) + .leftJoin( + resourceHeaderAuthExtendedCompatibility, + eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId) + ) .leftJoin(targets, eq(targets.resourceId, resources.resourceId)) .leftJoin( targetHealthCheck, diff --git a/server/routers/resource/setResourceHeaderAuth.ts b/server/routers/resource/setResourceHeaderAuth.ts index 87ffbacd..6c866578 100644 --- a/server/routers/resource/setResourceHeaderAuth.ts +++ b/server/routers/resource/setResourceHeaderAuth.ts @@ -1,23 +1,24 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db, resourceHeaderAuth } from "@server/db"; -import { eq } from "drizzle-orm"; +import {Request, Response, NextFunction} from "express"; +import {z} from "zod"; +import {db, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility} from "@server/db"; +import {eq} from "drizzle-orm"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { fromError } from "zod-validation-error"; -import { response } from "@server/lib/response"; +import {fromError} from "zod-validation-error"; +import {response} from "@server/lib/response"; import logger from "@server/logger"; -import { hashPassword } from "@server/auth/password"; -import { OpenAPITags, registry } from "@server/openApi"; +import {hashPassword} from "@server/auth/password"; +import {OpenAPITags, registry} from "@server/openApi"; const setResourceAuthMethodsParamsSchema = z.object({ resourceId: z.string().transform(Number).pipe(z.int().positive()) }); const setResourceAuthMethodsBodySchema = z.strictObject({ - user: z.string().min(4).max(100).nullable(), - password: z.string().min(4).max(100).nullable() - }); + user: z.string().min(4).max(100).nullable(), + password: z.string().min(4).max(100).nullable(), + extendedCompatibility: z.boolean().nullable() +}); registry.registerPath({ method: "post", @@ -66,21 +67,29 @@ export async function setResourceHeaderAuth( ); } - const { resourceId } = parsedParams.data; - const { user, password } = parsedBody.data; + const {resourceId} = parsedParams.data; + const {user, password, extendedCompatibility} = parsedBody.data; await db.transaction(async (trx) => { await trx .delete(resourceHeaderAuth) .where(eq(resourceHeaderAuth.resourceId, resourceId)); + await trx.delete(resourceHeaderAuthExtendedCompatibility).where(eq(resourceHeaderAuthExtendedCompatibility.resourceId, resourceId)); - if (user && password) { + if (user && password && extendedCompatibility !== null) { const headerAuthHash = await hashPassword(Buffer.from(`${user}:${password}`).toString("base64")); - await trx - .insert(resourceHeaderAuth) - .values({ resourceId, headerAuthHash }); + await Promise.all([ + trx + .insert(resourceHeaderAuth) + .values({resourceId, headerAuthHash}), + trx + .insert(resourceHeaderAuthExtendedCompatibility) + .values({resourceId, extendedCompatibilityIsActivated: extendedCompatibility}) + ]); } + + }); return response(res, { diff --git a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx index fe5f0ca2..fa4825ae 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx @@ -439,7 +439,8 @@ export default function ResourceAuthenticationPage() { api.post(`/resource/${resource.resourceId}/header-auth`, { user: null, - password: null + password: null, + extendedCompatibility: null, }) .then(() => { toast({ diff --git a/src/components/SetResourceHeaderAuthForm.tsx b/src/components/SetResourceHeaderAuthForm.tsx index b1a75543..00cd02b1 100644 --- a/src/components/SetResourceHeaderAuthForm.tsx +++ b/src/components/SetResourceHeaderAuthForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { Button } from "@app/components/ui/button"; +import {Button} from "@app/components/ui/button"; import { Form, FormControl, @@ -9,12 +9,12 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; -import { Input } from "@app/components/ui/input"; -import { toast } from "@app/hooks/useToast"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; +import {Input} from "@app/components/ui/input"; +import {toast} from "@app/hooks/useToast"; +import {zodResolver} from "@hookform/resolvers/zod"; +import {useEffect, useState} from "react"; +import {useForm} from "react-hook-form"; +import {z} from "zod"; import { Credenza, CredenzaBody, @@ -25,23 +25,27 @@ import { CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; -import { formatAxiosError } from "@app/lib/api"; -import { AxiosResponse } from "axios"; -import { Resource } from "@server/db"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useTranslations } from "next-intl"; +import {formatAxiosError} from "@app/lib/api"; +import {AxiosResponse} from "axios"; +import {Resource} from "@server/db"; +import {createApiClient} from "@app/lib/api"; +import {useEnvContext} from "@app/hooks/useEnvContext"; +import {useTranslations} from "next-intl"; +import {SwitchInput} from "@/components/SwitchInput"; +import {InfoPopup} from "@/components/ui/info-popup"; const setHeaderAuthFormSchema = z.object({ user: z.string().min(4).max(100), - password: z.string().min(4).max(100) + password: z.string().min(4).max(100), + extendedCompatibility: z.boolean() }); type SetHeaderAuthFormValues = z.infer; const defaultValues: Partial = { user: "", - password: "" + password: "", + extendedCompatibility: false }; type SetHeaderAuthFormProps = { @@ -52,11 +56,11 @@ type SetHeaderAuthFormProps = { }; export default function SetResourceHeaderAuthForm({ - open, - setOpen, - resourceId, - onSetHeaderAuth -}: SetHeaderAuthFormProps) { + open, + setOpen, + resourceId, + onSetHeaderAuth + }: SetHeaderAuthFormProps) { const api = createApiClient(useEnvContext()); const t = useTranslations(); @@ -80,18 +84,9 @@ export default function SetResourceHeaderAuthForm({ api.post>(`/resource/${resourceId}/header-auth`, { user: data.user, - password: data.password + password: data.password, + extendedCompatibility: data.extendedCompatibility }) - .catch((e) => { - toast({ - variant: "destructive", - title: t('resourceErrorHeaderAuthSetup'), - description: formatAxiosError( - e, - t('resourceErrorHeaderAuthSetupDescription') - ) - }); - }) .then(() => { toast({ title: t('resourceHeaderAuthSetup'), @@ -102,6 +97,16 @@ export default function SetResourceHeaderAuthForm({ onSetHeaderAuth(); } }) + .catch((e) => { + toast({ + variant: "destructive", + title: t('resourceErrorHeaderAuthSetup'), + description: formatAxiosError( + e, + t('resourceErrorHeaderAuthSetupDescription') + ) + }); + }) .finally(() => setLoading(false)); } @@ -132,7 +137,7 @@ export default function SetResourceHeaderAuthForm({ ( + render={({field}) => ( {t('user')} @@ -142,14 +147,14 @@ export default function SetResourceHeaderAuthForm({ {...field} /> - + )} /> ( + render={({field}) => ( {t('password')} @@ -159,7 +164,25 @@ export default function SetResourceHeaderAuthForm({ {...field} /> - + + + )} + /> + ( + + + + + )} /> diff --git a/src/components/SetResourcePasswordForm.tsx b/src/components/SetResourcePasswordForm.tsx index 07146865..2fef1380 100644 --- a/src/components/SetResourcePasswordForm.tsx +++ b/src/components/SetResourcePasswordForm.tsx @@ -78,16 +78,6 @@ export default function SetResourcePasswordForm({ api.post>(`/resource/${resourceId}/password`, { password: data.password }) - .catch((e) => { - toast({ - variant: "destructive", - title: t('resourceErrorPasswordSetup'), - description: formatAxiosError( - e, - t('resourceErrorPasswordSetupDescription') - ) - }); - }) .then(() => { toast({ title: t('resourcePasswordSetup'), @@ -98,6 +88,16 @@ export default function SetResourcePasswordForm({ onSetPassword(); } }) + .catch((e) => { + toast({ + variant: "destructive", + title: t('resourceErrorPasswordSetup'), + description: formatAxiosError( + e, + t('resourceErrorPasswordSetupDescription') + ) + }); + }) .finally(() => setLoading(false)); } diff --git a/src/components/SetResourcePincodeForm.tsx b/src/components/SetResourcePincodeForm.tsx index d58d0c85..5306a297 100644 --- a/src/components/SetResourcePincodeForm.tsx +++ b/src/components/SetResourcePincodeForm.tsx @@ -84,16 +84,6 @@ export default function SetResourcePincodeForm({ api.post>(`/resource/${resourceId}/pincode`, { pincode: data.pincode }) - .catch((e) => { - toast({ - variant: "destructive", - title: t('resourceErrorPincodeSetup'), - description: formatAxiosError( - e, - t('resourceErrorPincodeSetupDescription') - ) - }); - }) .then(() => { toast({ title: t('resourcePincodeSetup'), @@ -104,6 +94,16 @@ export default function SetResourcePincodeForm({ onSetPincode(); } }) + .catch((e) => { + toast({ + variant: "destructive", + title: t('resourceErrorPincodeSetup'), + description: formatAxiosError( + e, + t('resourceErrorPincodeSetupDescription') + ) + }); + }) .finally(() => setLoading(false)); } diff --git a/src/components/SwitchInput.tsx b/src/components/SwitchInput.tsx index a2291c2e..e76b5277 100644 --- a/src/components/SwitchInput.tsx +++ b/src/components/SwitchInput.tsx @@ -1,11 +1,16 @@ import React from "react"; -import { Switch } from "./ui/switch"; -import { Label } from "./ui/label"; +import {Switch} from "./ui/switch"; +import {Label} from "./ui/label"; +import {Button} from "@/components/ui/button"; +import {Info} from "lucide-react"; +import {info} from "winston"; +import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover"; interface SwitchComponentProps { id: string; label?: string; description?: string; + info?: string; checked?: boolean; defaultChecked?: boolean; disabled?: boolean; @@ -13,14 +18,26 @@ interface SwitchComponentProps { } export function SwitchInput({ - id, - label, - description, - disabled, - checked, - defaultChecked = false, - onCheckedChange -}: SwitchComponentProps) { + id, + label, + description, + info, + disabled, + checked, + defaultChecked = false, + onCheckedChange + }: SwitchComponentProps) { + const defaultTrigger = ( + + ); + return (
@@ -32,6 +49,18 @@ export function SwitchInput({ disabled={disabled} /> {label && } + {info && + + {defaultTrigger} + + + {info && ( +

+ {info} +

+ )} +
+
}
{description && (