Merge branch 'dev' of github.com:jln-brtn/pangolin into jln-brtn-dev

This commit is contained in:
Owen
2025-12-20 15:34:32 -05:00
17 changed files with 477 additions and 281 deletions

View File

@@ -1,5 +1,7 @@
{ {
"setupCreate": "Create the organization, site, and resources", "setupCreate": "Create the organization, site, and resources",
"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",
"setupNewOrg": "New Organization", "setupNewOrg": "New Organization",
"setupCreateOrg": "Create Organization", "setupCreateOrg": "Create Organization",
"setupCreateResources": "Create Resources", "setupCreateResources": "Create Resources",

View File

@@ -456,6 +456,14 @@ export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
headerAuthHash: varchar("headerAuthHash").notNull() 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", { export const resourceAccessToken = pgTable("resourceAccessToken", {
accessTokenId: varchar("accessTokenId").primaryKey(), accessTokenId: varchar("accessTokenId").primaryKey(),
orgId: varchar("orgId") orgId: varchar("orgId")
@@ -856,6 +864,7 @@ export type ResourceSession = InferSelectModel<typeof resourceSessions>;
export type ResourcePincode = InferSelectModel<typeof resourcePincode>; export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
export type ResourcePassword = InferSelectModel<typeof resourcePassword>; export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>; export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<typeof resourceHeaderAuthExtendedCompatibility>;
export type ResourceOtp = InferSelectModel<typeof resourceOtp>; export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>; export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>; export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;

View File

@@ -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 { import {
Resource, Resource,
ResourcePassword, ResourcePassword,
@@ -14,7 +16,9 @@ import {
sessions, sessions,
userOrgs, userOrgs,
userResources, userResources,
users users,
ResourceHeaderAuthExtendedCompatibility,
resourceHeaderAuthExtendedCompatibility
} from "@server/db"; } from "@server/db";
import { and, eq } from "drizzle-orm"; import { and, eq } from "drizzle-orm";
@@ -23,6 +27,7 @@ export type ResourceWithAuth = {
pincode: ResourcePincode | null; pincode: ResourcePincode | null;
password: ResourcePassword | null; password: ResourcePassword | null;
headerAuth: ResourceHeaderAuth | null; headerAuth: ResourceHeaderAuth | null;
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null
org: Org; org: Org;
}; };
@@ -52,7 +57,14 @@ export async function getResourceByDomain(
resourceHeaderAuth, resourceHeaderAuth,
eq(resourceHeaderAuth.resourceId, resources.resourceId) eq(resourceHeaderAuth.resourceId, resources.resourceId)
) )
.innerJoin(orgs, eq(orgs.orgId, resources.orgId)) .leftJoin(
resourceHeaderAuthExtendedCompatibility,
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId)
)
.innerJoin(
orgs,
eq(orgs.orgId, resources.orgId)
)
.where(eq(resources.fullDomain, domain)) .where(eq(resources.fullDomain, domain))
.limit(1); .limit(1);
@@ -65,6 +77,7 @@ export async function getResourceByDomain(
pincode: result.resourcePincode, pincode: result.resourcePincode,
password: result.resourcePassword, password: result.resourcePassword,
headerAuth: result.resourceHeaderAuth, headerAuth: result.resourceHeaderAuth,
headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility,
org: result.orgs org: result.orgs
}; };
} }

View File

