Compare commits

...

18 Commits

Author SHA1 Message Date
Owen
7e9f18bf24 Update migration to allow all ports 2025-12-22 21:57:14 -05:00
Owen
ab3be26790 Working on remote nodes 2025-12-22 21:53:57 -05:00
Owen
5c67a1cb12 Format 2025-12-22 16:28:41 -05:00
Owen
e28ab19ed4 Add header 2025-12-22 16:28:19 -05:00
Owen
59f8334cfd Fix ee export of MaintenanceSchema 2025-12-22 16:27:54 -05:00
Milo Schwartz
718bec4bbc Merge pull request #2151 from fosrl/dev
1.14.0 ready
2025-12-22 13:16:30 -08:00
miloschwartz
2d731cb24b Merge branch 'main' into dev 2025-12-22 16:15:28 -05:00
miloschwartz
1905936950 parse request ip in exchange session 2025-12-22 15:48:24 -05:00
miloschwartz
c362bc673c add min version to product updates 2025-12-22 15:28:44 -05:00
miloschwartz
4da0a752ef make auto redirect to idp a select input 2025-12-22 15:03:57 -05:00
Owen
221ee6a1c2 Remove warning for limit 2025-12-22 14:07:49 -05:00
Owen
2e60ecec87 Add maintence options to blueprints 2025-12-22 14:00:50 -05:00
miloschwartz
71386d3b05 fix request ip port strip issue with badger >=1.3.0 2025-12-22 12:35:40 -05:00
Jacky Fong
89a7e2e4dc handle olm as well 2025-12-22 10:25:30 -05:00
Jacky Fong
27440700a5 fix: Don't treat newt release-candidate as a "update" in the site list 2025-12-22 10:25:30 -05:00
Owen
b5019cef12 Ignore the -arm64 and -amd64 tags 2025-12-21 21:21:24 -05:00
Owen
7e48cbe1aa Set file location 2025-12-21 21:16:57 -05:00
Owen
4b2c570e73 Fix bad rc check 2025-12-21 21:15:22 -05:00
70 changed files with 984 additions and 674 deletions

View File

@@ -99,7 +99,7 @@ jobs:
id: check-rc id: check-rc
run: | run: |
TAG=${{ env.TAG }} TAG=${{ env.TAG }}
if [[ "$TAG" == *".rc."* ]]; then if [[ "$TAG" == *"-rc."* ]]; then
echo "IS_RC=true" >> $GITHUB_ENV echo "IS_RC=true" >> $GITHUB_ENV
else else
echo "IS_RC=false" >> $GITHUB_ENV echo "IS_RC=false" >> $GITHUB_ENV
@@ -171,7 +171,7 @@ jobs:
id: check-rc id: check-rc
run: | run: |
TAG=${{ env.TAG }} TAG=${{ env.TAG }}
if [[ "$TAG" == *".rc."* ]]; then if [[ "$TAG" == *"-rc."* ]]; then
echo "IS_RC=true" >> $GITHUB_ENV echo "IS_RC=true" >> $GITHUB_ENV
else else
echo "IS_RC=false" >> $GITHUB_ENV echo "IS_RC=false" >> $GITHUB_ENV
@@ -219,7 +219,7 @@ jobs:
id: check-rc id: check-rc
run: | run: |
TAG=${{ env.TAG }} TAG=${{ env.TAG }}
if [[ "$TAG" == *".rc."* ]]; then if [[ "$TAG" == *"-rc."* ]]; then
echo "IS_RC=true" >> $GITHUB_ENV echo "IS_RC=true" >> $GITHUB_ENV
else else
echo "IS_RC=false" >> $GITHUB_ENV echo "IS_RC=false" >> $GITHUB_ENV
@@ -322,13 +322,18 @@ jobs:
shell: bash shell: bash
- name: Login to GHCR - name: Login to GHCR
env:
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
run: | run: |
mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")"
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}" skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
shell: bash shell: bash
- name: Copy tag from Docker Hub to GHCR - name: Copy tag from Docker Hub to GHCR
# Mirror the already-built image (all architectures) to GHCR so we can sign it # Mirror the already-built image (all architectures) to GHCR so we can sign it
# Wait a bit for both architectures to be available in Docker Hub manifest # Wait a bit for both architectures to be available in Docker Hub manifest
env:
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
run: | run: |
set -euo pipefail set -euo pipefail
TAG=${{ env.TAG }} TAG=${{ env.TAG }}

View File

@@ -45,7 +45,7 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
skopeo list-tags --retry-times 3 docker://"${SOURCE_IMAGE}" \ skopeo list-tags --retry-times 3 docker://"${SOURCE_IMAGE}" \
| jq -r '.Tags[]' | sort -u > src-tags.txt | jq -r '.Tags[]' | grep -v -e '-arm64' -e '-amd64' | sort -u > src-tags.txt
echo "Found source tags: $(wc -l < src-tags.txt)" echo "Found source tags: $(wc -l < src-tags.txt)"
head -n 20 src-tags.txt || true head -n 20 src-tags.txt || true

View File

@@ -2349,6 +2349,7 @@
"enterConfirmation": "Enter confirmation", "enterConfirmation": "Enter confirmation",
"blueprintViewDetails": "Details", "blueprintViewDetails": "Details",
"defaultIdentityProvider": "Default Identity Provider", "defaultIdentityProvider": "Default Identity Provider",
"defaultIdentityProviderDescription": "When a default identity provider is selected, the user will be automatically redirected to the provider for authentication.",
"editInternalResourceDialogNetworkSettings": "Network Settings", "editInternalResourceDialogNetworkSettings": "Network Settings",
"editInternalResourceDialogAccessPolicy": "Access Policy", "editInternalResourceDialogAccessPolicy": "Access Policy",
"editInternalResourceDialogAddRoles": "Add Roles", "editInternalResourceDialogAddRoles": "Add Roles",

View File

@@ -134,13 +134,15 @@ export const resources = pgTable("resources", {
proxyProtocol: boolean("proxyProtocol").notNull().default(false), proxyProtocol: boolean("proxyProtocol").notNull().default(false),
proxyProtocolVersion: integer("proxyProtocolVersion").default(1), proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
maintenanceModeEnabled: boolean("maintenanceModeEnabled").notNull().default(false), maintenanceModeEnabled: boolean("maintenanceModeEnabled")
.notNull()
.default(false),
maintenanceModeType: text("maintenanceModeType", { maintenanceModeType: text("maintenanceModeType", {
enum: ["forced", "automatic"] enum: ["forced", "automatic"]
}).default("forced"), // "forced" = always show, "automatic" = only when down }).default("forced"), // "forced" = always show, "automatic" = only when down
maintenanceTitle: text("maintenanceTitle"), maintenanceTitle: text("maintenanceTitle"),
maintenanceMessage: text("maintenanceMessage"), maintenanceMessage: text("maintenanceMessage"),
maintenanceEstimatedTime: text("maintenanceEstimatedTime"), maintenanceEstimatedTime: text("maintenanceEstimatedTime")
}); });
export const targets = pgTable("targets", { export const targets = pgTable("targets", {
@@ -223,8 +225,8 @@ export const siteResources = pgTable("siteResources", {
enabled: boolean("enabled").notNull().default(true), enabled: boolean("enabled").notNull().default(true),
alias: varchar("alias"), alias: varchar("alias"),
aliasAddress: varchar("aliasAddress"), aliasAddress: varchar("aliasAddress"),
tcpPortRangeString: varchar("tcpPortRangeString"), tcpPortRangeString: varchar("tcpPortRangeString").notNull().default("*"),
udpPortRangeString: varchar("udpPortRangeString"), udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
disableIcmp: boolean("disableIcmp").notNull().default(false) disableIcmp: boolean("disableIcmp").notNull().default(false)
}); });
@@ -464,13 +466,22 @@ export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
headerAuthHash: varchar("headerAuthHash").notNull() headerAuthHash: varchar("headerAuthHash").notNull()
}); });
export const resourceHeaderAuthExtendedCompatibility = pgTable("resourceHeaderAuthExtendedCompatibility", { export const resourceHeaderAuthExtendedCompatibility = pgTable(
headerAuthExtendedCompatibilityId: serial("headerAuthExtendedCompatibilityId").primaryKey(), "resourceHeaderAuthExtendedCompatibility",
resourceId: integer("resourceId") {
.notNull() headerAuthExtendedCompatibilityId: serial(
.references(() => resources.resourceId, { onDelete: "cascade" }), "headerAuthExtendedCompatibilityId"
extendedCompatibilityIsActivated: boolean("extendedCompatibilityIsActivated").notNull().default(true), ).primaryKey(),
}); resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
extendedCompatibilityIsActivated: boolean(
"extendedCompatibilityIsActivated"
)
.notNull()
.default(true)
}
);
export const resourceAccessToken = pgTable("resourceAccessToken", { export const resourceAccessToken = pgTable("resourceAccessToken", {
accessTokenId: varchar("accessTokenId").primaryKey(), accessTokenId: varchar("accessTokenId").primaryKey(),
@@ -872,7 +883,9 @@ 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 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,6 +1,4 @@
import { import { db, loginPage, LoginPage, loginPageOrg, Org, orgs } from "@server/db";
db, loginPage, LoginPage, loginPageOrg, Org, orgs,
} from "@server/db";
import { import {
Resource, Resource,
ResourcePassword, ResourcePassword,
@@ -27,7 +25,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 headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
org: Org; org: Org;
}; };
@@ -59,12 +57,12 @@ export async function getResourceByDomain(
) )
.leftJoin( .leftJoin(
resourceHeaderAuthExtendedCompatibility, resourceHeaderAuthExtendedCompatibility,
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId) eq(
) resourceHeaderAuthExtendedCompatibility.resourceId,
.innerJoin( resources.resourceId
orgs, )
eq(orgs.orgId, resources.orgId)
) )
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
.where(eq(resources.fullDomain, domain)) .where(eq(resources.fullDomain, domain))
.limit(1); .limit(1);
@@ -77,7 +75,8 @@ 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, 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,35 +122,39 @@ 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"
}), }),
headers: text("headers"), // comma-separated list of headers to add to the request 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), proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
maintenanceModeEnabled: integer("maintenanceModeEnabled", { mode: "boolean" }) maintenanceModeEnabled: integer("maintenanceModeEnabled", {
mode: "boolean"
})
.notNull() .notNull()
.default(false), .default(false),
maintenanceModeType: text("maintenanceModeType", { maintenanceModeType: text("maintenanceModeType", {
@@ -158,12 +162,11 @@ export const resources = sqliteTable("resources", {
}).default("forced"), // "forced" = always show, "automatic" = only when down }).default("forced"), // "forced" = always show, "automatic" = only when down
maintenanceTitle: text("maintenanceTitle"), maintenanceTitle: text("maintenanceTitle"),
maintenanceMessage: text("maintenanceMessage"), maintenanceMessage: text("maintenanceMessage"),
maintenanceEstimatedTime: text("maintenanceEstimatedTime"), maintenanceEstimatedTime: text("maintenanceEstimatedTime")
}); });
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"
@@ -178,7 +181,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
@@ -192,8 +195,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"),
@@ -215,7 +218,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
@@ -223,7 +226,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")
@@ -236,10 +239,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"
@@ -250,9 +253,9 @@ export const siteResources = sqliteTable("siteResources", {
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
alias: text("alias"), alias: text("alias"),
aliasAddress: text("aliasAddress"), aliasAddress: text("aliasAddress"),
tcpPortRangeString: text("tcpPortRangeString"), tcpPortRangeString: text("tcpPortRangeString").notNull().default("*"),
udpPortRangeString: text("udpPortRangeString"), udpPortRangeString: text("udpPortRangeString").notNull().default("*"),
disableIcmp: integer("disableIcmp", { mode: "boolean" }) disableIcmp: integer("disableIcmp", { mode: "boolean" }).notNull().default(false)
}); });
export const clientSiteResources = sqliteTable("clientSiteResources", { export const clientSiteResources = sqliteTable("clientSiteResources", {
@@ -292,20 +295,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")
@@ -339,7 +342,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")
}); });
@@ -378,7 +381,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")
}); });
@@ -424,10 +427,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()
}); });
@@ -435,7 +438,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" })
@@ -447,7 +450,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()
}); });
@@ -455,14 +458,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"
@@ -471,28 +474,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()
}); });
@@ -504,13 +507,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")
}); });
@@ -518,92 +521,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", {
@@ -612,7 +615,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()
}); });
@@ -623,7 +626,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()
}); });
@@ -633,28 +636,38 @@ 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", { export const resourceHeaderAuthExtendedCompatibility = sqliteTable(
headerAuthExtendedCompatibilityId: integer("headerAuthExtendedCompatibilityId").primaryKey({ "resourceHeaderAuthExtendedCompatibility",
autoIncrement: true {
}), headerAuthExtendedCompatibilityId: integer(
resourceId: integer("resourceId") "headerAuthExtendedCompatibilityId"
.notNull() ).primaryKey({
.references(() => resources.resourceId, {onDelete: "cascade"}), autoIncrement: true
extendedCompatibilityIsActivated: integer("extendedCompatibilityIsActivated", {mode: "boolean"}).notNull().default(true) }),
}); resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
extendedCompatibilityIsActivated: integer(
"extendedCompatibilityIsActivated",
{ mode: "boolean" }
)
.notNull()
.default(true)
}
);
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"),
@@ -667,13 +680,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"
}), }),
@@ -705,11 +718,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", {
@@ -718,7 +731,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()
@@ -730,11 +743,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
@@ -742,17 +755,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"),
@@ -772,7 +785,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(),
@@ -800,22 +813,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"
@@ -826,10 +839,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")
}); });
@@ -847,19 +860,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"),
@@ -876,7 +889,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),
@@ -932,7 +945,9 @@ 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 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

@@ -0,0 +1,3 @@
import { z } from "zod";
export const MaintenanceSchema = z.object({});

View File

@@ -1,4 +1,14 @@
import { db, newts, blueprints, Blueprint, Site, siteResources, roleSiteResources, userSiteResources, clientSiteResources } from "@server/db"; import {
db,
newts,
blueprints,
Blueprint,
Site,
siteResources,
roleSiteResources,
userSiteResources,
clientSiteResources
} from "@server/db";
import { Config, ConfigSchema } from "./types"; import { Config, ConfigSchema } from "./types";
import { ProxyResourcesResults, updateProxyResources } from "./proxyResources"; import { ProxyResourcesResults, updateProxyResources } from "./proxyResources";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
@@ -126,7 +136,7 @@ export async function applyBlueprint({
) )
.then((rows) => rows.map((row) => row.roleId)); .then((rows) => rows.map((row) => row.roleId));
const existingUserIds= await trx const existingUserIds = await trx
.select() .select()
.from(userSiteResources) .from(userSiteResources)
.where( .where(
@@ -134,7 +144,8 @@ export async function applyBlueprint({
userSiteResources.siteResourceId, userSiteResources.siteResourceId,
result.oldSiteResource.siteResourceId result.oldSiteResource.siteResourceId
) )
).then((rows) => rows.map((row) => row.userId)); )
.then((rows) => rows.map((row) => row.userId));
const existingClientIds = await trx const existingClientIds = await trx
.select() .select()
@@ -144,13 +155,19 @@ export async function applyBlueprint({
clientSiteResources.siteResourceId, clientSiteResources.siteResourceId,
result.oldSiteResource.siteResourceId result.oldSiteResource.siteResourceId
) )
).then((rows) => rows.map((row) => row.clientId)); )
.then((rows) => rows.map((row) => row.clientId));
// delete the existing site resource // delete the existing site resource
await trx await trx
.delete(siteResources) .delete(siteResources)
.where( .where(
and(eq(siteResources.siteResourceId, result.oldSiteResource.siteResourceId)) and(
eq(
siteResources.siteResourceId,
result.oldSiteResource.siteResourceId
)
)
); );
await rebuildClientAssociationsFromSiteResource( await rebuildClientAssociationsFromSiteResource(
@@ -161,7 +178,7 @@ export async function applyBlueprint({
const [insertedSiteResource] = await trx const [insertedSiteResource] = await trx
.insert(siteResources) .insert(siteResources)
.values({ .values({
...result.newSiteResource, ...result.newSiteResource
}) })
.returning(); .returning();
@@ -172,18 +189,20 @@ export async function applyBlueprint({
if (existingRoleIds.length > 0) { if (existingRoleIds.length > 0) {
await trx.insert(roleSiteResources).values( await trx.insert(roleSiteResources).values(
existingRoleIds.map((roleId) => ({ existingRoleIds.map((roleId) => ({
roleId, roleId,
siteResourceId: insertedSiteResource!.siteResourceId siteResourceId:
insertedSiteResource!.siteResourceId
})) }))
); );
} }
if (existingUserIds.length > 0) { if (existingUserIds.length > 0) {
await trx.insert(userSiteResources).values( await trx.insert(userSiteResources).values(
existingUserIds.map((userId) => ({ existingUserIds.map((userId) => ({
userId, userId,
siteResourceId: insertedSiteResource!.siteResourceId siteResourceId:
insertedSiteResource!.siteResourceId
})) }))
); );
} }
@@ -192,7 +211,8 @@ export async function applyBlueprint({
await trx.insert(clientSiteResources).values( await trx.insert(clientSiteResources).values(
existingClientIds.map((clientId) => ({ existingClientIds.map((clientId) => ({
clientId, clientId,
siteResourceId: insertedSiteResource!.siteResourceId siteResourceId:
insertedSiteResource!.siteResourceId
})) }))
); );
} }
@@ -201,7 +221,6 @@ export async function applyBlueprint({
insertedSiteResource, insertedSiteResource,
trx trx
); );
} else { } else {
const [newSite] = await trx const [newSite] = await trx
.select() .select()

View File

@@ -2,7 +2,8 @@ import {
domains, domains,
orgDomains, orgDomains,
Resource, Resource,
resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility, resourceHeaderAuth,
resourceHeaderAuthExtendedCompatibility,
resourcePincode, resourcePincode,
resourceRules, resourceRules,
resourceWhitelist, resourceWhitelist,
@@ -16,8 +17,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 +26,13 @@ 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 { isLicensedOrSubscribed } from "../isLicencedOrSubscribed";
import { build } from "@server/build";
export type ProxyResourcesResults = { export type ProxyResourcesResults = {
proxyResource: Resource; proxyResource: Resource;
@@ -63,7 +65,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 +77,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 +95,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
); );
@@ -209,6 +211,16 @@ export async function updateProxyResources(
resource = existingResource; resource = existingResource;
} else { } else {
// Update existing resource // Update existing resource
const isLicensed = await isLicensedOrSubscribed(orgId);
if (build == "enterprise" && !isLicensed) {
logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed
resourceData.maintenance = undefined;
}
[resource] = await trx [resource] = await trx
.update(resources) .update(resources)
.set({ .set({
@@ -228,12 +240,19 @@ 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,
applyRules: applyRules:
resourceData.rules && resourceData.rules.length > 0 resourceData.rules && resourceData.rules.length > 0,
maintenanceModeEnabled:
resourceData.maintenance?.enabled,
maintenanceModeType: resourceData.maintenance?.type,
maintenanceTitle: resourceData.maintenance?.title,
maintenanceMessage: resourceData.maintenance?.message,
maintenanceEstimatedTime:
resourceData.maintenance?.["estimated-time"]
}) })
.where( .where(
eq(resources.resourceId, existingResource.resourceId) eq(resources.resourceId, existingResource.resourceId)
@@ -303,8 +322,13 @@ export async function updateProxyResources(
const headerAuthPassword = const headerAuthPassword =
resourceData.auth?.["basic-auth"]?.password; resourceData.auth?.["basic-auth"]?.password;
const headerAuthExtendedCompatibility = const headerAuthExtendedCompatibility =
resourceData.auth?.["basic-auth"]?.extendedCompatibility; resourceData.auth?.["basic-auth"]
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) { ?.extendedCompatibility;
if (
headerAuthUser &&
headerAuthPassword &&
headerAuthExtendedCompatibility !== null
) {
const headerAuthHash = await hashPassword( const headerAuthHash = await hashPassword(
Buffer.from( Buffer.from(
`${headerAuthUser}:${headerAuthPassword}` `${headerAuthUser}:${headerAuthPassword}`
@@ -315,10 +339,13 @@ export async function updateProxyResources(
resourceId: existingResource.resourceId, resourceId: existingResource.resourceId,
headerAuthHash headerAuthHash
}), }),
trx.insert(resourceHeaderAuthExtendedCompatibility).values({ trx
resourceId: existingResource.resourceId, .insert(resourceHeaderAuthExtendedCompatibility)
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility .values({
}) resourceId: existingResource.resourceId,
extendedCompatibilityIsActivated:
headerAuthExtendedCompatibility
})
]); ]);
} }
} }
@@ -380,7 +407,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(
@@ -392,7 +419,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(
@@ -437,7 +464,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
); );
@@ -622,6 +649,15 @@ export async function updateProxyResources(
); );
} }
const isLicensed = await isLicensedOrSubscribed(orgId);
if (build == "enterprise" && !isLicensed) {
logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed
resourceData.maintenance = undefined;
}
// Create new resource // Create new resource
const [newResource] = await trx const [newResource] = await trx
.insert(resources) .insert(resources)
@@ -643,7 +679,13 @@ export async function updateProxyResources(
ssl: resourceSsl, ssl: resourceSsl,
headers: headers || null, headers: headers || null,
applyRules: applyRules:
resourceData.rules && resourceData.rules.length > 0 resourceData.rules && resourceData.rules.length > 0,
maintenanceModeEnabled: resourceData.maintenance?.enabled,
maintenanceModeType: resourceData.maintenance?.type,
maintenanceTitle: resourceData.maintenance?.title,
maintenanceMessage: resourceData.maintenance?.message,
maintenanceEstimatedTime:
resourceData.maintenance?.["estimated-time"]
}) })
.returning(); .returning();
@@ -674,9 +716,14 @@ 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; const headerAuthExtendedCompatibility =
resourceData.auth?.["basic-auth"]?.extendedCompatibility;
if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) { if (
headerAuthUser &&
headerAuthPassword &&
headerAuthExtendedCompatibility !== null
) {
const headerAuthHash = await hashPassword( const headerAuthHash = await hashPassword(
Buffer.from( Buffer.from(
`${headerAuthUser}:${headerAuthPassword}` `${headerAuthUser}:${headerAuthPassword}`
@@ -688,10 +735,13 @@ export async function updateProxyResources(
resourceId: newResource.resourceId, resourceId: newResource.resourceId,
headerAuthHash headerAuthHash
}), }),
trx.insert(resourceHeaderAuthExtendedCompatibility).values({ trx
resourceId: newResource.resourceId, .insert(resourceHeaderAuthExtendedCompatibility)
extendedCompatibilityIsActivated: headerAuthExtendedCompatibility .values({
}), resourceId: newResource.resourceId,
extendedCompatibilityIsActivated:
headerAuthExtendedCompatibility
})
]); ]);
} }
} }
@@ -1043,7 +1093,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