@@ -12,22 +12,22 @@ import { no } from "zod/v4/locales";
export const domains = sqliteTable("domains", { export const domains = sqliteTable("domains", {
domainId: text("domainId").primaryKey(), domainId: text("domainId").primaryKey(),
baseDomain: text("baseDomain").notNull(), baseDomain: text("baseDomain").notNull(),
configManaged: integer("configManaged", { mode: "boolean" }) configManaged: integer("configManaged", {mode: "boolean"})
.notNull() .notNull()
.default(false), .default(false),
type: text("type"), // "ns", "cname", "wildcard" type: text("type"), // "ns", "cname", "wildcard"
verified: integer("verified", { mode: "boolean" }).notNull().default(false), verified: integer("verified", {mode: "boolean"}).notNull().default(false),
failed: integer("failed", { mode: "boolean" }).notNull().default(false), failed: integer("failed", {mode: "boolean"}).notNull().default(false),
tries: integer("tries").notNull().default(0), tries: integer("tries").notNull().default(0),
certResolver: text("certResolver"), certResolver: text("certResolver"),
preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" }) preferWildcardCert: integer("preferWildcardCert", {mode: "boolean"})
}); });
export const dnsRecords = sqliteTable("dnsRecords", { export const dnsRecords = sqliteTable("dnsRecords", {
id: integer("id").primaryKey({ autoIncrement: true }), id: integer("id").primaryKey({autoIncrement: true}),
domainId: text("domainId") domainId: text("domainId")
.notNull() .notNull()
.references(() => domains.domainId, { onDelete: "cascade" }), .references(() => domains.domainId, {onDelete: "cascade"}),
recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT" recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT"
baseDomain: text("baseDomain"), baseDomain: text("baseDomain"),
@@ -41,7 +41,7 @@ export const orgs = sqliteTable("orgs", {
subnet: text("subnet"), subnet: text("subnet"),
utilitySubnet: text("utilitySubnet"), // this is the subnet for utility addresses utilitySubnet: text("utilitySubnet"), // this is the subnet for utility addresses
createdAt: text("createdAt"), createdAt: text("createdAt"),
requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }), requireTwoFactor: integer("requireTwoFactor", {mode: "boolean"}),
maxSessionLengthHours: integer("maxSessionLengthHours"), // hours maxSessionLengthHours: integer("maxSessionLengthHours"), // hours
passwordExpiryDays: integer("passwordExpiryDays"), // days passwordExpiryDays: integer("passwordExpiryDays"), // days
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
@@ -58,23 +58,23 @@ export const orgs = sqliteTable("orgs", {
export const userDomains = sqliteTable("userDomains", { export const userDomains = sqliteTable("userDomains", {
userId: text("userId") userId: text("userId")
.notNull() .notNull()
.references(() => users.userId, { onDelete: "cascade" }), .references(() => users.userId, {onDelete: "cascade"}),
domainId: text("domainId") domainId: text("domainId")
.notNull() .notNull()
.references(() => domains.domainId, { onDelete: "cascade" }) .references(() => domains.domainId, {onDelete: "cascade"})
}); });
export const orgDomains = sqliteTable("orgDomains", { export const orgDomains = sqliteTable("orgDomains", {
orgId: text("orgId") orgId: text("orgId")
.notNull() .notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, {onDelete: "cascade"}),
domainId: text("domainId") domainId: text("domainId")
.notNull() .notNull()
.references(() => domains.domainId, { onDelete: "cascade" }) .references(() => domains.domainId, {onDelete: "cascade"})
}); });
export const sites = sqliteTable("sites", { export const sites = sqliteTable("sites", {
siteId: integer("siteId").primaryKey({ autoIncrement: true }), siteId: integer("siteId").primaryKey({autoIncrement: true}),
orgId: text("orgId") orgId: text("orgId")
.references(() => orgs.orgId, { .references(() => orgs.orgId, {
onDelete: "cascade" onDelete: "cascade"
@@ -91,7 +91,7 @@ export const sites = sqliteTable("sites", {
megabytesOut: integer("bytesOut").default(0), megabytesOut: integer("bytesOut").default(0),
lastBandwidthUpdate: text("lastBandwidthUpdate"), lastBandwidthUpdate: text("lastBandwidthUpdate"),
type: text("type").notNull(), // "newt" or "wireguard" 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 // 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 address: text("address"), // this is the address of the wireguard interface in newt
@@ -99,14 +99,14 @@ export const sites = sqliteTable("sites", {
publicKey: text("publicKey"), // TODO: Fix typo in publicKey publicKey: text("publicKey"), // TODO: Fix typo in publicKey
lastHolePunch: integer("lastHolePunch"), lastHolePunch: integer("lastHolePunch"),
listenPort: integer("listenPort"), listenPort: integer("listenPort"),
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" }) dockerSocketEnabled: integer("dockerSocketEnabled", {mode: "boolean"})
.notNull() .notNull()
.default(true) .default(true)
}); });
export const resources = sqliteTable("resources", { export const resources = sqliteTable("resources", {
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }), resourceId: integer("resourceId").primaryKey({autoIncrement: true}),
resourceGuid: text("resourceGuid", { length: 36 }) resourceGuid: text("resourceGuid", {length: 36})
.unique() .unique()
.notNull() .notNull()
.$defaultFn(() => randomUUID()), .$defaultFn(() => randomUUID()),
@@ -122,27 +122,27 @@ export const resources = sqliteTable("resources", {
domainId: text("domainId").references(() => domains.domainId, { domainId: text("domainId").references(() => domains.domainId, {
onDelete: "set null" onDelete: "set null"
}), }),
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false), ssl: integer("ssl", {mode: "boolean"}).notNull().default(false),
blockAccess: integer("blockAccess", { mode: "boolean" }) blockAccess: integer("blockAccess", {mode: "boolean"})
.notNull() .notNull()
.default(false), .default(false),
sso: integer("sso", { mode: "boolean" }).notNull().default(true), sso: integer("sso", {mode: "boolean"}).notNull().default(true),
http: integer("http", { mode: "boolean" }).notNull().default(true), http: integer("http", {mode: "boolean"}).notNull().default(true),
protocol: text("protocol").notNull(), protocol: text("protocol").notNull(),
proxyPort: integer("proxyPort"), proxyPort: integer("proxyPort"),
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" }) emailWhitelistEnabled: integer("emailWhitelistEnabled", {mode: "boolean"})
.notNull() .notNull()
.default(false), .default(false),
applyRules: integer("applyRules", { mode: "boolean" }) applyRules: integer("applyRules", {mode: "boolean"})
.notNull() .notNull()
.default(false), .default(false),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), enabled: integer("enabled", {mode: "boolean"}).notNull().default(true),
stickySession: integer("stickySession", { mode: "boolean" }) stickySession: integer("stickySession", {mode: "boolean"})
.notNull() .notNull()
.default(false), .default(false),
tlsServerName: text("tlsServerName"), tlsServerName: text("tlsServerName"),
setHostHeader: text("setHostHeader"), setHostHeader: text("setHostHeader"),
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true), enableProxy: integer("enableProxy", {mode: "boolean"}).default(true),
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
onDelete: "set null" onDelete: "set null"
}), }),
@@ -154,7 +154,7 @@ export const resources = sqliteTable("resources", {
}); });
export const targets = sqliteTable("targets", { export const targets = sqliteTable("targets", {
targetId: integer("targetId").primaryKey({ autoIncrement: true }), targetId: integer("targetId").primaryKey({autoIncrement: true}),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.references(() => resources.resourceId, { .references(() => resources.resourceId, {
onDelete: "cascade" onDelete: "cascade"
@@ -169,7 +169,7 @@ export const targets = sqliteTable("targets", {
method: text("method"), method: text("method"),
port: integer("port").notNull(), port: integer("port").notNull(),
internalPort: integer("internalPort"), internalPort: integer("internalPort"),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), enabled: integer("enabled", {mode: "boolean"}).notNull().default(true),
path: text("path"), path: text("path"),
pathMatchType: text("pathMatchType"), // exact, prefix, regex pathMatchType: text("pathMatchType"), // exact, prefix, regex
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
@@ -183,8 +183,8 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
}), }),
targetId: integer("targetId") targetId: integer("targetId")
.notNull() .notNull()
.references(() => targets.targetId, { onDelete: "cascade" }), .references(() => targets.targetId, {onDelete: "cascade"}),
hcEnabled: integer("hcEnabled", { mode: "boolean" }) hcEnabled: integer("hcEnabled", {mode: "boolean"})
.notNull() .notNull()
.default(false), .default(false),
hcPath: text("hcPath"), hcPath: text("hcPath"),
@@ -206,7 +206,7 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", {
}); });
export const exitNodes = sqliteTable("exitNodes", { export const exitNodes = sqliteTable("exitNodes", {
exitNodeId: integer("exitNodeId").primaryKey({ autoIncrement: true }), exitNodeId: integer("exitNodeId").primaryKey({autoIncrement: true}),
name: text("name").notNull(), name: text("name").notNull(),
address: text("address").notNull(), // this is the address of the wireguard interface in gerbil 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 endpoint: text("endpoint").notNull(), // this is how to reach gerbil externally - gets put into the wireguard config
@@ -214,7 +214,7 @@ export const exitNodes = sqliteTable("exitNodes", {
listenPort: integer("listenPort").notNull(), listenPort: integer("listenPort").notNull(),
reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control
maxConnections: integer("maxConnections"), maxConnections: integer("maxConnections"),
online: integer("online", { mode: "boolean" }).notNull().default(false), online: integer("online", {mode: "boolean"}).notNull().default(false),
lastPing: integer("lastPing"), lastPing: integer("lastPing"),
type: text("type").default("gerbil"), // gerbil, remoteExitNode type: text("type").default("gerbil"), // gerbil, remoteExitNode
region: text("region") region: text("region")
@@ -227,10 +227,10 @@ export const siteResources = sqliteTable("siteResources", {
}), }),
siteId: integer("siteId") siteId: integer("siteId")
.notNull() .notNull()
.references(() => sites.siteId, { onDelete: "cascade" }), .references(() => sites.siteId, {onDelete: "cascade"}),
orgId: text("orgId") orgId: text("orgId")
.notNull() .notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, {onDelete: "cascade"}),
niceId: text("niceId").notNull(), niceId: text("niceId").notNull(),
name: text("name").notNull(), name: text("name").notNull(),
mode: text("mode").notNull(), // "host" | "cidr" | "port" mode: text("mode").notNull(), // "host" | "cidr" | "port"
@@ -283,20 +283,20 @@ export const users = sqliteTable("user", {
onDelete: "cascade" onDelete: "cascade"
}), }),
passwordHash: text("passwordHash"), passwordHash: text("passwordHash"),
twoFactorEnabled: integer("twoFactorEnabled", { mode: "boolean" }) twoFactorEnabled: integer("twoFactorEnabled", {mode: "boolean"})
.notNull() .notNull()
.default(false), .default(false),
twoFactorSetupRequested: integer("twoFactorSetupRequested", { twoFactorSetupRequested: integer("twoFactorSetupRequested", {
mode: "boolean" mode: "boolean"
}).default(false), }).default(false),
twoFactorSecret: text("twoFactorSecret"), twoFactorSecret: text("twoFactorSecret"),
emailVerified: integer("emailVerified", { mode: "boolean" }) emailVerified: integer("emailVerified", {mode: "boolean"})
.notNull() .notNull()
.default(false), .default(false),
dateCreated: text("dateCreated").notNull(), dateCreated: text("dateCreated").notNull(),
termsAcceptedTimestamp: text("termsAcceptedTimestamp"), termsAcceptedTimestamp: text("termsAcceptedTimestamp"),
termsVersion: text("termsVersion"), termsVersion: text("termsVersion"),
serverAdmin: integer("serverAdmin", { mode: "boolean" }) serverAdmin: integer("serverAdmin", {mode: "boolean"})
.notNull() .notNull()
.default(false), .default(false),
lastPasswordChange: integer("lastPasswordChange") lastPasswordChange: integer("lastPasswordChange")
@@ -330,7 +330,7 @@ export const webauthnChallenge = sqliteTable("webauthnChallenge", {
export const setupTokens = sqliteTable("setupTokens", { export const setupTokens = sqliteTable("setupTokens", {
tokenId: text("tokenId").primaryKey(), tokenId: text("tokenId").primaryKey(),
token: text("token").notNull(), token: text("token").notNull(),
used: integer("used", { mode: "boolean" }).notNull().default(false), used: integer("used", {mode: "boolean"}).notNull().default(false),
dateCreated: text("dateCreated").notNull(), dateCreated: text("dateCreated").notNull(),
dateUsed: text("dateUsed") dateUsed: text("dateUsed")
}); });
@@ -369,7 +369,7 @@ export const clients = sqliteTable("clients", {
lastBandwidthUpdate: text("lastBandwidthUpdate"), lastBandwidthUpdate: text("lastBandwidthUpdate"),
lastPing: integer("lastPing"), lastPing: integer("lastPing"),
type: text("type").notNull(), // "olm" type: text("type").notNull(), // "olm"
online: integer("online", { mode: "boolean" }).notNull().default(false), online: integer("online", {mode: "boolean"}).notNull().default(false),
// endpoint: text("endpoint"), // endpoint: text("endpoint"),
lastHolePunch: integer("lastHolePunch") lastHolePunch: integer("lastHolePunch")
}); });
@@ -415,10 +415,10 @@ export const olms = sqliteTable("olms", {
}); });
export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", { export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
codeId: integer("id").primaryKey({ autoIncrement: true }), codeId: integer("id").primaryKey({autoIncrement: true}),
userId: text("userId") userId: text("userId")
.notNull() .notNull()
.references(() => users.userId, { onDelete: "cascade" }), .references(() => users.userId, {onDelete: "cascade"}),
codeHash: text("codeHash").notNull() codeHash: text("codeHash").notNull()
}); });
@@ -426,7 +426,7 @@ export const sessions = sqliteTable("session", {
sessionId: text("id").primaryKey(), sessionId: text("id").primaryKey(),
userId: text("userId") userId: text("userId")
.notNull() .notNull()
.references(() => users.userId, { onDelete: "cascade" }), .references(() => users.userId, {onDelete: "cascade"}),
expiresAt: integer("expiresAt").notNull(), expiresAt: integer("expiresAt").notNull(),
issuedAt: integer("issuedAt"), issuedAt: integer("issuedAt"),
deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" }) deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" })
@@ -438,7 +438,7 @@ export const newtSessions = sqliteTable("newtSession", {
sessionId: text("id").primaryKey(), sessionId: text("id").primaryKey(),
newtId: text("newtId") newtId: text("newtId")
.notNull() .notNull()
.references(() => newts.newtId, { onDelete: "cascade" }), .references(() => newts.newtId, {onDelete: "cascade"}),
expiresAt: integer("expiresAt").notNull() expiresAt: integer("expiresAt").notNull()
}); });
@@ -446,14 +446,14 @@ export const olmSessions = sqliteTable("clientSession", {
sessionId: text("id").primaryKey(), sessionId: text("id").primaryKey(),
olmId: text("olmId") olmId: text("olmId")
.notNull() .notNull()
.references(() => olms.olmId, { onDelete: "cascade" }), .references(() => olms.olmId, {onDelete: "cascade"}),
expiresAt: integer("expiresAt").notNull() expiresAt: integer("expiresAt").notNull()
}); });
export const userOrgs = sqliteTable("userOrgs", { export const userOrgs = sqliteTable("userOrgs", {
userId: text("userId") userId: text("userId")
.notNull() .notNull()
.references(() => users.userId, { onDelete: "cascade" }), .references(() => users.userId, {onDelete: "cascade"}),
orgId: text("orgId") orgId: text("orgId")
.references(() => orgs.orgId, { .references(() => orgs.orgId, {
onDelete: "cascade" onDelete: "cascade"
@@ -462,28 +462,28 @@ export const userOrgs = sqliteTable("userOrgs", {
roleId: integer("roleId") roleId: integer("roleId")
.notNull() .notNull()
.references(() => roles.roleId), .references(() => roles.roleId),
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false), isOwner: integer("isOwner", {mode: "boolean"}).notNull().default(false),
autoProvisioned: integer("autoProvisioned", { autoProvisioned: integer("autoProvisioned", {
mode: "boolean" mode: "boolean"
}).default(false) }).default(false)
}); });
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", { export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
codeId: integer("id").primaryKey({ autoIncrement: true }), codeId: integer("id").primaryKey({autoIncrement: true}),
userId: text("userId") userId: text("userId")
.notNull() .notNull()
.references(() => users.userId, { onDelete: "cascade" }), .references(() => users.userId, {onDelete: "cascade"}),
email: text("email").notNull(), email: text("email").notNull(),
code: text("code").notNull(), code: text("code").notNull(),
expiresAt: integer("expiresAt").notNull() expiresAt: integer("expiresAt").notNull()
}); });
export const passwordResetTokens = sqliteTable("passwordResetTokens", { export const passwordResetTokens = sqliteTable("passwordResetTokens", {
tokenId: integer("id").primaryKey({ autoIncrement: true }), tokenId: integer("id").primaryKey({autoIncrement: true}),
email: text("email").notNull(), email: text("email").notNull(),
userId: text("userId") userId: text("userId")
.notNull() .notNull()
.references(() => users.userId, { onDelete: "cascade" }), .references(() => users.userId, {onDelete: "cascade"}),
tokenHash: text("tokenHash").notNull(), tokenHash: text("tokenHash").notNull(),
expiresAt: integer("expiresAt").notNull() expiresAt: integer("expiresAt").notNull()
}); });
@@ -495,13 +495,13 @@ export const actions = sqliteTable("actions", {
}); });
export const roles = sqliteTable("roles", { export const roles = sqliteTable("roles", {
roleId: integer("roleId").primaryKey({ autoIncrement: true }), roleId: integer("roleId").primaryKey({autoIncrement: true}),
orgId: text("orgId") orgId: text("orgId")
.references(() => orgs.orgId, { .references(() => orgs.orgId, {
onDelete: "cascade" onDelete: "cascade"
}) })
.notNull(), .notNull(),
isAdmin: integer("isAdmin", { mode: "boolean" }), isAdmin: integer("isAdmin", {mode: "boolean"}),
name: text("name").notNull(), name: text("name").notNull(),
description: text("description") description: text("description")
}); });
@@ -509,92 +509,92 @@ export const roles = sqliteTable("roles", {
export const roleActions = sqliteTable("roleActions", { export const roleActions = sqliteTable("roleActions", {
roleId: integer("roleId") roleId: integer("roleId")
.notNull() .notNull()
.references(() => roles.roleId, { onDelete: "cascade" }), .references(() => roles.roleId, {onDelete: "cascade"}),
actionId: text("actionId") actionId: text("actionId")
.notNull() .notNull()
.references(() => actions.actionId, { onDelete: "cascade" }), .references(() => actions.actionId, {onDelete: "cascade"}),
orgId: text("orgId") orgId: text("orgId")
.notNull() .notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }) .references(() => orgs.orgId, {onDelete: "cascade"})
}); });
export const userActions = sqliteTable("userActions", { export const userActions = sqliteTable("userActions", {
userId: text("userId") userId: text("userId")
.notNull() .notNull()
.references(() => users.userId, { onDelete: "cascade" }), .references(() => users.userId, {onDelete: "cascade"}),
actionId: text("actionId") actionId: text("actionId")
.notNull() .notNull()
.references(() => actions.actionId, { onDelete: "cascade" }), .references(() => actions.actionId, {onDelete: "cascade"}),
orgId: text("orgId") orgId: text("orgId")
.notNull() .notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }) .references(() => orgs.orgId, {onDelete: "cascade"})
}); });
export const roleSites = sqliteTable("roleSites", { export const roleSites = sqliteTable("roleSites", {
roleId: integer("roleId") roleId: integer("roleId")
.notNull() .notNull()
.references(() => roles.roleId, { onDelete: "cascade" }), .references(() => roles.roleId, {onDelete: "cascade"}),
siteId: integer("siteId") siteId: integer("siteId")
.notNull() .notNull()
.references(() => sites.siteId, { onDelete: "cascade" }) .references(() => sites.siteId, {onDelete: "cascade"})
}); });
export const userSites = sqliteTable("userSites", { export const userSites = sqliteTable("userSites", {
userId: text("userId") userId: text("userId")
.notNull() .notNull()
.references(() => users.userId, { onDelete: "cascade" }), .references(() => users.userId, {onDelete: "cascade"}),
siteId: integer("siteId") siteId: integer("siteId")
.notNull() .notNull()
.references(() => sites.siteId, { onDelete: "cascade" }) .references(() => sites.siteId, {onDelete: "cascade"})
}); });
export const userClients = sqliteTable("userClients", { export const userClients = sqliteTable("userClients", {
userId: text("userId") userId: text("userId")
.notNull() .notNull()
.references(() => users.userId, { onDelete: "cascade" }), .references(() => users.userId, {onDelete: "cascade"}),
clientId: integer("clientId") clientId: integer("clientId")
.notNull() .notNull()
.references(() => clients.clientId, { onDelete: "cascade" }) .references(() => clients.clientId, {onDelete: "cascade"})
}); });
export const roleClients = sqliteTable("roleClients", { export const roleClients = sqliteTable("roleClients", {
roleId: integer("roleId") roleId: integer("roleId")
.notNull() .notNull()
.references(() => roles.roleId, { onDelete: "cascade" }), .references(() => roles.roleId, {onDelete: "cascade"}),
clientId: integer("clientId") clientId: integer("clientId")
.notNull() .notNull()
.references(() => clients.clientId, { onDelete: "cascade" }) .references(() => clients.clientId, {onDelete: "cascade"})
}); });
export const roleResources = sqliteTable("roleResources", { export const roleResources = sqliteTable("roleResources", {
roleId: integer("roleId") roleId: integer("roleId")
.notNull() .notNull()
.references(() => roles.roleId, { onDelete: "cascade" }), .references(() => roles.roleId, {onDelete: "cascade"}),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }) .references(() => resources.resourceId, {onDelete: "cascade"})
}); });
export const userResources = sqliteTable("userResources", { export const userResources = sqliteTable("userResources", {
userId: text("userId") userId: text("userId")
.notNull() .notNull()
.references(() => users.userId, { onDelete: "cascade" }), .references(() => users.userId, {onDelete: "cascade"}),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }) .references(() => resources.resourceId, {onDelete: "cascade"})
}); });
export const userInvites = sqliteTable("userInvites", { export const userInvites = sqliteTable("userInvites", {
inviteId: text("inviteId").primaryKey(), inviteId: text("inviteId").primaryKey(),
orgId: text("orgId") orgId: text("orgId")
.notNull() .notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, {onDelete: "cascade"}),
email: text("email").notNull(), email: text("email").notNull(),
expiresAt: integer("expiresAt").notNull(), expiresAt: integer("expiresAt").notNull(),
tokenHash: text("token").notNull(), tokenHash: text("token").notNull(),
roleId: integer("roleId") roleId: integer("roleId")
.notNull() .notNull()
.references(() => roles.roleId, { onDelete: "cascade" }) .references(() => roles.roleId, {onDelete: "cascade"})
}); });
export const resourcePincode = sqliteTable("resourcePincode", { export const resourcePincode = sqliteTable("resourcePincode", {
@@ -603,7 +603,7 @@ export const resourcePincode = sqliteTable("resourcePincode", {
}), }),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, {onDelete: "cascade"}),
pincodeHash: text("pincodeHash").notNull(), pincodeHash: text("pincodeHash").notNull(),
digitLength: integer("digitLength").notNull() digitLength: integer("digitLength").notNull()
}); });
@@ -614,7 +614,7 @@ export const resourcePassword = sqliteTable("resourcePassword", {
}), }),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, {onDelete: "cascade"}),
passwordHash: text("passwordHash").notNull() passwordHash: text("passwordHash").notNull()
}); });
@@ -624,18 +624,28 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", {
}), }),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, {onDelete: "cascade"}),
headerAuthHash: text("headerAuthHash").notNull() 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", { export const resourceAccessToken = sqliteTable("resourceAccessToken", {
accessTokenId: text("accessTokenId").primaryKey(), accessTokenId: text("accessTokenId").primaryKey(),
orgId: text("orgId") orgId: text("orgId")
.notNull() .notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, {onDelete: "cascade"}),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, {onDelete: "cascade"}),
tokenHash: text("tokenHash").notNull(), tokenHash: text("tokenHash").notNull(),
sessionLength: integer("sessionLength").notNull(), sessionLength: integer("sessionLength").notNull(),
expiresAt: integer("expiresAt"), expiresAt: integer("expiresAt"),
@@ -648,13 +658,13 @@ export const resourceSessions = sqliteTable("resourceSessions", {
sessionId: text("id").primaryKey(), sessionId: text("id").primaryKey(),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, {onDelete: "cascade"}),
expiresAt: integer("expiresAt").notNull(), expiresAt: integer("expiresAt").notNull(),
sessionLength: integer("sessionLength").notNull(), sessionLength: integer("sessionLength").notNull(),
doNotExtend: integer("doNotExtend", { mode: "boolean" }) doNotExtend: integer("doNotExtend", {mode: "boolean"})
.notNull() .notNull()
.default(false), .default(false),
isRequestToken: integer("isRequestToken", { mode: "boolean" }), isRequestToken: integer("isRequestToken", {mode: "boolean"}),
userSessionId: text("userSessionId").references(() => sessions.sessionId, { userSessionId: text("userSessionId").references(() => sessions.sessionId, {
onDelete: "cascade" onDelete: "cascade"
}), }),
@@ -686,11 +696,11 @@ export const resourceSessions = sqliteTable("resourceSessions", {
}); });
export const resourceWhitelist = sqliteTable("resourceWhitelist", { export const resourceWhitelist = sqliteTable("resourceWhitelist", {
whitelistId: integer("id").primaryKey({ autoIncrement: true }), whitelistId: integer("id").primaryKey({autoIncrement: true}),
email: text("email").notNull(), email: text("email").notNull(),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }) .references(() => resources.resourceId, {onDelete: "cascade"})
}); });
export const resourceOtp = sqliteTable("resourceOtp", { export const resourceOtp = sqliteTable("resourceOtp", {
@@ -699,7 +709,7 @@ export const resourceOtp = sqliteTable("resourceOtp", {
}), }),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, {onDelete: "cascade"}),
email: text("email").notNull(), email: text("email").notNull(),
otpHash: text("otpHash").notNull(), otpHash: text("otpHash").notNull(),
expiresAt: integer("expiresAt").notNull() expiresAt: integer("expiresAt").notNull()
@@ -711,11 +721,11 @@ export const versionMigrations = sqliteTable("versionMigrations", {
}); });
export const resourceRules = sqliteTable("resourceRules", { export const resourceRules = sqliteTable("resourceRules", {
ruleId: integer("ruleId").primaryKey({ autoIncrement: true }), ruleId: integer("ruleId").primaryKey({autoIncrement: true}),
resourceId: integer("resourceId") resourceId: integer("resourceId")
.notNull() .notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }), .references(() => resources.resourceId, {onDelete: "cascade"}),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), enabled: integer("enabled", {mode: "boolean"}).notNull().default(true),
priority: integer("priority").notNull(), priority: integer("priority").notNull(),
action: text("action").notNull(), // ACCEPT, DROP, PASS action: text("action").notNull(), // ACCEPT, DROP, PASS
match: text("match").notNull(), // CIDR, PATH, IP match: text("match").notNull(), // CIDR, PATH, IP
@@ -723,17 +733,17 @@ export const resourceRules = sqliteTable("resourceRules", {
}); });
export const supporterKey = sqliteTable("supporterKey", { export const supporterKey = sqliteTable("supporterKey", {
keyId: integer("keyId").primaryKey({ autoIncrement: true }), keyId: integer("keyId").primaryKey({autoIncrement: true}),
key: text("key").notNull(), key: text("key").notNull(),
githubUsername: text("githubUsername").notNull(), githubUsername: text("githubUsername").notNull(),
phrase: text("phrase"), phrase: text("phrase"),
tier: text("tier"), tier: text("tier"),
valid: integer("valid", { mode: "boolean" }).notNull().default(false) valid: integer("valid", {mode: "boolean"}).notNull().default(false)
}); });
// Identity Providers // Identity Providers
export const idp = sqliteTable("idp", { export const idp = sqliteTable("idp", {
idpId: integer("idpId").primaryKey({ autoIncrement: true }), idpId: integer("idpId").primaryKey({autoIncrement: true}),
name: text("name").notNull(), name: text("name").notNull(),
type: text("type").notNull(), type: text("type").notNull(),
defaultRoleMapping: text("defaultRoleMapping"), defaultRoleMapping: text("defaultRoleMapping"),
@@ -753,7 +763,7 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", {
variant: text("variant").notNull().default("oidc"), variant: text("variant").notNull().default("oidc"),
idpId: integer("idpId") idpId: integer("idpId")
.notNull() .notNull()
.references(() => idp.idpId, { onDelete: "cascade" }), .references(() => idp.idpId, {onDelete: "cascade"}),
clientId: text("clientId").notNull(), clientId: text("clientId").notNull(),
clientSecret: text("clientSecret").notNull(), clientSecret: text("clientSecret").notNull(),
authUrl: text("authUrl").notNull(), authUrl: text("authUrl").notNull(),
@@ -781,22 +791,22 @@ export const apiKeys = sqliteTable("apiKeys", {
apiKeyHash: text("apiKeyHash").notNull(), apiKeyHash: text("apiKeyHash").notNull(),
lastChars: text("lastChars").notNull(), lastChars: text("lastChars").notNull(),
createdAt: text("dateCreated").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", { export const apiKeyActions = sqliteTable("apiKeyActions", {
apiKeyId: text("apiKeyId") apiKeyId: text("apiKeyId")
.notNull() .notNull()
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }), .references(() => apiKeys.apiKeyId, {onDelete: "cascade"}),
actionId: text("actionId") actionId: text("actionId")
.notNull() .notNull()
.references(() => actions.actionId, { onDelete: "cascade" }) .references(() => actions.actionId, {onDelete: "cascade"})
}); });
export const apiKeyOrg = sqliteTable("apiKeyOrg", { export const apiKeyOrg = sqliteTable("apiKeyOrg", {
apiKeyId: text("apiKeyId") apiKeyId: text("apiKeyId")
.notNull() .notNull()
.references(() => apiKeys.apiKeyId, { onDelete: "cascade" }), .references(() => apiKeys.apiKeyId, {onDelete: "cascade"}),
orgId: text("orgId") orgId: text("orgId")
.references(() => orgs.orgId, { .references(() => orgs.orgId, {
onDelete: "cascade" onDelete: "cascade"
@@ -807,10 +817,10 @@ export const apiKeyOrg = sqliteTable("apiKeyOrg", {
export const idpOrg = sqliteTable("idpOrg", { export const idpOrg = sqliteTable("idpOrg", {
idpId: integer("idpId") idpId: integer("idpId")
.notNull() .notNull()
.references(() => idp.idpId, { onDelete: "cascade" }), .references(() => idp.idpId, {onDelete: "cascade"}),
orgId: text("orgId") orgId: text("orgId")
.notNull() .notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }), .references(() => orgs.orgId, {onDelete: "cascade"}),
roleMapping: text("roleMapping"), roleMapping: text("roleMapping"),
orgMapping: text("orgMapping") orgMapping: text("orgMapping")
}); });
@@ -828,19 +838,19 @@ export const blueprints = sqliteTable("blueprints", {
name: text("name").notNull(), name: text("name").notNull(),
source: text("source").notNull(), source: text("source").notNull(),
createdAt: integer("createdAt").notNull(), createdAt: integer("createdAt").notNull(),
succeeded: integer("succeeded", { mode: "boolean" }).notNull(), succeeded: integer("succeeded", {mode: "boolean"}).notNull(),
contents: text("contents").notNull(), contents: text("contents").notNull(),
message: text("message") message: text("message")
}); });
export const requestAuditLog = sqliteTable( export const requestAuditLog = sqliteTable(
"requestAuditLog", "requestAuditLog",
{ {
id: integer("id").primaryKey({ autoIncrement: true }), id: integer("id").primaryKey({autoIncrement: true}),
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
orgId: text("orgId").references(() => orgs.orgId, { orgId: text("orgId").references(() => orgs.orgId, {
onDelete: "cascade" onDelete: "cascade"
}), }),
action: integer("action", { mode: "boolean" }).notNull(), action: integer("action", {mode: "boolean"}).notNull(),
reason: integer("reason").notNull(), reason: integer("reason").notNull(),
actorType: text("actorType"), actorType: text("actorType"),
actor: text("actor"), actor: text("actor"),
@@ -857,7 +867,7 @@ export const requestAuditLog = sqliteTable(
host: text("host"), host: text("host"),
path: text("path"), path: text("path"),
method: text("method"), method: text("method"),
tls: integer("tls", { mode: "boolean" }) tls: integer("tls", {mode: "boolean"})
}, },
(table) => [ (table) => [
index("idx_requestAuditLog_timestamp").on(table.timestamp), index("idx_requestAuditLog_timestamp").on(table.timestamp),
@@ -913,6 +923,7 @@ export type ResourceSession = InferSelectModel<typeof resourceSessions>;
export type ResourcePincode = InferSelectModel<typeof resourcePincode>; export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
export type ResourcePassword = InferSelectModel<typeof resourcePassword>; export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>; export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<typeof resourceHeaderAuthExtendedCompatibility>;
export type ResourceOtp = InferSelectModel<typeof resourceOtp>; export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>; export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>; export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;

View File

@@ -2,7 +2,7 @@ import {
domains, domains,
orgDomains, orgDomains,
Resource, Resource,
resourceHeaderAuth, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility,
resourcePincode, resourcePincode,
resourceRules, resourceRules,
resourceWhitelist, resourceWhitelist,
@@ -16,8 +16,8 @@ import {
userResources, userResources,
users users
} from "@server/db"; } from "@server/db";
import { resources, targets, sites } from "@server/db"; import {resources, targets, sites} from "@server/db";
import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm"; import {eq, and, asc, or, ne, count, isNotNull} from "drizzle-orm";
import { import {
Config, Config,
ConfigSchema, ConfigSchema,
@@ -25,12 +25,12 @@ import {
TargetData TargetData
} from "./types"; } from "./types";
import logger from "@server/logger"; import logger from "@server/logger";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; import {createCertificate} from "#dynamic/routers/certificates/createCertificate";
import { pickPort } from "@server/routers/target/helpers"; import {pickPort} from "@server/routers/target/helpers";
import { resourcePassword } from "@server/db"; import {resourcePassword} from "@server/db";
import { hashPassword } from "@server/auth/password"; import {hashPassword} from "@server/auth/password";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; import {isValidCIDR, isValidIP, isValidUrlGlobPattern} from "../validators";
import { get } from "http"; import {get} from "http";
export type ProxyResourcesResults = { export type ProxyResourcesResults = {
proxyResource: Resource; proxyResource: Resource;
@@ -63,7 +63,7 @@ export async function updateProxyResources(
if (targetSiteId) { if (targetSiteId) {
// Look up site by niceId // Look up site by niceId
[site] = await trx [site] = await trx
.select({ siteId: sites.siteId }) .select({siteId: sites.siteId})
.from(sites) .from(sites)
.where( .where(
and( and(
@@ -75,7 +75,7 @@ export async function updateProxyResources(
} else if (siteId) { } else if (siteId) {
// Use the provided siteId directly, but verify it belongs to the org // Use the provided siteId directly, but verify it belongs to the org
[site] = await trx [site] = await trx
.select({ siteId: sites.siteId }) .select({siteId: sites.siteId})
.from(sites) .from(sites)
.where( .where(
and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)) and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))
@@ -93,7 +93,7 @@ export async function updateProxyResources(
let internalPortToCreate; let internalPortToCreate;
if (!targetData["internal-port"]) { if (!targetData["internal-port"]) {
const { internalPort, targetIps } = await pickPort( const {internalPort, targetIps} = await pickPort(
site.siteId!, site.siteId!,
trx trx
); );
@@ -228,7 +228,7 @@ export async function updateProxyResources(
tlsServerName: resourceData["tls-server-name"] || null, tlsServerName: resourceData["tls-server-name"] || null,
emailWhitelistEnabled: resourceData.auth?.[ emailWhitelistEnabled: resourceData.auth?.[
"whitelist-users" "whitelist-users"
] ]
? resourceData.auth["whitelist-users"].length > 0 ? resourceData.auth["whitelist-users"].length > 0
: false, : false,
headers: headers || null, headers: headers || null,
@@ -287,21 +287,39 @@ export async function updateProxyResources(
existingResource.resourceId existingResource.resourceId
) )
); );
await trx
.delete(resourceHeaderAuthExtendedCompatibility)
.where(
eq(
resourceHeaderAuthExtendedCompatibility.resourceId,
existingResource.resourceId
)
);
if (resourceData.auth?.["basic-auth"]) { if (resourceData.auth?.["basic-auth"]) {
const headerAuthUser = const headerAuthUser =
resourceData.auth?.["basic-auth"]?.user; resourceData.auth?.["basic-auth"]?.user;
const headerAuthPassword = const headerAuthPassword =
resourceData.auth?.["basic-auth"]?.password; resourceData.auth?.["basic-auth"]?.password;
if (headerAuthUser && headerAuthPassword) { const headerAuthExtendedCompatibility =
resourceData.auth?.["basic-auth"]?.extendedCompatibility;
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) {
const headerAuthHash = await hashPassword( const headerAuthHash = await hashPassword(
Buffer.from( Buffer.from(
`${headerAuthUser}:${headerAuthPassword}` `${headerAuthUser}:${headerAuthPassword}`
).toString("base64") ).toString("base64")
); );
await trx.insert(resourceHeaderAuth).values({ await Promise.all([
resourceId: existingResource.resourceId, trx.insert(resourceHeaderAuth).values({
headerAuthHash resourceId: existingResource.resourceId,
}); headerAuthHash
}),
trx.insert(resourceHeaderAuthExtendedCompatibility).values({
resourceId: existingResource.resourceId,
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility
})
]);
} }
} }
@@ -362,7 +380,7 @@ export async function updateProxyResources(
if (targetSiteId) { if (targetSiteId) {
// Look up site by niceId // Look up site by niceId
[site] = await trx [site] = await trx
.select({ siteId: sites.siteId }) .select({siteId: sites.siteId})
.from(sites) .from(sites)
.where( .where(
and( and(
@@ -374,7 +392,7 @@ export async function updateProxyResources(
} else if (siteId) { } else if (siteId) {
// Use the provided siteId directly, but verify it belongs to the org // Use the provided siteId directly, but verify it belongs to the org
[site] = await trx [site] = await trx
.select({ siteId: sites.siteId }) .select({siteId: sites.siteId})
.from(sites) .from(sites)
.where( .where(
and( and(
@@ -419,7 +437,7 @@ export async function updateProxyResources(
if (checkIfTargetChanged(existingTarget, updatedTarget)) { if (checkIfTargetChanged(existingTarget, updatedTarget)) {
let internalPortToUpdate; let internalPortToUpdate;
if (!targetData["internal-port"]) { if (!targetData["internal-port"]) {
const { internalPort, targetIps } = await pickPort( const {internalPort, targetIps} = await pickPort(
site.siteId!, site.siteId!,
trx trx
); );
@@ -656,18 +674,25 @@ export async function updateProxyResources(
const headerAuthUser = resourceData.auth?.["basic-auth"]?.user; const headerAuthUser = resourceData.auth?.["basic-auth"]?.user;
const headerAuthPassword = const headerAuthPassword =
resourceData.auth?.["basic-auth"]?.password; resourceData.auth?.["basic-auth"]?.password;
const headerAuthExtendedCompatibility = resourceData.auth?.["basic-auth"]?.extendedCompatibility;
if (headerAuthUser && headerAuthPassword) { if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) {
const headerAuthHash = await hashPassword( const headerAuthHash = await hashPassword(
Buffer.from( Buffer.from(
`${headerAuthUser}:${headerAuthPassword}` `${headerAuthUser}:${headerAuthPassword}`
).toString("base64") ).toString("base64")
); );
await trx.insert(resourceHeaderAuth).values({ await Promise.all([
resourceId: newResource.resourceId, trx.insert(resourceHeaderAuth).values({
headerAuthHash resourceId: newResource.resourceId,
}); headerAuthHash
}),
trx.insert(resourceHeaderAuthExtendedCompatibility).values({
resourceId: newResource.resourceId,
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility
}),
]);
} }
} }
@@ -1018,7 +1043,7 @@ async function getDomain(
trx: Transaction trx: Transaction
) { ) {
const [fullDomainExists] = await trx const [fullDomainExists] = await trx
.select({ resourceId: resources.resourceId }) .select({resourceId: resources.resourceId})
.from(resources) .from(resources)
.where( .where(
and( and(

View File

@@ -53,12 +53,11 @@ export const AuthSchema = z.object({
// pincode has to have 6 digits // pincode has to have 6 digits
pincode: z.number().min(100000).max(999999).optional(), pincode: z.number().min(100000).max(999999).optional(),
password: z.string().min(1).optional(), password: z.string().min(1).optional(),
"basic-auth": z "basic-auth": z.object({
.object({ user: z.string().min(1),
user: z.string().min(1), password: z.string().min(1),
password: z.string().min(1) extendedCompatibility: z.boolean().default(false)
}) }).optional(),
.optional(),
"sso-enabled": z.boolean().optional().default(false), "sso-enabled": z.boolean().optional().default(false),
"sso-roles": z "sso-roles": z
.array(z.string()) .array(z.string())

View File

@@ -36,8 +36,10 @@ import {
LoginPage, LoginPage,
resourceHeaderAuth, resourceHeaderAuth,
ResourceHeaderAuth, ResourceHeaderAuth,
resourceHeaderAuthExtendedCompatibility,
ResourceHeaderAuthExtendedCompatibility,
orgs, orgs,
requestAuditLog requestAuditLog,
} from "@server/db"; } from "@server/db";
import { import {
resources, resources,
@@ -175,6 +177,7 @@ export type ResourceWithAuth = {
pincode: ResourcePincode | null; pincode: ResourcePincode | null;
password: ResourcePassword | null; password: ResourcePassword | null;
headerAuth: ResourceHeaderAuth | null; headerAuth: ResourceHeaderAuth | null;
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
}; };
export type UserSessionWithUser = { export type UserSessionWithUser = {
@@ -498,6 +501,10 @@ hybridRouter.get(
resourceHeaderAuth, resourceHeaderAuth,
eq(resourceHeaderAuth.resourceId, resources.resourceId) eq(resourceHeaderAuth.resourceId, resources.resourceId)
) )
.leftJoin(
resourceHeaderAuthExtendedCompatibility,
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId)
)
.where(eq(resources.fullDomain, domain)) .where(eq(resources.fullDomain, domain))
.limit(1); .limit(1);
@@ -530,7 +537,8 @@ hybridRouter.get(
resource: result.resources, resource: result.resources,
pincode: result.resourcePincode, pincode: result.resourcePincode,
password: result.resourcePassword, password: result.resourcePassword,
headerAuth: result.resourceHeaderAuth headerAuth: result.resourceHeaderAuth,
headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility
}; };
return response<ResourceWithAuth>(res, { return response<ResourceWithAuth>(res, {

View File

@@ -10,7 +10,7 @@ Reasons:
100 - Allowed by Rule 100 - Allowed by Rule
101 - Allowed No Auth 101 - Allowed No Auth
102 - Valid Access Token 102 - Valid Access Token
103 - Valid header auth 103 - Valid Header Auth (HTTP Basic Auth)
104 - Valid Pincode 104 - Valid Pincode
105 - Valid Password 105 - Valid Password
106 - Valid email 106 - Valid email

View File

@@ -13,7 +13,7 @@ import {
LoginPage, LoginPage,
Org, Org,
Resource, Resource,
ResourceHeaderAuth, ResourceHeaderAuth, ResourceHeaderAuthExtendedCompatibility,
ResourcePassword, ResourcePassword,
ResourcePincode, ResourcePincode,
ResourceRule, ResourceRule,
@@ -66,6 +66,7 @@ type BasicUserData = {
export type VerifyUserResponse = { export type VerifyUserResponse = {
valid: boolean; valid: boolean;
headerAuthChallenged?: boolean;
redirectUrl?: string; redirectUrl?: string;
userData?: BasicUserData; userData?: BasicUserData;
}; };
@@ -147,6 +148,7 @@ export async function verifyResourceSession(
pincode: ResourcePincode | null; pincode: ResourcePincode | null;
password: ResourcePassword | null; password: ResourcePassword | null;
headerAuth: ResourceHeaderAuth | null; headerAuth: ResourceHeaderAuth | null;
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
org: Org; org: Org;
} }
| undefined = cache.get(resourceCacheKey); | undefined = cache.get(resourceCacheKey);
@@ -176,7 +178,7 @@ export async function verifyResourceSession(
cache.set(resourceCacheKey, resourceData, 5); cache.set(resourceCacheKey, resourceData, 5);
} }
const { resource, pincode, password, headerAuth } = resourceData; const { resource, pincode, password, headerAuth, headerAuthExtendedCompatibility } = resourceData;
if (!resource) { if (!resource) {
logger.debug(`Resource not found ${cleanHost}`); logger.debug(`Resource not found ${cleanHost}`);
@@ -456,7 +458,8 @@ export async function verifyResourceSession(
!sso && !sso &&
!pincode && !pincode &&
!password && !password &&
!resource.emailWhitelistEnabled !resource.emailWhitelistEnabled &&
!headerAuthExtendedCompatibility?.extendedCompatibilityIsActivated
) { ) {
logRequestAudit( logRequestAudit(
{ {
@@ -471,13 +474,15 @@ export async function verifyResourceSession(
return notAllowed(res); 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 there are no other auth methods we need to return unauthorized if nothing is provided
if ( if (
!sso && !sso &&
!pincode && !pincode &&
!password && !password &&
!resource.emailWhitelistEnabled !resource.emailWhitelistEnabled &&
!headerAuthExtendedCompatibility?.extendedCompatibilityIsActivated
) { ) {
logRequestAudit( logRequestAudit(
{ {
@@ -563,7 +568,7 @@ export async function verifyResourceSession(
} }
if (resourceSession) { 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( const accessPolicy = await enforceResourceSessionLength(
resourceSession, resourceSession,
resourceData.org resourceData.org
@@ -707,6 +712,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"); logger.debug("No more auth to check, resource not allowed");
if (config.getRawConfig().app.log_failed_attempts) { if (config.getRawConfig().app.log_failed_attempts) {
@@ -839,6 +849,46 @@ function allowed(res: Response, userData?: BasicUserData) {
return response<VerifyUserResponse>(res, data); return response<VerifyUserResponse>(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<VerifyUserResponse>(res, data);
}
async function isUserAllowedToAccessResource( async function isUserAllowedToAccessResource(
userSessionId: string, userSessionId: string,
resource: Resource, resource: Resource,

View File

@@ -1,19 +1,19 @@
import { Request, Response, NextFunction } from "express"; import {Request, Response, NextFunction} from "express";
import { z } from "zod"; import {z} from "zod";
import { import {
db, db,
resourceHeaderAuth, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility,
resourcePassword, resourcePassword,
resourcePincode, resourcePincode,
resources resources
} from "@server/db"; } from "@server/db";
import { eq } from "drizzle-orm"; import {eq} from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { fromError } from "zod-validation-error"; import {fromError} from "zod-validation-error";
import logger from "@server/logger"; import logger from "@server/logger";
import { build } from "@server/build"; import {build} from "@server/build";
const getResourceAuthInfoSchema = z.strictObject({ const getResourceAuthInfoSchema = z.strictObject({
resourceGuid: z.string() resourceGuid: z.string()
@@ -27,6 +27,7 @@ export type GetResourceAuthInfoResponse = {
password: boolean; password: boolean;
pincode: boolean; pincode: boolean;
headerAuth: boolean; headerAuth: boolean;
headerAuthExtendedCompatibility: boolean;
sso: boolean; sso: boolean;
blockAccess: boolean; blockAccess: boolean;
url: string; url: string;
@@ -51,53 +52,68 @@ export async function getResourceAuthInfo(
); );
} }
const { resourceGuid } = parsedParams.data; const {resourceGuid} = parsedParams.data;
const isGuidInteger = /^\d+$/.test(resourceGuid); const isGuidInteger = /^\d+$/.test(resourceGuid);
const [result] = const [result] =
isGuidInteger && build === "saas" isGuidInteger && build === "saas"
? await db ? await db
.select() .select()
.from(resources) .from(resources)
.leftJoin( .leftJoin(
resourcePincode, resourcePincode,
eq(resourcePincode.resourceId, resources.resourceId) eq(resourcePincode.resourceId, resources.resourceId)
) )
.leftJoin( .leftJoin(
resourcePassword, resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId) eq(resourcePassword.resourceId, resources.resourceId)
) )
.leftJoin( .leftJoin(
resourceHeaderAuth, resourceHeaderAuth,
eq( eq(
resourceHeaderAuth.resourceId, resourceHeaderAuth.resourceId,
resources.resourceId resources.resourceId
) )
) )
.where(eq(resources.resourceId, Number(resourceGuid))) .leftJoin(
.limit(1) resourceHeaderAuthExtendedCompatibility,
eq(
resourceHeaderAuthExtendedCompatibility.resourceId,
resources.resourceId
)
)
.where(eq(resources.resourceId, Number(resourceGuid)))
.limit(1)
: await db : await db
.select() .select()
.from(resources) .from(resources)
.leftJoin( .leftJoin(
resourcePincode, resourcePincode,
eq(resourcePincode.resourceId, resources.resourceId) eq(resourcePincode.resourceId, resources.resourceId)
) )
.leftJoin( .leftJoin(
resourcePassword, resourcePassword,
eq(resourcePassword.resourceId, resources.resourceId) eq(resourcePassword.resourceId, resources.resourceId)
) )
.leftJoin(
resourceHeaderAuth, .leftJoin(
eq( resourceHeaderAuth,
resourceHeaderAuth.resourceId, eq(
resources.resourceId resourceHeaderAuth.resourceId,
) resources.resourceId
) )
.where(eq(resources.resourceGuid, resourceGuid)) )
.limit(1); .leftJoin(
resourceHeaderAuthExtendedCompatibility,
eq(
resourceHeaderAuthExtendedCompatibility.resourceId,
resources.resourceId
)
)
.where(eq(resources.resourceGuid, resourceGuid))
.limit(1);
const resource = result?.resources; const resource = result?.resources;
if (!resource) { if (!resource) {
@@ -109,6 +125,7 @@ export async function getResourceAuthInfo(
const pincode = result?.resourcePincode; const pincode = result?.resourcePincode;
const password = result?.resourcePassword; const password = result?.resourcePassword;
const headerAuth = result?.resourceHeaderAuth; const headerAuth = result?.resourceHeaderAuth;
const headerAuthExtendedCompatibility = result?.resourceHeaderAuthExtendedCompatibility;
const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
@@ -121,6 +138,7 @@ export async function getResourceAuthInfo(
password: password !== null, password: password !== null,
pincode: pincode !== null, pincode: pincode !== null,
headerAuth: headerAuth !== null, headerAuth: headerAuth !== null,
headerAuthExtendedCompatibility: headerAuthExtendedCompatibility !== null,
sso: resource.sso, sso: resource.sso,
blockAccess: resource.blockAccess, blockAccess: resource.blockAccess,
url, url,

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import { db, resourceHeaderAuth } from "@server/db"; import {db, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility} from "@server/db";
import { import {
resources, resources,
userResources, userResources,
@@ -109,7 +109,7 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
domainId: resources.domainId, domainId: resources.domainId,
niceId: resources.niceId, niceId: resources.niceId,
headerAuthId: resourceHeaderAuth.headerAuthId, headerAuthId: resourceHeaderAuth.headerAuthId,
headerAuthExtendedCompatibilityId: resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId,
targetId: targets.targetId, targetId: targets.targetId,
targetIp: targets.ip, targetIp: targets.ip,
targetPort: targets.port, targetPort: targets.port,
@@ -131,6 +131,10 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
resourceHeaderAuth, resourceHeaderAuth,
eq(resourceHeaderAuth.resourceId, resources.resourceId) eq(resourceHeaderAuth.resourceId, resources.resourceId)
) )
.leftJoin(
resourceHeaderAuthExtendedCompatibility,
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId)
)
.leftJoin(targets, eq(targets.resourceId, resources.resourceId)) .leftJoin(targets, eq(targets.resourceId, resources.resourceId))
.leftJoin( .leftJoin(
targetHealthCheck, targetHealthCheck,

View File

@@ -1,14 +1,14 @@
import { Request, Response, NextFunction } from "express"; import {Request, Response, NextFunction} from "express";
import { z } from "zod"; import {z} from "zod";
import { db, resourceHeaderAuth } from "@server/db"; import {db, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility} from "@server/db";
import { eq } from "drizzle-orm"; import {eq} from "drizzle-orm";
import HttpCode from "@server/types/HttpCode"; import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import { fromError } from "zod-validation-error"; import {fromError} from "zod-validation-error";
import { response } from "@server/lib/response"; import {response} from "@server/lib/response";
import logger from "@server/logger"; import logger from "@server/logger";
import { hashPassword } from "@server/auth/password"; import {hashPassword} from "@server/auth/password";
import { OpenAPITags, registry } from "@server/openApi"; import {OpenAPITags, registry} from "@server/openApi";
const setResourceAuthMethodsParamsSchema = z.object({ const setResourceAuthMethodsParamsSchema = z.object({
resourceId: z.string().transform(Number).pipe(z.int().positive()) resourceId: z.string().transform(Number).pipe(z.int().positive())
@@ -16,7 +16,8 @@ const setResourceAuthMethodsParamsSchema = z.object({
const setResourceAuthMethodsBodySchema = z.strictObject({ const setResourceAuthMethodsBodySchema = z.strictObject({
user: z.string().min(4).max(100).nullable(), user: z.string().min(4).max(100).nullable(),
password: z.string().min(4).max(100).nullable() password: z.string().min(4).max(100).nullable(),
extendedCompatibility: z.boolean().nullable()
}); });
registry.registerPath({ registry.registerPath({
@@ -66,23 +67,29 @@ export async function setResourceHeaderAuth(
); );
} }
const { resourceId } = parsedParams.data; const {resourceId} = parsedParams.data;
const { user, password } = parsedBody.data; const {user, password, extendedCompatibility} = parsedBody.data;
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
await trx await trx
.delete(resourceHeaderAuth) .delete(resourceHeaderAuth)
.where(eq(resourceHeaderAuth.resourceId, resourceId)); .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( const headerAuthHash = await hashPassword(Buffer.from(`${user}:${password}`).toString("base64"));
Buffer.from(`${user}:${password}`).toString("base64")
);
await trx await Promise.all([
.insert(resourceHeaderAuth) trx
.values({ resourceId, headerAuthHash }); .insert(resourceHeaderAuth)
.values({resourceId, headerAuthHash}),
trx
.insert(resourceHeaderAuthExtendedCompatibility)
.values({resourceId, extendedCompatibilityIsActivated: extendedCompatibility})
]);
} }
}); });
return response(res, { return response(res, {

View File

@@ -397,7 +397,8 @@ export default function ResourceAuthenticationPage() {
api.post(`/resource/${resource.resourceId}/header-auth`, { api.post(`/resource/${resource.resourceId}/header-auth`, {
user: null, user: null,
password: null password: null,
extendedCompatibility: null,
}) })
.then(() => { .then(() => {
toast({ toast({

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Button } from "@app/components/ui/button"; import {Button} from "@app/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
@@ -9,12 +9,12 @@ import {
FormLabel, FormLabel,
FormMessage FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import { Input } from "@app/components/ui/input"; import {Input} from "@app/components/ui/input";
import { toast } from "@app/hooks/useToast"; import {toast} from "@app/hooks/useToast";
import { zodResolver } from "@hookform/resolvers/zod"; import {zodResolver} from "@hookform/resolvers/zod";
import { useEffect, useState } from "react"; import {useEffect, useState} from "react";
import { useForm } from "react-hook-form"; import {useForm} from "react-hook-form";
import { z } from "zod"; import {z} from "zod";
import { import {
Credenza, Credenza,
CredenzaBody, CredenzaBody,
@@ -25,23 +25,27 @@ import {
CredenzaHeader, CredenzaHeader,
CredenzaTitle CredenzaTitle
} from "@app/components/Credenza"; } from "@app/components/Credenza";
import { formatAxiosError } from "@app/lib/api"; import {formatAxiosError} from "@app/lib/api";
import { AxiosResponse } from "axios"; import {AxiosResponse} from "axios";
import { Resource } from "@server/db"; import {Resource} from "@server/db";
import { createApiClient } from "@app/lib/api"; import {createApiClient} from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext"; import {useEnvContext} from "@app/hooks/useEnvContext";
import { useTranslations } from "next-intl"; import {useTranslations} from "next-intl";
import {SwitchInput} from "@/components/SwitchInput";
import {InfoPopup} from "@/components/ui/info-popup";
const setHeaderAuthFormSchema = z.object({ const setHeaderAuthFormSchema = z.object({
user: z.string().min(4).max(100), 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<typeof setHeaderAuthFormSchema>; type SetHeaderAuthFormValues = z.infer<typeof setHeaderAuthFormSchema>;
const defaultValues: Partial<SetHeaderAuthFormValues> = { const defaultValues: Partial<SetHeaderAuthFormValues> = {
user: "", user: "",
password: "" password: "",
extendedCompatibility: false
}; };
type SetHeaderAuthFormProps = { type SetHeaderAuthFormProps = {
@@ -52,11 +56,11 @@ type SetHeaderAuthFormProps = {
}; };
export default function SetResourceHeaderAuthForm({ export default function SetResourceHeaderAuthForm({
open, open,
setOpen, setOpen,
resourceId, resourceId,
onSetHeaderAuth onSetHeaderAuth
}: SetHeaderAuthFormProps) { }: SetHeaderAuthFormProps) {
const api = createApiClient(useEnvContext()); const api = createApiClient(useEnvContext());
const t = useTranslations(); const t = useTranslations();
@@ -78,23 +82,11 @@ export default function SetResourceHeaderAuthForm({
async function onSubmit(data: SetHeaderAuthFormValues) { async function onSubmit(data: SetHeaderAuthFormValues) {
setLoading(true); setLoading(true);
api.post<AxiosResponse<Resource>>( api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/header-auth`, {
`/resource/${resourceId}/header-auth`, user: data.user,
{ password: data.password,
user: data.user, extendedCompatibility: data.extendedCompatibility
password: data.password })
}
)
.catch((e) => {
toast({
variant: "destructive",
title: t("resourceErrorHeaderAuthSetup"),
description: formatAxiosError(
e,
t("resourceErrorHeaderAuthSetupDescription")
)
});
})
.then(() => { .then(() => {
toast({ toast({
title: t("resourceHeaderAuthSetup"), title: t("resourceHeaderAuthSetup"),
@@ -105,6 +97,16 @@ export default function SetResourceHeaderAuthForm({
onSetHeaderAuth(); onSetHeaderAuth();
} }
}) })
.catch((e) => {
toast({
variant: "destructive",
title: t('resourceErrorHeaderAuthSetup'),
description: formatAxiosError(
e,
t('resourceErrorHeaderAuthSetupDescription')
)
});
})
.finally(() => setLoading(false)); .finally(() => setLoading(false));
} }
@@ -137,7 +139,7 @@ export default function SetResourceHeaderAuthForm({
<FormField <FormField
control={form.control} control={form.control}
name="user" name="user"
render={({ field }) => ( render={({field}) => (
<FormItem> <FormItem>
<FormLabel>{t("user")}</FormLabel> <FormLabel>{t("user")}</FormLabel>
<FormControl> <FormControl>
@@ -147,14 +149,14 @@ export default function SetResourceHeaderAuthForm({
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage/>
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="password" name="password"
render={({ field }) => ( render={({field}) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>
{t("password")} {t("password")}
@@ -166,7 +168,25 @@ export default function SetResourceHeaderAuthForm({
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="extendedCompatibility"
render={({field}) => (
<FormItem>
<FormControl>
<SwitchInput
id="header-auth-compatibility-toggle"
label={t("headerAuthCompatibility")}
info={t('headerAuthCompatibilityInfo')}
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage/>
</FormItem> </FormItem>
)} )}
/> />

View File

@@ -78,16 +78,6 @@ export default function SetResourcePasswordForm({
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/password`, { api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/password`, {
password: data.password password: data.password
}) })
.catch((e) => {
toast({
variant: "destructive",
title: t("resourceErrorPasswordSetup"),
description: formatAxiosError(
e,
t("resourceErrorPasswordSetupDescription")
)
});
})
.then(() => { .then(() => {
toast({ toast({
title: t("resourcePasswordSetup"), title: t("resourcePasswordSetup"),
@@ -98,6 +88,16 @@ export default function SetResourcePasswordForm({
onSetPassword(); onSetPassword();
} }
}) })
.catch((e) => {
toast({
variant: "destructive",
title: t('resourceErrorPasswordSetup'),
description: formatAxiosError(
e,
t('resourceErrorPasswordSetupDescription')
)
});
})
.finally(() => setLoading(false)); .finally(() => setLoading(false));
} }

View File

@@ -84,16 +84,6 @@ export default function SetResourcePincodeForm({
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/pincode`, { api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/pincode`, {
pincode: data.pincode pincode: data.pincode
}) })
.catch((e) => {
toast({
variant: "destructive",
title: t("resourceErrorPincodeSetup"),
description: formatAxiosError(
e,
t("resourceErrorPincodeSetupDescription")
)
});
})
.then(() => { .then(() => {
toast({ toast({
title: t("resourcePincodeSetup"), title: t("resourcePincodeSetup"),
@@ -104,6 +94,16 @@ export default function SetResourcePincodeForm({
onSetPincode(); onSetPincode();
} }
}) })
.catch((e) => {
toast({
variant: "destructive",
title: t('resourceErrorPincodeSetup'),
description: formatAxiosError(
e,
t('resourceErrorPincodeSetupDescription')
)
});
})
.finally(() => setLoading(false)); .finally(() => setLoading(false));
} }

View File

@@ -1,11 +1,16 @@
import React from "react"; import React from "react";
import { Switch } from "./ui/switch"; import {Switch} from "./ui/switch";
import { Label } from "./ui/label"; 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 { interface SwitchComponentProps {
id: string; id: string;
label?: string; label?: string;
description?: string; description?: string;
info?: string;
checked?: boolean; checked?: boolean;
defaultChecked?: boolean; defaultChecked?: boolean;
disabled?: boolean; disabled?: boolean;
@@ -13,14 +18,26 @@ interface SwitchComponentProps {
} }
export function SwitchInput({ export function SwitchInput({
id, id,
label, label,
description, description,
disabled, info,
checked, disabled,
defaultChecked = false, checked,
onCheckedChange defaultChecked = false,
}: SwitchComponentProps) { onCheckedChange
}: SwitchComponentProps) {
const defaultTrigger = (
<Button
variant="ghost"
size="icon"
className="h-6 w-6 rounded-full p-0"
>
<Info className="h-4 w-4"/>
<span className="sr-only">Show info</span>
</Button>
);
return ( return (
<div> <div>
<div className="flex items-center space-x-2 mb-2"> <div className="flex items-center space-x-2 mb-2">
@@ -32,6 +49,18 @@ export function SwitchInput({
disabled={disabled} disabled={disabled}
/> />
{label && <Label htmlFor={id}>{label}</Label>} {label && <Label htmlFor={id}>{label}</Label>}
{info && <Popover>
<PopoverTrigger asChild>
{defaultTrigger}
</PopoverTrigger>
<PopoverContent className="w-80">
{info && (
<p className="text-sm text-muted-foreground">
{info}
</p>
)}
</PopoverContent>
</Popover>}
</div> </div>
{description && ( {description && (
<span className="text-muted-foreground text-sm"> <span className="text-muted-foreground text-sm">