@@ -1,5 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { portRangeStringSchema } from "@server/lib/ip"; import { portRangeStringSchema } from "@server/lib/ip";
import { MaintenanceSchema } from "#dynamic/lib/blueprints/MaintenanceSchema";
export const SiteSchema = z.object({ export const SiteSchema = z.object({
name: z.string().min(1).max(100), name: z.string().min(1).max(100),
@@ -53,11 +54,13 @@ 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.object({ "basic-auth": z
user: z.string().min(1), .object({
password: z.string().min(1), user: z.string().min(1),
extendedCompatibility: z.boolean().default(true) password: z.string().min(1),
}).optional(), extendedCompatibility: z.boolean().default(true)
})
.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())
@@ -156,7 +159,8 @@ export const ResourceSchema = z
"host-header": z.string().optional(), "host-header": z.string().optional(),
"tls-server-name": z.string().optional(), "tls-server-name": z.string().optional(),
headers: z.array(HeaderSchema).optional(), headers: z.array(HeaderSchema).optional(),
rules: z.array(RuleSchema).optional() rules: z.array(RuleSchema).optional(),
maintenance: MaintenanceSchema.optional()
}) })
.refine( .refine(
(resource) => { (resource) => {

View File

@@ -318,10 +318,7 @@ export function doCidrsOverlap(cidr1: string, cidr2: string): boolean {
const range2 = cidrToRange(cidr2); const range2 = cidrToRange(cidr2);
// Overlap if the ranges intersect // Overlap if the ranges intersect
return ( return range1.start <= range2.end && range2.start <= range1.end;
range1.start <= range2.end &&
range2.start <= range1.end
);
} }
export async function getNextAvailableClientSubnet( export async function getNextAvailableClientSubnet(

View File

@@ -216,7 +216,10 @@ export const configSchema = z
.default(["newt", "wireguard", "local"]), .default(["newt", "wireguard", "local"]),
allow_raw_resources: z.boolean().optional().default(true), allow_raw_resources: z.boolean().optional().default(true),
file_mode: z.boolean().optional().default(false), file_mode: z.boolean().optional().default(false),
pp_transport_prefix: z.string().optional().default("pp-transport-v") pp_transport_prefix: z
.string()
.optional()
.default("pp-transport-v")
}) })
.optional() .optional()
.prefault({}), .prefault({}),

View File

@@ -294,12 +294,12 @@ export async function getTraefikConfig(
certResolver: resolverName, certResolver: resolverName,
...(preferWildcard ...(preferWildcard
? { ? {
domains: [ domains: [
{ {
main: wildCard main: wildCard
} }
] ]
} }
: {}) : {})
}; };
@@ -475,9 +475,9 @@ export async function getTraefikConfig(
// RECEIVE BANDWIDTH ENDPOINT. // RECEIVE BANDWIDTH ENDPOINT.
// TODO: HOW TO HANDLE ^^^^^^ BETTER // TODO: HOW TO HANDLE ^^^^^^ BETTER
const anySitesOnline = ( const anySitesOnline = targets.some(
targets (target) => target.site.online
).some((target) => target.site.online); );
return ( return (
targets targets
@@ -544,14 +544,14 @@ export async function getTraefikConfig(
})(), })(),
...(resource.stickySession ...(resource.stickySession
? { ? {
sticky: { sticky: {
cookie: { cookie: {
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
secure: resource.ssl, secure: resource.ssl,
httpOnly: true httpOnly: true
} }
} }
} }
: {}) : {})
} }
}; };
@@ -603,9 +603,9 @@ export async function getTraefikConfig(
loadBalancer: { loadBalancer: {
servers: (() => { servers: (() => {
// Check if any sites are online // Check if any sites are online
const anySitesOnline = ( const anySitesOnline = targets.some(
targets (target) => target.site.online
).some((target) => target.site.online); );
return targets return targets
.filter((target) => { .filter((target) => {
@@ -654,18 +654,18 @@ export async function getTraefikConfig(
})(), })(),
...(resource.proxyProtocol && protocol == "tcp" ...(resource.proxyProtocol && protocol == "tcp"
? { ? {
serversTransport: `${ppPrefix}${resource.proxyProtocolVersion || 1}@file` // TODO: does @file here cause issues? serversTransport: `${ppPrefix}${resource.proxyProtocolVersion || 1}@file` // TODO: does @file here cause issues?
} }
: {}), : {}),
...(resource.stickySession ...(resource.stickySession
? { ? {
sticky: { sticky: {
ipStrategy: { ipStrategy: {
depth: 0, depth: 0,
sourcePort: true sourcePort: true
} }
} }
} }
: {}) : {})
} }
}; };

View File

@@ -0,0 +1,22 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { z } from "zod";
export const MaintenanceSchema = z.object({
enabled: z.boolean().optional(),
type: z.enum(["forced", "automatic"]).optional(),
title: z.string().max(255).nullable().optional(),
message: z.string().max(2000).nullable().optional(),
"estimated-time": z.string().max(100).nullable().optional()
});

View File

@@ -23,10 +23,10 @@ import {
} from "@server/lib/checkOrgAccessPolicy"; } from "@server/lib/checkOrgAccessPolicy";
import { UserType } from "@server/types/UserTypes"; import { UserType } from "@server/types/UserTypes";
export async function enforceResourceSessionLength( export function enforceResourceSessionLength(
resourceSession: ResourceSession, resourceSession: ResourceSession,
org: Org org: Org
): Promise<{ valid: boolean; error?: string }> { ): { valid: boolean; error?: string } {
if (org.maxSessionLengthHours) { if (org.maxSessionLengthHours) {
const sessionIssuedAt = resourceSession.issuedAt; // may be null const sessionIssuedAt = resourceSession.issuedAt; // may be null
const maxSessionLengthHours = org.maxSessionLengthHours; const maxSessionLengthHours = org.maxSessionLengthHours;

View File

@@ -71,9 +71,9 @@ export async function getTraefikConfig(
siteTypes: string[], siteTypes: string[],
filterOutNamespaceDomains = false, filterOutNamespaceDomains = false,
generateLoginPageRouters = false, generateLoginPageRouters = false,
allowRawResources = true allowRawResources = true,
allowMaintenancePage = true
): Promise<any> { ): Promise<any> {
// Get resources with their targets and sites in a single optimized query // Get resources with their targets and sites in a single optimized query
// Start from sites on this exit node, then join to targets and resources // Start from sites on this exit node, then join to targets and resources
const resourcesWithTargetsAndSites = await db const resourcesWithTargetsAndSites = await db
@@ -435,17 +435,15 @@ export async function getTraefikConfig(
} }
} }
const availableServers = targets.filter( const availableServers = targets.filter((target) => {
(target) => { if (!target.enabled) return false;
if (!target.enabled) return false;
if (!target.site.online) return false; if (!target.site.online) return false;
if (target.health == "unhealthy") return false; if (target.health == "unhealthy") return false;
return true; return true;
} });
);
const hasHealthyServers = availableServers.length > 0; const hasHealthyServers = availableServers.length > 0;
@@ -794,9 +792,9 @@ export async function getTraefikConfig(
loadBalancer: { loadBalancer: {
servers: (() => { servers: (() => {
// Check if any sites are online // Check if any sites are online
const anySitesOnline = ( const anySitesOnline = targets.some(
targets (target) => target.site.online
).some((target) => target.site.online); );
return targets return targets
.filter((target) => { .filter((target) => {

View File

@@ -40,6 +40,7 @@ import {
ResourceHeaderAuthExtendedCompatibility, ResourceHeaderAuthExtendedCompatibility,
orgs, orgs,
requestAuditLog, requestAuditLog,
Org
} from "@server/db"; } from "@server/db";
import { import {
resources, resources,
@@ -79,6 +80,7 @@ import { maxmindLookup } from "@server/db/maxmind";
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
import semver from "semver"; import semver from "semver";
import { maxmindAsnLookup } from "@server/db/maxmindAsn"; import { maxmindAsnLookup } from "@server/db/maxmindAsn";
import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy";
// Zod schemas for request validation // Zod schemas for request validation
const getResourceByDomainParamsSchema = z.strictObject({ const getResourceByDomainParamsSchema = z.strictObject({
@@ -94,6 +96,12 @@ const getUserOrgRoleParamsSchema = z.strictObject({
orgId: z.string().min(1, "Organization ID is required") orgId: z.string().min(1, "Organization ID is required")
}); });
const getUserOrgSessionVerifySchema = z.strictObject({
userId: z.string().min(1, "User ID is required"),
orgId: z.string().min(1, "Organization ID is required"),
sessionId: z.string().min(1, "Session ID is required")
});
const getRoleResourceAccessParamsSchema = z.strictObject({ const getRoleResourceAccessParamsSchema = z.strictObject({
roleId: z roleId: z
.string() .string()
@@ -178,6 +186,7 @@ export type ResourceWithAuth = {
password: ResourcePassword | null; password: ResourcePassword | null;
headerAuth: ResourceHeaderAuth | null; headerAuth: ResourceHeaderAuth | null;
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
org: Org
}; };
export type UserSessionWithUser = { export type UserSessionWithUser = {
@@ -503,8 +512,12 @@ hybridRouter.get(
) )
.leftJoin( .leftJoin(
resourceHeaderAuthExtendedCompatibility, resourceHeaderAuthExtendedCompatibility,
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId) 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);
@@ -538,7 +551,9 @@ hybridRouter.get(
pincode: result.resourcePincode, pincode: result.resourcePincode,
password: result.resourcePassword, password: result.resourcePassword,
headerAuth: result.resourceHeaderAuth, headerAuth: result.resourceHeaderAuth,
headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility headerAuthExtendedCompatibility:
result.resourceHeaderAuthExtendedCompatibility,
org: result.orgs
}; };
return response<ResourceWithAuth>(res, { return response<ResourceWithAuth>(res, {
@@ -818,6 +833,69 @@ hybridRouter.get(
} }
); );
// Get user organization role
hybridRouter.get(
"/user/:userId/org/:orgId/session/:sessionId/verify",
async (req: Request, res: Response, next: NextFunction) => {
try {
const parsedParams = getUserOrgSessionVerifySchema.safeParse(
req.params
);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { userId, orgId, sessionId } = parsedParams.data;
const remoteExitNode = req.remoteExitNode;
if (!remoteExitNode || !remoteExitNode.exitNodeId) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Remote exit node not found"
)
);
}
if (await checkExitNodeOrg(remoteExitNode.exitNodeId, orgId)) {
return next(
createHttpError(
HttpCode.UNAUTHORIZED,
"User is not authorized to access this organization"
)
);
}
const accessPolicy = await checkOrgAccessPolicy({
orgId,
userId,
sessionId
});
return response(res, {
data: accessPolicy,
success: true,
error: false,
message: "User org access policy retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Failed to get user org role"
)
);
}
}
);
// Check if role has access to resource // Check if role has access to resource
hybridRouter.get( hybridRouter.get(
"/role/:roleId/resource/:resourceId/access", "/role/:roleId/resource/:resourceId/access",

View File

@@ -99,12 +99,13 @@ async function query(query: Q) {
.where(and(baseConditions, not(isNull(requestAuditLog.location)))) .where(and(baseConditions, not(isNull(requestAuditLog.location))))
.groupBy(requestAuditLog.location) .groupBy(requestAuditLog.location)
.orderBy(desc(totalQ)) .orderBy(desc(totalQ))
.limit(DISTINCT_LIMIT+1); .limit(DISTINCT_LIMIT + 1);
if (requestsPerCountry.length > DISTINCT_LIMIT) { if (requestsPerCountry.length > DISTINCT_LIMIT) {
// throw an error // throw an error
throw createHttpError( throw createHttpError(
HttpCode.BAD_REQUEST, HttpCode.BAD_REQUEST,
// todo: is this even possible?
`Too many distinct countries. Please narrow your query.` `Too many distinct countries. Please narrow your query.`
); );
} }

View File

@@ -189,22 +189,22 @@ async function queryUniqueFilterAttributes(
.selectDistinct({ actor: requestAuditLog.actor }) .selectDistinct({ actor: requestAuditLog.actor })
.from(requestAuditLog) .from(requestAuditLog)
.where(baseConditions) .where(baseConditions)
.limit(DISTINCT_LIMIT+1), .limit(DISTINCT_LIMIT + 1),
primaryDb primaryDb
.selectDistinct({ locations: requestAuditLog.location }) .selectDistinct({ locations: requestAuditLog.location })
.from(requestAuditLog) .from(requestAuditLog)
.where(baseConditions) .where(baseConditions)
.limit(DISTINCT_LIMIT+1), .limit(DISTINCT_LIMIT + 1),
primaryDb primaryDb
.selectDistinct({ hosts: requestAuditLog.host }) .selectDistinct({ hosts: requestAuditLog.host })
.from(requestAuditLog) .from(requestAuditLog)
.where(baseConditions) .where(baseConditions)
.limit(DISTINCT_LIMIT+1), .limit(DISTINCT_LIMIT + 1),
primaryDb primaryDb
.selectDistinct({ paths: requestAuditLog.path }) .selectDistinct({ paths: requestAuditLog.path })
.from(requestAuditLog) .from(requestAuditLog)
.where(baseConditions) .where(baseConditions)
.limit(DISTINCT_LIMIT+1), .limit(DISTINCT_LIMIT + 1),
primaryDb primaryDb
.selectDistinct({ .selectDistinct({
id: requestAuditLog.resourceId, id: requestAuditLog.resourceId,
@@ -216,18 +216,20 @@ async function queryUniqueFilterAttributes(
eq(requestAuditLog.resourceId, resources.resourceId) eq(requestAuditLog.resourceId, resources.resourceId)
) )
.where(baseConditions) .where(baseConditions)
.limit(DISTINCT_LIMIT+1) .limit(DISTINCT_LIMIT + 1)
]); ]);
if ( // TODO: for stuff like the paths this is too restrictive so lets just show some of the paths and the user needs to
uniqueActors.length > DISTINCT_LIMIT || // refine the time range to see what they need to see
uniqueLocations.length > DISTINCT_LIMIT || // if (
uniqueHosts.length > DISTINCT_LIMIT || // uniqueActors.length > DISTINCT_LIMIT ||
uniquePaths.length > DISTINCT_LIMIT || // uniqueLocations.length > DISTINCT_LIMIT ||
uniqueResources.length > DISTINCT_LIMIT // uniqueHosts.length > DISTINCT_LIMIT ||
) { // uniquePaths.length > DISTINCT_LIMIT ||
throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range."); // uniqueResources.length > DISTINCT_LIMIT
} // ) {
// throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range.");
// }
return { return {
actors: uniqueActors actors: uniqueActors
@@ -307,10 +309,12 @@ export async function queryRequestAuditLogs(
} catch (error) { } catch (error) {
logger.error(error); logger.error(error);
// if the message is "Too many distinct filter attributes to retrieve. Please refine your time range.", return a 400 and the message // if the message is "Too many distinct filter attributes to retrieve. Please refine your time range.", return a 400 and the message
if (error instanceof Error && error.message === "Too many distinct filter attributes to retrieve. Please refine your time range.") { if (
return next( error instanceof Error &&
createHttpError(HttpCode.BAD_REQUEST, error.message) error.message ===
); "Too many distinct filter attributes to retrieve. Please refine your time range."
) {
return next(createHttpError(HttpCode.BAD_REQUEST, error.message));
} }
return next( return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")

View File

@@ -62,7 +62,26 @@ export async function exchangeSession(
cleanHost = cleanHost.slice(0, -1 * matched.length); cleanHost = cleanHost.slice(0, -1 * matched.length);
} }
const clientIp = requestIp?.split(":")[0]; const clientIp = requestIp
? (() => {
if (requestIp.startsWith("[") && requestIp.includes("]")) {
const ipv6Match = requestIp.match(/\[(.*?)\]/);
if (ipv6Match) {
return ipv6Match[1];
}
}
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}/;
if (ipv4Pattern.test(requestIp)) {
const lastColonIndex = requestIp.lastIndexOf(":");
if (lastColonIndex !== -1) {
return requestIp.substring(0, lastColonIndex);
}
}
return requestIp;
})()
: undefined;
const [resource] = await db const [resource] = await db
.select() .select()

View File

@@ -13,7 +13,8 @@ import {
LoginPage, LoginPage,
Org, Org,
Resource, Resource,
ResourceHeaderAuth, ResourceHeaderAuthExtendedCompatibility, ResourceHeaderAuth,
ResourceHeaderAuthExtendedCompatibility,
ResourcePassword, ResourcePassword,
ResourcePincode, ResourcePincode,
ResourceRule, ResourceRule,
@@ -39,6 +40,8 @@ import {
} from "#dynamic/lib/checkOrgAccessPolicy"; } from "#dynamic/lib/checkOrgAccessPolicy";
import { logRequestAudit } from "./logRequestAudit"; import { logRequestAudit } from "./logRequestAudit";
import cache from "@server/lib/cache"; import cache from "@server/lib/cache";
import semver from "semver";
import { APP_VERSION } from "@server/lib/consts";
const verifyResourceSessionSchema = z.object({ const verifyResourceSessionSchema = z.object({
sessions: z.record(z.string(), z.string()).optional(), sessions: z.record(z.string(), z.string()).optional(),
@@ -50,7 +53,8 @@ const verifyResourceSessionSchema = z.object({
path: z.string(), path: z.string(),
method: z.string(), method: z.string(),
tls: z.boolean(), tls: z.boolean(),
requestIp: z.string().optional() requestIp: z.string().optional(),
badgerVersion: z.string().optional()
}); });
export type VerifyResourceSessionSchema = z.infer< export type VerifyResourceSessionSchema = z.infer<
@@ -69,6 +73,7 @@ export type VerifyUserResponse = {
headerAuthChallenged?: boolean; headerAuthChallenged?: boolean;
redirectUrl?: string; redirectUrl?: string;
userData?: BasicUserData; userData?: BasicUserData;
pangolinVersion?: string;
}; };
export async function verifyResourceSession( export async function verifyResourceSession(
@@ -97,7 +102,8 @@ export async function verifyResourceSession(
requestIp, requestIp,
path, path,
headers, headers,
query query,
badgerVersion
} = parsedBody.data; } = parsedBody.data;
// Extract HTTP Basic Auth credentials if present // Extract HTTP Basic Auth credentials if present
@@ -105,7 +111,15 @@ export async function verifyResourceSession(
const clientIp = requestIp const clientIp = requestIp
? (() => { ? (() => {
logger.debug("Request IP:", { requestIp }); const isNewerBadger =
badgerVersion &&
semver.valid(badgerVersion) &&
semver.gte(badgerVersion, "1.3.1");
if (isNewerBadger) {
return requestIp;
}
if (requestIp.startsWith("[") && requestIp.includes("]")) { if (requestIp.startsWith("[") && requestIp.includes("]")) {
// if brackets are found, extract the IPv6 address from between the brackets // if brackets are found, extract the IPv6 address from between the brackets
const ipv6Match = requestIp.match(/\[(.*?)\]/); const ipv6Match = requestIp.match(/\[(.*?)\]/);
@@ -114,12 +128,17 @@ export async function verifyResourceSession(
} }
} }
// ivp4 // Check if it looks like IPv4 (contains dots and matches IPv4 pattern)
// split at last colon // IPv4 format: x.x.x.x where x is 0-255
const lastColonIndex = requestIp.lastIndexOf(":"); const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}/;
if (lastColonIndex !== -1) { if (ipv4Pattern.test(requestIp)) {
return requestIp.substring(0, lastColonIndex); const lastColonIndex = requestIp.lastIndexOf(":");
if (lastColonIndex !== -1) {
return requestIp.substring(0, lastColonIndex);
}
} }
// Return as is
return requestIp; return requestIp;
})() })()
: undefined; : undefined;
@@ -130,9 +149,7 @@ export async function verifyResourceSession(
? await getCountryCodeFromIp(clientIp) ? await getCountryCodeFromIp(clientIp)
: undefined; : undefined;
const ipAsn = clientIp const ipAsn = clientIp ? await getAsnFromIp(clientIp) : undefined;
? await getAsnFromIp(clientIp)
: undefined;
let cleanHost = host; let cleanHost = host;
// if the host ends with :port, strip it // if the host ends with :port, strip it
@@ -178,7 +195,13 @@ export async function verifyResourceSession(
cache.set(resourceCacheKey, resourceData, 5); cache.set(resourceCacheKey, resourceData, 5);
} }
const { resource, pincode, password, headerAuth, headerAuthExtendedCompatibility } = resourceData; const {
resource,
pincode,
password,
headerAuth,
headerAuthExtendedCompatibility
} = resourceData;
if (!resource) { if (!resource) {
logger.debug(`Resource not found ${cleanHost}`); logger.debug(`Resource not found ${cleanHost}`);
@@ -474,8 +497,7 @@ 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 &&
@@ -713,7 +735,11 @@ export async function verifyResourceSession(
} }
// If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge // If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge
if (headerAuthExtendedCompatibility && headerAuthExtendedCompatibility.extendedCompatibilityIsActivated && !clientHeaderAuth){ if (
headerAuthExtendedCompatibility &&
headerAuthExtendedCompatibility.extendedCompatibilityIsActivated &&
!clientHeaderAuth
) {
return headerAuthChallenged(res, redirectPath, resource.orgId); return headerAuthChallenged(res, redirectPath, resource.orgId);
} }
@@ -825,7 +851,7 @@ async function notAllowed(
} }
const data = { const data = {
data: { valid: false, redirectUrl }, data: { valid: false, redirectUrl, pangolinVersion: APP_VERSION },
success: true, success: true,
error: false, error: false,
message: "Access denied", message: "Access denied",
@@ -839,8 +865,8 @@ function allowed(res: Response, userData?: BasicUserData) {
const data = { const data = {
data: data:
userData !== undefined && userData !== null userData !== undefined && userData !== null
? { valid: true, ...userData } ? { valid: true, ...userData, pangolinVersion: APP_VERSION }
: { valid: true }, : { valid: true, pangolinVersion: APP_VERSION },
success: true, success: true,
error: false, error: false,
message: "Access allowed", message: "Access allowed",
@@ -879,7 +905,12 @@ async function headerAuthChallenged(
} }
const data = { const data = {
data: { headerAuthChallenged: true, valid: false, redirectUrl }, data: {
headerAuthChallenged: true,
valid: false,
redirectUrl,
pangolinVersion: APP_VERSION
},
success: true, success: true,
error: false, error: false,
message: "Access denied", message: "Access denied",

View File

@@ -56,12 +56,12 @@ async function getLatestOlmVersion(): Promise<string | null> {
return null; return null;
} }
const tags = await response.json(); let tags = await response.json();
if (!Array.isArray(tags) || tags.length === 0) { if (!Array.isArray(tags) || tags.length === 0) {
logger.warn("No tags found for Olm repository"); logger.warn("No tags found for Olm repository");
return null; return null;
} }
tags = tags.filter((version) => !version.name.includes("rc"));
const latestVersion = tags[0].name; const latestVersion = tags[0].name;
olmVersionCache.set("latestOlmVersion", latestVersion); olmVersionCache.set("latestOlmVersion", latestVersion);

View File

@@ -52,7 +52,7 @@ export async function getConfig(
} }
// clean up the public key - keep only valid base64 characters (A-Z, a-z, 0-9, +, /, =) // clean up the public key - keep only valid base64 characters (A-Z, a-z, 0-9, +, /, =)
const cleanedPublicKey = publicKey.replace(/[^A-Za-z0-9+/=]/g, ''); const cleanedPublicKey = publicKey.replace(/[^A-Za-z0-9+/=]/g, "");
const exitNode = await createExitNode(cleanedPublicKey, reachableAt); const exitNode = await createExitNode(cleanedPublicKey, reachableAt);

View File

@@ -858,7 +858,6 @@ authenticated.put(
blueprints.applyJSONBlueprint blueprints.applyJSONBlueprint
); );
authenticated.get( authenticated.get(
"/org/:orgId/blueprint/:blueprintId", "/org/:orgId/blueprint/:blueprintId",
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
@@ -866,7 +865,6 @@ authenticated.get(
blueprints.getBlueprint blueprints.getBlueprint
); );
authenticated.get( authenticated.get(
"/org/:orgId/blueprints", "/org/:orgId/blueprints",
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,

View File

@@ -21,8 +21,7 @@ export async function pickOrgDefaults(
// const subnet = await getNextAvailableOrgSubnet(); // const subnet = await getNextAvailableOrgSubnet();
// Just hard code the subnet for now for everyone // Just hard code the subnet for now for everyone
const subnet = config.getRawConfig().orgs.subnet_group; const subnet = config.getRawConfig().orgs.subnet_group;
const utilitySubnet = const utilitySubnet = config.getRawConfig().orgs.utility_subnet_group;
config.getRawConfig().orgs.utility_subnet_group;
return response<PickOrgDefaultsResponse>(res, { return response<PickOrgDefaultsResponse>(res, {
data: { data: {

View File

@@ -1,19 +1,20 @@
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, resourceHeaderAuthExtendedCompatibility, 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()
@@ -52,68 +53,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
) )
) )
.leftJoin( .leftJoin(
resourceHeaderAuthExtendedCompatibility, resourceHeaderAuthExtendedCompatibility,
eq( eq(
resourceHeaderAuthExtendedCompatibility.resourceId, resourceHeaderAuthExtendedCompatibility.resourceId,
resources.resourceId resources.resourceId
) )
) )
.where(eq(resources.resourceId, Number(resourceGuid))) .where(eq(resources.resourceId, Number(resourceGuid)))
.limit(1) .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( .leftJoin(
resourceHeaderAuth, resourceHeaderAuth,
eq( eq(
resourceHeaderAuth.resourceId, resourceHeaderAuth.resourceId,
resources.resourceId resources.resourceId
) )
) )
.leftJoin( .leftJoin(
resourceHeaderAuthExtendedCompatibility, resourceHeaderAuthExtendedCompatibility,
eq( eq(
resourceHeaderAuthExtendedCompatibility.resourceId, resourceHeaderAuthExtendedCompatibility.resourceId,
resources.resourceId resources.resourceId
) )
) )
.where(eq(resources.resourceGuid, resourceGuid)) .where(eq(resources.resourceGuid, resourceGuid))
.limit(1); .limit(1);
const resource = result?.resources; const resource = result?.resources;
if (!resource) { if (!resource) {
@@ -125,7 +126,8 @@ 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 headerAuthExtendedCompatibility =
result?.resourceHeaderAuthExtendedCompatibility;
const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
@@ -138,7 +140,8 @@ 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, headerAuthExtendedCompatibility:
headerAuthExtendedCompatibility !== null,
sso: resource.sso, sso: resource.sso,
blockAccess: resource.blockAccess, blockAccess: resource.blockAccess,
url, url,

View File

@@ -1,6 +1,10 @@
import { Request, Response, NextFunction } from "express"; import { Request, Response, NextFunction } from "express";
import { z } from "zod"; import { z } from "zod";
import {db, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility} from "@server/db"; import {
db,
resourceHeaderAuth,
resourceHeaderAuthExtendedCompatibility
} from "@server/db";
import { import {
resources, resources,
userResources, userResources,
@@ -109,7 +113,8 @@ 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, headerAuthExtendedCompatibilityId:
resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId,
targetId: targets.targetId, targetId: targets.targetId,
targetIp: targets.ip, targetIp: targets.ip,
targetPort: targets.port, targetPort: targets.port,
@@ -133,7 +138,10 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
) )
.leftJoin( .leftJoin(
resourceHeaderAuthExtendedCompatibility, resourceHeaderAuthExtendedCompatibility,
eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId) eq(
resourceHeaderAuthExtendedCompatibility.resourceId,
resources.resourceId
)
) )
.leftJoin(targets, eq(targets.resourceId, resources.resourceId)) .leftJoin(targets, eq(targets.resourceId, resources.resourceId))
.leftJoin( .leftJoin(

View File

@@ -1,14 +1,18 @@
import {Request, Response, NextFunction} from "express"; import { Request, Response, NextFunction } from "express";
import {z} from "zod"; import { z } from "zod";
import {db, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility} from "@server/db"; import {
import {eq} from "drizzle-orm"; db,
resourceHeaderAuth,
resourceHeaderAuthExtendedCompatibility
} from "@server/db";
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())
@@ -67,29 +71,40 @@ export async function setResourceHeaderAuth(
); );
} }
const {resourceId} = parsedParams.data; const { resourceId } = parsedParams.data;
const {user, password, extendedCompatibility} = 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)); await trx
.delete(resourceHeaderAuthExtendedCompatibility)
.where(
eq(
resourceHeaderAuthExtendedCompatibility.resourceId,
resourceId
)
);
if (user && password && extendedCompatibility !== null) { if (user && password && extendedCompatibility !== null) {
const headerAuthHash = await hashPassword(Buffer.from(`${user}:${password}`).toString("base64")); const headerAuthHash = await hashPassword(
Buffer.from(`${user}:${password}`).toString("base64")
);
await Promise.all([ await Promise.all([
trx trx
.insert(resourceHeaderAuth) .insert(resourceHeaderAuth)
.values({resourceId, headerAuthHash}), .values({ resourceId, headerAuthHash }),
trx trx
.insert(resourceHeaderAuthExtendedCompatibility) .insert(resourceHeaderAuthExtendedCompatibility)
.values({resourceId, extendedCompatibilityIsActivated: extendedCompatibility}) .values({
resourceId,
extendedCompatibilityIsActivated:
extendedCompatibility
})
]); ]);
} }
}); });
return response(res, { return response(res, {

View File

@@ -7,4 +7,4 @@ export type GetMaintenanceInfoResponse = {
maintenanceTitle: string | null; maintenanceTitle: string | null;
maintenanceMessage: string | null; maintenanceMessage: string | null;
maintenanceEstimatedTime: string | null; maintenanceEstimatedTime: string | null;
} };

View File

@@ -343,7 +343,9 @@ async function updateHttpResource(
const isLicensed = await isLicensedOrSubscribed(resource.orgId); const isLicensed = await isLicensedOrSubscribed(resource.orgId);
if (build == "enterprise" && !isLicensed) { if (build == "enterprise" && !isLicensed) {
logger.warn("Server is not licensed! Clearing set maintenance screen values"); logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed // null the maintenance mode fields if not licensed
updateData.maintenanceModeEnabled = undefined; updateData.maintenanceModeEnabled = undefined;
updateData.maintenanceModeType = undefined; updateData.maintenanceModeType = undefined;

View File

@@ -39,12 +39,12 @@ async function getLatestNewtVersion(): Promise<string | null> {
return null; return null;
} }
const tags = await response.json(); let tags = await response.json();
if (!Array.isArray(tags) || tags.length === 0) { if (!Array.isArray(tags) || tags.length === 0) {
logger.warn("No tags found for Newt repository"); logger.warn("No tags found for Newt repository");
return null; return null;
} }
tags = tags.filter((version) => !version.name.includes("rc"));
const latestVersion = tags[0].name; const latestVersion = tags[0].name;
cache.set("latestNewtVersion", latestVersion); cache.set("latestNewtVersion", latestVersion);

View File

@@ -11,7 +11,11 @@ import {
userSiteResources userSiteResources
} from "@server/db"; } from "@server/db";
import { getUniqueSiteResourceName } from "@server/db/names"; import { getUniqueSiteResourceName } from "@server/db/names";
import { getNextAvailableAliasAddress, isIpInCidr, portRangeStringSchema } from "@server/lib/ip"; import {
getNextAvailableAliasAddress,
isIpInCidr,
portRangeStringSchema
} from "@server/lib/ip";
import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations"; import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations";
import response from "@server/lib/response"; import response from "@server/lib/response";
import logger from "@server/logger"; import logger from "@server/logger";
@@ -69,7 +73,10 @@ const createSiteResourceSchema = z
const domainRegex = const domainRegex =
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
const isValidDomain = domainRegex.test(data.destination); const isValidDomain = domainRegex.test(data.destination);
const isValidAlias = data.alias !== undefined && data.alias !== null && data.alias.trim() !== ""; const isValidAlias =
data.alias !== undefined &&
data.alias !== null &&
data.alias.trim() !== "";
return isValidDomain && isValidAlias; // require the alias to be set in the case of domain return isValidDomain && isValidAlias; // require the alias to be set in the case of domain
} }
@@ -182,7 +189,9 @@ export async function createSiteResource(
.limit(1); .limit(1);
if (!org) { if (!org) {
return next(createHttpError(HttpCode.NOT_FOUND, "Organization not found")); return next(
createHttpError(HttpCode.NOT_FOUND, "Organization not found")
);
} }
if (!org.subnet || !org.utilitySubnet) { if (!org.subnet || !org.utilitySubnet) {
@@ -195,10 +204,13 @@ export async function createSiteResource(
} }
// Only check if destination is an IP address // Only check if destination is an IP address
const isIp = z.union([z.ipv4(), z.ipv6()]).safeParse(destination).success; const isIp = z
.union([z.ipv4(), z.ipv6()])
.safeParse(destination).success;
if ( if (
isIp && isIp &&
(isIpInCidr(destination, org.subnet) || isIpInCidr(destination, org.utilitySubnet)) (isIpInCidr(destination, org.subnet) ||
isIpInCidr(destination, org.utilitySubnet))
) { ) {
return next( return next(
createHttpError( createHttpError(

View File

@@ -88,9 +88,7 @@ export async function deleteSiteResource(
); );
}); });
logger.info( logger.info(`Deleted site resource ${siteResourceId}`);
`Deleted site resource ${siteResourceId}`
);
return response(res, { return response(res, {
data: { message: "Site resource deleted successfully" }, data: { message: "Site resource deleted successfully" },

View File

@@ -204,7 +204,9 @@ export async function updateSiteResource(
.limit(1); .limit(1);
if (!org) { if (!org) {
return next(createHttpError(HttpCode.NOT_FOUND, "Organization not found")); return next(
createHttpError(HttpCode.NOT_FOUND, "Organization not found")
);
} }
if (!org.subnet || !org.utilitySubnet) { if (!org.subnet || !org.utilitySubnet) {
@@ -217,10 +219,13 @@ export async function updateSiteResource(
} }
// Only check if destination is an IP address // Only check if destination is an IP address
const isIp = z.union([z.ipv4(), z.ipv6()]).safeParse(destination).success; const isIp = z
.union([z.ipv4(), z.ipv6()])
.safeParse(destination).success;
if ( if (
isIp && isIp &&
(isIpInCidr(destination!, org.subnet) || isIpInCidr(destination!, org.utilitySubnet)) (isIpInCidr(destination!, org.subnet) ||
isIpInCidr(destination!, org.utilitySubnet))
) { ) {
return next( return next(
createHttpError( createHttpError(
@@ -295,7 +300,7 @@ export async function updateSiteResource(
const [insertedSiteResource] = await trx const [insertedSiteResource] = await trx
.insert(siteResources) .insert(siteResources)
.values({ .values({
...existingSiteResource, ...existingSiteResource
}) })
.returning(); .returning();
@@ -517,9 +522,14 @@ export async function handleMessagingForUpdatedSiteResource(
site: { siteId: number; orgId: string }, site: { siteId: number; orgId: string },
trx: Transaction trx: Transaction
) { ) {
logger.debug(
logger.debug("handleMessagingForUpdatedSiteResource: existingSiteResource is: ", existingSiteResource); "handleMessagingForUpdatedSiteResource: existingSiteResource is: ",
logger.debug("handleMessagingForUpdatedSiteResource: updatedSiteResource is: ", updatedSiteResource); existingSiteResource
);
logger.debug(
"handleMessagingForUpdatedSiteResource: updatedSiteResource is: ",
updatedSiteResource
);
const { mergedAllClients } = const { mergedAllClients } =
await rebuildClientAssociationsFromSiteResource( await rebuildClientAssociationsFromSiteResource(

View File

@@ -60,11 +60,11 @@ export default async function migration() {
); );
await db.execute( await db.execute(
sql`ALTER TABLE "siteResources" ADD COLUMN "tcpPortRangeString" varchar;` sql`ALTER TABLE "siteResources" ADD COLUMN "tcpPortRangeString" varchar NOT NULL DEFAULT '*';`
); );
await db.execute( await db.execute(
sql`ALTER TABLE "siteResources" ADD COLUMN "udpPortRangeString" varchar;` sql`ALTER TABLE "siteResources" ADD COLUMN "udpPortRangeString" varchar NOT NULL DEFAULT '*';`
); );
await db.execute( await db.execute(

View File

@@ -73,16 +73,18 @@ export default async function migration() {
).run(); ).run();
db.prepare( db.prepare(
`ALTER TABLE 'siteResources' ADD 'tcpPortRangeString' text;` `ALTER TABLE 'siteResources' ADD 'tcpPortRangeString' text DEFAULT '*' NOT NULL;`
).run(); ).run();
db.prepare( db.prepare(
`ALTER TABLE 'siteResources' ADD 'udpPortRangeString' text;` `ALTER TABLE 'siteResources' ADD 'udpPortRangeString' text DEFAULT '*' NOT NULL;`
).run(); ).run();
db.prepare( db.prepare(
`ALTER TABLE 'siteResources' ADD 'disableIcmp' integer;` `ALTER TABLE 'siteResources' ADD 'disableIcmp' integer NOT NULL DEFAULT false;`
).run(); ).run();
})(); })();
db.pragma("foreign_keys = ON"); db.pragma("foreign_keys = ON");

View File

@@ -71,7 +71,9 @@ export default async function GeneralSettingsPage({
<div className="space-y-6"> <div className="space-y-6">
<OrgInfoCard /> <OrgInfoCard />
<HorizontalTabs items={navItems}>{children}</HorizontalTabs> <HorizontalTabs items={navItems}>
{children}
</HorizontalTabs>
</div> </div>
</OrgUserProvider> </OrgUserProvider>
</OrgProvider> </OrgProvider>

View File

@@ -71,7 +71,7 @@ export default async function ClientResourcesPage(
niceId: siteResource.niceId, niceId: siteResource.niceId,
tcpPortRangeString: siteResource.tcpPortRangeString || null, tcpPortRangeString: siteResource.tcpPortRangeString || null,
udpPortRangeString: siteResource.udpPortRangeString || null, udpPortRangeString: siteResource.udpPortRangeString || null,
disableIcmp: siteResource.disableIcmp || false, disableIcmp: siteResource.disableIcmp || false
}; };
} }
); );

View File

@@ -16,7 +16,6 @@ import { SwitchInput } from "@app/components/SwitchInput";
import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Tag, TagInput } from "@app/components/tags/tag-input";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { CheckboxWithLabel } from "@app/components/ui/checkbox";
import { import {
Form, Form,
FormControl, FormControl,
@@ -184,9 +183,6 @@ export default function ResourceAuthenticationPage() {
const [ssoEnabled, setSsoEnabled] = useState(resource.sso); const [ssoEnabled, setSsoEnabled] = useState(resource.sso);
const [autoLoginEnabled, setAutoLoginEnabled] = useState(
resource.skipToIdpId !== null && resource.skipToIdpId !== undefined
);
const [selectedIdpId, setSelectedIdpId] = useState<number | null>( const [selectedIdpId, setSelectedIdpId] = useState<number | null>(
resource.skipToIdpId || null resource.skipToIdpId || null
); );
@@ -243,19 +239,8 @@ export default function ResourceAuthenticationPage() {
text: w.email text: w.email
})) }))
); );
if (autoLoginEnabled && !selectedIdpId && orgIdps.length > 0) {
setSelectedIdpId(orgIdps[0].idpId);
}
hasInitializedRef.current = true; hasInitializedRef.current = true;
}, [ }, [pageLoading, resourceRoles, resourceUsers, whitelist, orgIdps]);
pageLoading,
resourceRoles,
resourceUsers,
whitelist,
autoLoginEnabled,
selectedIdpId,
orgIdps
]);
const [, submitUserRolesForm, loadingSaveUsersRoles] = useActionState( const [, submitUserRolesForm, loadingSaveUsersRoles] = useActionState(
onSubmitUsersRoles, onSubmitUsersRoles,
@@ -269,16 +254,6 @@ export default function ResourceAuthenticationPage() {
const data = usersRolesForm.getValues(); const data = usersRolesForm.getValues();
try { try {
// Validate that an IDP is selected if auto login is enabled
if (autoLoginEnabled && !selectedIdpId) {
toast({
variant: "destructive",
title: t("error"),
description: t("selectIdpRequired")
});
return;
}
const jobs = [ const jobs = [
api.post(`/resource/${resource.resourceId}/roles`, { api.post(`/resource/${resource.resourceId}/roles`, {
roleIds: data.roles.map((i) => parseInt(i.id)) roleIds: data.roles.map((i) => parseInt(i.id))
@@ -288,7 +263,7 @@ export default function ResourceAuthenticationPage() {
}), }),
api.post(`/resource/${resource.resourceId}`, { api.post(`/resource/${resource.resourceId}`, {
sso: ssoEnabled, sso: ssoEnabled,
skipToIdpId: autoLoginEnabled ? selectedIdpId : null skipToIdpId: selectedIdpId
}) })
]; ];
@@ -296,7 +271,7 @@ export default function ResourceAuthenticationPage() {
updateResource({ updateResource({
sso: ssoEnabled, sso: ssoEnabled,
skipToIdpId: autoLoginEnabled ? selectedIdpId : null skipToIdpId: selectedIdpId
}); });
updateAuthInfo({ updateAuthInfo({
@@ -619,88 +594,53 @@ export default function ResourceAuthenticationPage() {
)} )}
{ssoEnabled && allIdps.length > 0 && ( {ssoEnabled && allIdps.length > 0 && (
<> <div className="space-y-2">
<div className="space-y-2 mb-3"> <label className="text-sm font-medium">
<CheckboxWithLabel {t("defaultIdentityProvider")}
label={t( </label>
"autoLoginExternalIdp" <Select
)} onValueChange={(value) => {
checked={autoLoginEnabled} if (value === "none") {
onCheckedChange={( setSelectedIdpId(null);
checked } else {
) => { setSelectedIdpId(
setAutoLoginEnabled( parseInt(value)
checked as boolean
); );
if ( }
checked && }}
allIdps.length > 0 value={
) { selectedIdpId
setSelectedIdpId( ? selectedIdpId.toString()
allIdps[0].id : "none"
); }
} else { >
setSelectedIdpId( <SelectTrigger className="w-full mt-1">
null <SelectValue
); placeholder={t(
} "selectIdpPlaceholder"
}}
/>
<p className="text-sm text-muted-foreground">
{t(
"autoLoginExternalIdpDescription"
)}
</p>
</div>
{autoLoginEnabled && (
<div className="space-y-2">
<label className="text-sm font-medium">
{t(
"defaultIdentityProvider"
)} )}
</label> />
<Select </SelectTrigger>
onValueChange={( <SelectContent>
value <SelectItem value="none">
) => {t("none")}
setSelectedIdpId( </SelectItem>
parseInt(value) {allIdps.map((idp) => (
) <SelectItem
} key={idp.id}
value={ value={idp.id.toString()}
selectedIdpId >
? selectedIdpId.toString() {idp.text}
: undefined </SelectItem>
} ))}
> </SelectContent>
<SelectTrigger className="w-full mt-1"> </Select>
<SelectValue <p className="text-sm text-muted-foreground">
placeholder={t( {t(
"selectIdpPlaceholder" "defaultIdentityProviderDescription"
)} )}
/> </p>
</SelectTrigger> </div>
<SelectContent>
{allIdps.map(
(idp) => (
<SelectItem
key={
idp.id
}
value={idp.id.toString()}
>
{
idp.text
}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
)}
</>
)} )}
</form> </form>
</Form> </Form>

View File

@@ -118,8 +118,7 @@ export default function ResourceRules(props: {
const [countrySelectValue, setCountrySelectValue] = useState(""); const [countrySelectValue, setCountrySelectValue] = useState("");
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] =
useState(false); useState(false);
const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false);
useState(false);
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const t = useTranslations();
const { env } = useEnvContext(); const { env } = useEnvContext();
@@ -542,7 +541,11 @@ export default function ResourceRules(props: {
updateRule(row.original.ruleId, { updateRule(row.original.ruleId, {
match: value, match: value,
value: value:
value === "COUNTRY" ? "US" : value === "ASN" ? "AS15169" : row.original.value value === "COUNTRY"
? "US"
: value === "ASN"
? "AS15169"
: row.original.value
}) })
} }
> >
@@ -559,9 +562,7 @@ export default function ResourceRules(props: {
</SelectItem> </SelectItem>
)} )}
{isMaxmindAsnAvailable && ( {isMaxmindAsnAvailable && (
<SelectItem value="ASN"> <SelectItem value="ASN">{RuleMatch.ASN}</SelectItem>
{RuleMatch.ASN}
</SelectItem>
)} )}
</SelectContent> </SelectContent>
</Select> </Select>
@@ -654,9 +655,7 @@ export default function ResourceRules(props: {
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="min-w-[200px] p-0"> <PopoverContent className="min-w-[200px] p-0">
<Command> <Command>
<CommandInput <CommandInput placeholder="Search ASNs or enter custom..." />
placeholder="Search ASNs or enter custom..."
/>
<CommandList> <CommandList>
<CommandEmpty> <CommandEmpty>
No ASN found. Enter a custom ASN below. No ASN found. Enter a custom ASN below.
@@ -665,7 +664,9 @@ export default function ResourceRules(props: {
{MAJOR_ASNS.map((asn) => ( {MAJOR_ASNS.map((asn) => (
<CommandItem <CommandItem
key={asn.code} key={asn.code}
value={asn.name + " " + asn.code} value={
asn.name + " " + asn.code
}
onSelect={() => { onSelect={() => {
updateRule( updateRule(
row.original.ruleId, row.original.ruleId,
@@ -1056,8 +1057,8 @@ export default function ResourceRules(props: {
</PopoverContent> </PopoverContent>
</Popover> </Popover>
) : addRuleForm.watch( ) : addRuleForm.watch(
"match" "match"
) === "ASN" ? ( ) === "ASN" ? (
<Popover <Popover
open={ open={
openAddRuleAsnSelect openAddRuleAsnSelect
@@ -1086,21 +1087,27 @@ export default function ResourceRules(props: {
field.value field.value
) )
?.name + ?.name +
" (" + " (" +
field.value + field.value +
")" || field.value ")" ||
field.value
: "Select ASN"} : "Select ASN"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-full p-0"> <PopoverContent className="w-full p-0">
<Command> <Command>
<CommandInput <CommandInput placeholder="Search ASNs or enter custom..." />
placeholder="Search ASNs or enter custom..."
/>
<CommandList> <CommandList>
<CommandEmpty> <CommandEmpty>
No ASN found. Use the custom input below. No
ASN
found.
Use
the
custom
input
below.
</CommandEmpty> </CommandEmpty>
<CommandGroup> <CommandGroup>
{MAJOR_ASNS.map( {MAJOR_ASNS.map(
@@ -1112,7 +1119,9 @@ export default function ResourceRules(props: {
asn.code asn.code
} }
value={ value={
asn.name + " " + asn.code asn.name +
" " +
asn.code
} }
onSelect={() => { onSelect={() => {
field.onChange( field.onChange(
@@ -1138,6 +1147,7 @@ export default function ResourceRules(props: {
{ {
asn.code asn.code
} }
) )
</CommandItem> </CommandItem>
) )
@@ -1148,14 +1158,32 @@ export default function ResourceRules(props: {
<div className="border-t p-2"> <div className="border-t p-2">
<Input <Input
placeholder="Enter custom ASN (e.g., AS15169)" placeholder="Enter custom ASN (e.g., AS15169)"
onKeyDown={(e) => { onKeyDown={(
if (e.key === "Enter") { e
const value = e.currentTarget.value ) => {
.toUpperCase() if (
.replace(/^AS/, ""); e.key ===
if (/^\d+$/.test(value)) { "Enter"
field.onChange("AS" + value); ) {
setOpenAddRuleAsnSelect(false); const value =
e.currentTarget.value
.toUpperCase()
.replace(
/^AS/,
""
);
if (
/^\d+$/.test(
value
)
) {
field.onChange(
"AS" +
value
);
setOpenAddRuleAsnSelect(
false
);
} }
} }
}} }}

View File

@@ -756,7 +756,9 @@ WantedBy=default.target`
render={({ field }) => ( render={({ field }) => (
<FormItem className="md:col-start-1 md:col-span-2"> <FormItem className="md:col-start-1 md:col-span-2">
<FormLabel> <FormLabel>
{t("siteAddress")} {t(
"siteAddress"
)}
</FormLabel> </FormLabel>
<FormControl> <FormControl>
<Input <Input
@@ -909,7 +911,7 @@ WantedBy=default.target`
? "squareOutlinePrimary" ? "squareOutlinePrimary"
: "squareOutline" : "squareOutline"
} }
className={`flex-1 min-w-[120px] ${platform === os ? "bg-primary/10" : ""} shadow-none`} className={`flex-1 min-w-30 ${platform === os ? "bg-primary/10" : ""} shadow-none`}
onClick={() => { onClick={() => {
setPlatform(os); setPlatform(os);
}} }}
@@ -940,7 +942,7 @@ WantedBy=default.target`
? "squareOutlinePrimary" ? "squareOutlinePrimary"
: "squareOutline" : "squareOutline"
} }
className={`flex-1 min-w-[120px] ${architecture === arch ? "bg-primary/10" : ""} shadow-none`} className={`flex-1 min-w-30 ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
onClick={() => onClick={() =>
setArchitecture( setArchitecture(
arch arch

View File

@@ -87,8 +87,6 @@ export default async function OrgAuthPage(props: {
redirect(env.app.dashboardUrl); redirect(env.app.dashboardUrl);
} }
console.log(user, forceLogin);
if (user && !forceLogin) { if (user && !forceLogin) {
let redirectToken: string | undefined; let redirectToken: string | undefined;
try { try {

View File

@@ -66,4 +66,3 @@ export const ClientDownloadBanner = () => {
}; };
export default ClientDownloadBanner; export default ClientDownloadBanner;

View File

@@ -99,14 +99,12 @@ export default function ClientResourcesTable({
siteId: number siteId: number
) => { ) => {
try { try {
await api await api.delete(`/site-resource/${resourceId}`).then(() => {
.delete(`/site-resource/${resourceId}`) startTransition(() => {
.then(() => { router.refresh();
startTransition(() => { setIsDeleteModalOpen(false);
router.refresh();
setIsDeleteModalOpen(false);
});
}); });
});
} catch (e) { } catch (e) {
console.error(t("resourceErrorDelete"), e); console.error(t("resourceErrorDelete"), e);
toast({ toast({

View File

@@ -87,7 +87,12 @@ const isValidPortRangeString = (val: string | undefined | null): boolean => {
return false; return false;
} }
if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) { if (
startPort < 1 ||
startPort > 65535 ||
endPort < 1 ||
endPort > 65535
) {
return false; return false;
} }
@@ -131,7 +136,10 @@ const getPortModeFromString = (val: string | undefined | null): PortMode => {
}; };
// Helper to get the port string for API from mode and custom value // Helper to get the port string for API from mode and custom value
const getPortStringFromMode = (mode: PortMode, customValue: string): string | undefined => { const getPortStringFromMode = (
mode: PortMode,
customValue: string
): string | undefined => {
if (mode === "all") return "*"; if (mode === "all") return "*";
if (mode === "blocked") return ""; if (mode === "blocked") return "";
return customValue; return customValue;
@@ -1097,8 +1105,7 @@ export default function CreateInternalResourceDialog({
size="sm" size="sm"
tags={ tags={
form.getValues() form.getValues()
.roles || .roles || []
[]
} }
setTags={( setTags={(
newRoles newRoles
@@ -1154,8 +1161,7 @@ export default function CreateInternalResourceDialog({
)} )}
tags={ tags={
form.getValues() form.getValues()
.users || .users || []
[]
} }
size="sm" size="sm"
setTags={( setTags={(
@@ -1245,9 +1251,7 @@ export default function CreateInternalResourceDialog({
restrictTagsToAutocompleteOptions={ restrictTagsToAutocompleteOptions={
true true
} }
sortTags={ sortTags={true}
true
}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -95,4 +95,3 @@ export const DismissableBanner = ({
}; };
export default DismissableBanner; export default DismissableBanner;

View File

@@ -501,13 +501,19 @@ export default function EditInternalResourceDialog({
// ]); // ]);
await queryClient.invalidateQueries( await queryClient.invalidateQueries(
resourceQueries.siteResourceRoles({ siteResourceId: resource.id }) resourceQueries.siteResourceRoles({
siteResourceId: resource.id
})
); );
await queryClient.invalidateQueries( await queryClient.invalidateQueries(
resourceQueries.siteResourceUsers({ siteResourceId: resource.id }) resourceQueries.siteResourceUsers({
siteResourceId: resource.id
})
); );
await queryClient.invalidateQueries( await queryClient.invalidateQueries(
resourceQueries.siteResourceClients({ siteResourceId: resource.id }) resourceQueries.siteResourceClients({
siteResourceId: resource.id
})
); );
toast({ toast({

View File

@@ -330,7 +330,7 @@ export default function ExitNodesTable({
isRefreshing={isRefreshing} isRefreshing={isRefreshing}
columnVisibility={{ columnVisibility={{
type: false, type: false,
address: false, address: false
}} }}
enableColumnVisibility={true} enableColumnVisibility={true}
/> />

View File

@@ -116,10 +116,12 @@ export function LayoutSidebar({
isCollapsed={isSidebarCollapsed} isCollapsed={isSidebarCollapsed}
/> />
</div> </div>
<div className={cn( <div
"w-full border-b border-border", className={cn(
isSidebarCollapsed && "mb-2" "w-full border-b border-border",
)} /> isSidebarCollapsed && "mb-2"
)}
/>
<div className="flex-1 overflow-y-auto relative"> <div className="flex-1 overflow-y-auto relative">
<div className="px-2 pt-1"> <div className="px-2 pt-1">
{!isAdminPage && user.serverAdmin && ( {!isAdminPage && user.serverAdmin && (

View File

@@ -120,7 +120,9 @@ export default function LoginForm({
const focusInput = () => { const focusInput = () => {
// Try using the ref first // Try using the ref first
if (otpContainerRef.current) { if (otpContainerRef.current) {
const hiddenInput = otpContainerRef.current.querySelector('input') as HTMLInputElement; const hiddenInput = otpContainerRef.current.querySelector(
"input"
) as HTMLInputElement;
if (hiddenInput) { if (hiddenInput) {
hiddenInput.focus(); hiddenInput.focus();
return; return;
@@ -128,17 +130,23 @@ export default function LoginForm({
} }
// Fallback: query the DOM // Fallback: query the DOM
const otpContainer = document.querySelector('[data-slot="input-otp"]'); const otpContainer = document.querySelector(
'[data-slot="input-otp"]'
);
if (!otpContainer) return; if (!otpContainer) return;
const hiddenInput = otpContainer.querySelector('input') as HTMLInputElement; const hiddenInput = otpContainer.querySelector(
"input"
) as HTMLInputElement;
if (hiddenInput) { if (hiddenInput) {
hiddenInput.focus(); hiddenInput.focus();
return; return;
} }
// Last resort: click the first slot // Last resort: click the first slot
const firstSlot = otpContainer.querySelector('[data-slot="input-otp-slot"]') as HTMLElement; const firstSlot = otpContainer.querySelector(
'[data-slot="input-otp-slot"]'
) as HTMLElement;
if (firstSlot) { if (firstSlot) {
firstSlot.click(); firstSlot.click();
} }
@@ -508,7 +516,10 @@ export default function LoginForm({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<div ref={otpContainerRef} className="flex justify-center"> <div
ref={otpContainerRef}
className="flex justify-center"
>
<InputOTP <InputOTP
maxLength={6} maxLength={6}
{...field} {...field}

View File

@@ -11,9 +11,7 @@ type MachineClientsBannerProps = {
orgId: string; orgId: string;
}; };
export const MachineClientsBanner = ({ export const MachineClientsBanner = ({ orgId }: MachineClientsBannerProps) => {
orgId
}: MachineClientsBannerProps) => {
const t = useTranslations(); const t = useTranslations();
return ( return (
@@ -57,4 +55,3 @@ export const MachineClientsBanner = ({
}; };
export default MachineClientsBanner; export default MachineClientsBanner;

View File

@@ -39,4 +39,3 @@ export default function OrgInfoCard({}: OrgInfoCardProps) {
</Alert> </Alert>
); );
} }

View File

@@ -119,4 +119,3 @@ export default async function OrgLoginPage({
</div> </div>
); );
} }

View File

@@ -95,7 +95,7 @@ export function OrgSelectionForm() {
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{t("orgAuthWhatsThis")}{" "} {t("orgAuthWhatsThis")}{" "}
<Link <Link
href="https://docs.pangolin.net/manage/identity-providers/add-an-idp" href="https://docs.pangolin.net/manage/organizations/org-id"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="underline" className="underline"

View File

@@ -51,4 +51,3 @@ export const PrivateResourcesBanner = ({
}; };
export default PrivateResourcesBanner; export default PrivateResourcesBanner;

View File

@@ -41,7 +41,10 @@ export default function ProductUpdates({
const data = useQueries({ const data = useQueries({
queries: [ queries: [
productUpdatesQueries.list(env.app.notifications.product_updates), productUpdatesQueries.list(
env.app.notifications.product_updates,
env.app.version
),
productUpdatesQueries.latestVersion( productUpdatesQueries.latestVersion(
env.app.notifications.new_releases env.app.notifications.new_releases
) )
@@ -189,7 +192,7 @@ function ProductUpdatesListPopup({
<div <div
className={cn( className={cn(
"relative z-1 cursor-pointer block group", "relative z-1 cursor-pointer block group",
"rounded-md border border-primary/30 bg-gradient-to-br dark:from-primary/20 from-primary/20 via-background to-background p-2 py-3 w-full flex flex-col gap-2 text-sm", "rounded-md border border-primary/30 bg-linear-to-br dark:from-primary/20 from-primary/20 via-background to-background p-2 py-3 w-full flex flex-col gap-2 text-sm",
"transition duration-300 ease-in-out", "transition duration-300 ease-in-out",
"data-closed:opacity-0 data-closed:translate-y-full" "data-closed:opacity-0 data-closed:translate-y-full"
)} )}
@@ -343,7 +346,7 @@ function NewVersionAvailable({
rel="noopener noreferrer" rel="noopener noreferrer"
className={cn( className={cn(
"relative z-2 group cursor-pointer block", "relative z-2 group cursor-pointer block",
"rounded-md border border-primary/30 bg-gradient-to-br dark:from-primary/20 from-primary/20 via-background to-background p-2 py-3 w-full flex flex-col gap-2 text-sm", "rounded-md border border-primary/30 bg-linear-to-br dark:from-primary/20 from-primary/20 via-background to-background p-2 py-3 w-full flex flex-col gap-2 text-sm",
"transition duration-300 ease-in-out", "transition duration-300 ease-in-out",
"data-closed:opacity-0 data-closed:translate-y-full" "data-closed:opacity-0 data-closed:translate-y-full"
)} )}

View File

@@ -20,4 +20,3 @@ export const ProxyResourcesBanner = () => {
}; };
export default ProxyResourcesBanner; export default ProxyResourcesBanner;

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,14 +25,14 @@ 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 { SwitchInput } from "@/components/SwitchInput";
import {InfoPopup} from "@/components/ui/info-popup"; 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),
@@ -56,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();
@@ -82,11 +82,14 @@ export default function SetResourceHeaderAuthForm({
async function onSubmit(data: SetHeaderAuthFormValues) { async function onSubmit(data: SetHeaderAuthFormValues) {
setLoading(true); setLoading(true);
api.post<AxiosResponse<Resource>>(`/resource/${resourceId}/header-auth`, { api.post<AxiosResponse<Resource>>(
user: data.user, `/resource/${resourceId}/header-auth`,
password: data.password, {
extendedCompatibility: data.extendedCompatibility user: data.user,
}) password: data.password,
extendedCompatibility: data.extendedCompatibility
}
)
.then(() => { .then(() => {
toast({ toast({
title: t("resourceHeaderAuthSetup"), title: t("resourceHeaderAuthSetup"),
@@ -100,10 +103,10 @@ export default function SetResourceHeaderAuthForm({
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('resourceErrorHeaderAuthSetup'), title: t("resourceErrorHeaderAuthSetup"),
description: formatAxiosError( description: formatAxiosError(
e, e,
t('resourceErrorHeaderAuthSetupDescription') t("resourceErrorHeaderAuthSetupDescription")
) )
}); });
}) })
@@ -139,7 +142,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>
@@ -149,14 +152,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")}
@@ -168,25 +171,31 @@ export default function SetResourceHeaderAuthForm({
{...field} {...field}
/> />
</FormControl> </FormControl>
<FormMessage/> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />
<FormField <FormField
control={form.control} control={form.control}
name="extendedCompatibility" name="extendedCompatibility"
render={({field}) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormControl> <FormControl>
<SwitchInput <SwitchInput
id="header-auth-compatibility-toggle" id="header-auth-compatibility-toggle"
label={t("headerAuthCompatibility")} label={t(
info={t('headerAuthCompatibilityInfo')} "headerAuthCompatibility"
)}
info={t(
"headerAuthCompatibilityInfo"
)}
checked={field.value} checked={field.value}
onCheckedChange={field.onChange} onCheckedChange={
field.onChange
}
/> />
</FormControl> </FormControl>
<FormMessage/> <FormMessage />
</FormItem> </FormItem>
)} )}
/> />

View File

@@ -91,10 +91,10 @@ export default function SetResourcePasswordForm({
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('resourceErrorPasswordSetup'), title: t("resourceErrorPasswordSetup"),
description: formatAxiosError( description: formatAxiosError(
e, e,
t('resourceErrorPasswordSetupDescription') t("resourceErrorPasswordSetupDescription")
) )
}); });
}) })

View File

@@ -97,10 +97,10 @@ export default function SetResourcePincodeForm({
.catch((e) => { .catch((e) => {
toast({ toast({
variant: "destructive", variant: "destructive",
title: t('resourceErrorPincodeSetup'), title: t("resourceErrorPincodeSetup"),
description: formatAxiosError( description: formatAxiosError(
e, e,
t('resourceErrorPincodeSetupDescription') t("resourceErrorPincodeSetupDescription")
) )
}); });
}) })

View File

@@ -37,4 +37,3 @@ export const SitesBanner = () => {
}; };
export default SitesBanner; export default SitesBanner;

View File

@@ -1,10 +1,14 @@
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 { Button } from "@/components/ui/button";
import {Info} from "lucide-react"; import { Info } from "lucide-react";
import {info} from "winston"; import { info } from "winston";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover"; import {
Popover,
PopoverContent,
PopoverTrigger
} from "@/components/ui/popover";
interface SwitchComponentProps { interface SwitchComponentProps {
id: string; id: string;
@@ -18,22 +22,22 @@ interface SwitchComponentProps {
} }
export function SwitchInput({ export function SwitchInput({
id, id,
label, label,
description, description,
info, info,
disabled, disabled,
checked, checked,
defaultChecked = false, defaultChecked = false,
onCheckedChange onCheckedChange
}: SwitchComponentProps) { }: SwitchComponentProps) {
const defaultTrigger = ( const defaultTrigger = (
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-6 w-6 rounded-full p-0" className="h-6 w-6 rounded-full p-0"
> >
<Info className="h-4 w-4"/> <Info className="h-4 w-4" />
<span className="sr-only">Show info</span> <span className="sr-only">Show info</span>
</Button> </Button>
); );
@@ -49,18 +53,20 @@ export function SwitchInput({
disabled={disabled} disabled={disabled}
/> />
{label && <Label htmlFor={id}>{label}</Label>} {label && <Label htmlFor={id}>{label}</Label>}
{info && <Popover> {info && (
<PopoverTrigger asChild> <Popover>
{defaultTrigger} <PopoverTrigger asChild>
</PopoverTrigger> {defaultTrigger}
<PopoverContent className="w-80"> </PopoverTrigger>
{info && ( <PopoverContent className="w-80">
<p className="text-sm text-muted-foreground"> {info && (
{info} <p className="text-sm text-muted-foreground">
</p> {info}
)} </p>
</PopoverContent> )}
</Popover>} </PopoverContent>
</Popover>
)}
</div> </div>
{description && ( {description && (
<span className="text-muted-foreground text-sm"> <span className="text-muted-foreground text-sm">

View File

@@ -276,14 +276,15 @@ function AuthPageSettings({
<> <>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle>{t("customDomain")}</SettingsSectionTitle> <SettingsSectionTitle>
{t("customDomain")}
</SettingsSectionTitle>
<SettingsSectionDescription> <SettingsSectionDescription>
{t("authPageDescription")} {t("authPageDescription")}
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<SettingsSectionForm> <SettingsSectionForm>
<PaidFeaturesAlert /> <PaidFeaturesAlert />
<Form {...form}> <Form {...form}>

View File

@@ -58,12 +58,7 @@ export default function IdpLoginButtons({
let redirectToUrl: string | undefined; let redirectToUrl: string | undefined;
try { try {
console.log( console.log("generating", idpId, redirect || "/", orgId);
"generating",
idpId,
redirect || "/",
orgId
);
const safeRedirect = cleanRedirect(redirect || "/"); const safeRedirect = cleanRedirect(redirect || "/");
const response = await generateOidcUrlProxy( const response = await generateOidcUrlProxy(
idpId, idpId,

View File

@@ -288,7 +288,10 @@ export function DataTable<TData, TValue>({
useEffect(() => { useEffect(() => {
if (persistPageSize && pagination.pageSize !== pageSize) { if (persistPageSize && pagination.pageSize !== pageSize) {
// Only store if user has actually changed it from initial value // Only store if user has actually changed it from initial value
if (hasUserChangedPageSize.current && pagination.pageSize !== initialPageSize.current) { if (
hasUserChangedPageSize.current &&
pagination.pageSize !== initialPageSize.current
) {
setStoredPageSize(pagination.pageSize, tableId); setStoredPageSize(pagination.pageSize, tableId);
} }
setPageSize(pagination.pageSize); setPageSize(pagination.pageSize);
@@ -298,7 +301,9 @@ export function DataTable<TData, TValue>({
useEffect(() => { useEffect(() => {
// Persist column visibility to localStorage when it changes (but not on initial mount) // Persist column visibility to localStorage when it changes (but not on initial mount)
if (shouldPersistColumnVisibility) { if (shouldPersistColumnVisibility) {
const hasChanged = JSON.stringify(columnVisibility) !== JSON.stringify(initialColumnVisibilityState.current); const hasChanged =
JSON.stringify(columnVisibility) !==
JSON.stringify(initialColumnVisibilityState.current);
if (hasChanged) { if (hasChanged) {
// Mark as user-initiated change and persist // Mark as user-initiated change and persist
hasUserChangedColumnVisibility.current = true; hasUserChangedColumnVisibility.current = true;

View File

@@ -41,12 +41,13 @@ export type LatestVersionResponse = {
}; };
export const productUpdatesQueries = { export const productUpdatesQueries = {
list: (enabled: boolean) => list: (enabled: boolean, version?: string) =>
queryOptions({ queryOptions({
queryKey: ["PRODUCT_UPDATES"] as const, queryKey: ["PRODUCT_UPDATES"] as const,
queryFn: async ({ signal }) => { queryFn: async ({ signal }) => {
const sp = new URLSearchParams({ const sp = new URLSearchParams({
build build,
...(version ? { version } : {})
}); });
const data = await remote.get<ResponseT<ProductUpdate[]>>( const data = await remote.get<ResponseT<ProductUpdate[]>>(
`/product-updates?${sp.toString()}`, `/product-updates?${sp.toString()}`,