Compare commits

...

6 Commits

Author SHA1 Message Date
Owen 517e1d15c8 Add 1.19.0 migrations 2026-06-01 14:42:32 -07:00
Owen 3d6d5f176a Hide verify button 2026-06-01 14:10:08 -07:00
Owen 5dd19edb56 Hold the hp error message until after 18 tries 2026-06-01 14:05:19 -07:00
Owen c6a52ffc75 Dont run migration again when rc 2026-06-01 13:58:04 -07:00
Owen 09b2671759 Send hp error to olm 2026-06-01 13:57:54 -07:00
Owen d11a244caa Push mode and sign key adjustments for native mode 2026-06-01 11:41:55 -07:00
11 changed files with 856 additions and 74 deletions
+2 -2
View File
@@ -467,14 +467,14 @@ export async function signSshKey(
const validFor = 300n;
expiresIn = Number(validFor); // seconds
const cert = signPublicKey(caKeys.privateKeyPem, publicKey, {
cert = signPublicKey(caKeys.privateKeyPem, publicKey, {
keyId: `${usernameToUse}@${resource.niceId}`,
validPrincipals: [usernameToUse, resource.niceId],
validAfter: now - 60n, // Start 1 min ago for clock skew
validBefore: now + validFor
});
const messageIds: number[] = [];
messageIds = [];
for (const siteId of siteIds) {
// get the site
const [newt] = await db
+8
View File
@@ -1,4 +1,8 @@
import { sendToClient } from "#dynamic/routers/ws";
import config from "@server/lib/config";
const udpPort = config.getRawConfig().gerbil.clients_start_port;
// Error codes for registration failures
export const OlmErrorCodes = {
OLM_NOT_FOUND: {
@@ -86,6 +90,10 @@ export const OlmErrorCodes = {
TERMINATED_BLOCKED: {
code: "TERMINATED_BLOCKED",
message: "This session was terminated because access was blocked."
},
HOLEPUNCH_MISSING: {
code: "HOLEPUNCH_MISSING",
message: `Unable to coordinate client P2P connection. Please ensure your client can reach the server on UDP port ${udpPort} and try registering again.`
}
} as const;
@@ -20,6 +20,14 @@ import { handleFingerprintInsertion } from "./fingerprintingUtils";
import { build } from "@server/build";
import { canCompress } from "@server/lib/clientVersionChecks";
import config from "@server/lib/config";
import cache from "#dynamic/lib/cache";
const HOLEPUNCH_STALE_CHAIN_THRESHOLD = 18;
const HOLEPUNCH_STALE_CHAIN_TTL_SECONDS = 1800;
function getHolePunchChainCounterKey(olmId: string, chainId: string): string {
return `olm:register:stale_holepunch:${olmId}:${chainId}`;
}
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
logger.info("[handleOlmRegisterMessage] Handling register olm message");
@@ -319,6 +327,24 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
);
}
let staleHolePunchChainCount: number | undefined;
const hasChainId =
chainId !== undefined && chainId !== null && String(chainId) !== "";
if (hasChainId) {
const cacheKey = getHolePunchChainCounterKey(
olm.olmId,
String(chainId)
);
const existingCount = (await cache.get<number>(cacheKey)) ?? 0;
staleHolePunchChainCount = existingCount + 1;
await cache.set(
cacheKey,
staleHolePunchChainCount,
HOLEPUNCH_STALE_CHAIN_TTL_SECONDS
);
}
// this prevents us from accepting a register from an olm that has not hole punched yet.
// the olm will pump the register so we can keep checking
// TODO: I still think there is a better way to do this rather than locking it out here but ???
@@ -327,6 +353,34 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
`[handleOlmRegisterMessage] Client last hole punch is too old and we have sites to send; skipping this register. The client is failing to hole punch and identify its network address with the server. Can the client reach the server on UDP port ${config.getRawConfig().gerbil.clients_start_port}?`,
{ orgId: client.orgId, clientId: client.clientId }
);
if (!hasChainId) {
logger.debug(
"[handleOlmRegisterMessage] Skipping HOLEPUNCH_MISSING because chainId is missing",
{
orgId: client.orgId,
clientId: client.clientId,
olmId: olm.olmId
}
);
return;
}
if (staleHolePunchChainCount === HOLEPUNCH_STALE_CHAIN_THRESHOLD) {
sendOlmError(OlmErrorCodes.HOLEPUNCH_MISSING, olm.olmId);
} else {
logger.debug(
"[handleOlmRegisterMessage] Suppressing HOLEPUNCH_MISSING until chain threshold is met",
{
orgId: client.orgId,
clientId: client.clientId,
olmId: olm.olmId,
chainId,
staleHolePunchChainCount,
threshold: HOLEPUNCH_STALE_CHAIN_THRESHOLD
}
);
}
return;
}
+1 -1
View File
@@ -386,7 +386,7 @@ export type GetUserResourcesResponse = {
domain: string;
enabled: boolean;
protected: boolean;
mode: string;
ode: string;
}>;
siteResources: Array<{
siteResourceId: number;
@@ -196,6 +196,7 @@ function querySiteResourcesBase() {
disableIcmp: siteResources.disableIcmp,
authDaemonMode: siteResources.authDaemonMode,
authDaemonPort: siteResources.authDaemonPort,
pamMode: siteResources.pamMode,
subdomain: siteResources.subdomain,
domainId: siteResources.domainId,
fullDomain: siteResources.fullDomain,
+24 -2
View File
@@ -25,6 +25,7 @@ import m16 from "./scriptsPg/1.17.0";
import m17 from "./scriptsPg/1.18.0";
import m18 from "./scriptsPg/1.18.3";
import m19 from "./scriptsPg/1.18.4";
import m20 from "./scriptsPg/1.19.0";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -49,7 +50,8 @@ const migrations = [
{ version: "1.17.0", run: m16 },
{ version: "1.18.0", run: m17 },
{ version: "1.18.3", run: m18 },
{ version: "1.18.4", run: m19 }
{ version: "1.18.4", run: m19 },
{ version: "1.19.0", run: m20 }
// Add new migrations here as they are created
] as {
version: string;
@@ -128,7 +130,7 @@ async function executeScripts() {
console.log(`Starting migrations from version ${startVersion}`);
const migrationsToRun = migrations.filter((migration) =>
semver.gt(migration.version, startVersion)
shouldRunMigration(migration.version, startVersion)
);
console.log(
@@ -179,3 +181,23 @@ async function executeScripts() {
throw error;
}
}
function shouldRunMigration(migrationVersion: string, currentVersion: string) {
const migration = semver.parse(migrationVersion);
const current = semver.parse(currentVersion);
// Treat x.y.z-rc.* as equivalent to x.y.z so restarts on RC builds do not re-run the same migration.
if (
migration &&
current &&
migration.prerelease.length === 0 &&
current.prerelease[0] === "rc" &&
migration.major === current.major &&
migration.minor === current.minor &&
migration.patch === current.patch
) {
return false;
}
return semver.gt(migrationVersion, currentVersion);
}
+24 -2
View File
@@ -43,6 +43,7 @@ import m37 from "./scriptsSqlite/1.17.0";
import m38 from "./scriptsSqlite/1.18.0";
import m39 from "./scriptsSqlite/1.18.3";
import m40 from "./scriptsSqlite/1.18.4";
import m41 from "./scriptsSqlite/1.19.0";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -83,7 +84,8 @@ const migrations = [
{ version: "1.17.0", run: m37 },
{ version: "1.18.0", run: m38 },
{ version: "1.18.3", run: m39 },
{ version: "1.18.4", run: m40 }
{ version: "1.18.4", run: m40 },
{ version: "1.19.0", run: m41 }
// Add new migrations here as they are created
] as const;
@@ -169,7 +171,7 @@ async function executeScripts() {
console.log(`Starting migrations from version ${startVersion}`);
const migrationsToRun = migrations.filter((migration) =>
semver.gt(migration.version, startVersion)
shouldRunMigration(migration.version, startVersion)
);
console.log(
@@ -223,3 +225,23 @@ async function executeScripts() {
throw error;
}
}
function shouldRunMigration(migrationVersion: string, currentVersion: string) {
const migration = semver.parse(migrationVersion);
const current = semver.parse(currentVersion);
// Treat x.y.z-rc.* as equivalent to x.y.z so restarts on RC builds do not re-run the same migration.
if (
migration &&
current &&
migration.prerelease.length === 0 &&
current.prerelease[0] === "rc" &&
migration.major === current.major &&
migration.minor === current.minor &&
migration.patch === current.patch
) {
return false;
}
return semver.gt(migrationVersion, currentVersion);
}
+313
View File
@@ -0,0 +1,313 @@
import { db } from "@server/db/pg/driver";
import { APP_PATH } from "@server/lib/consts";
import { sql } from "drizzle-orm";
import fs from "fs";
import yaml from "js-yaml";
import path from "path";
import z from "zod";
import { fromZodError } from "zod-validation-error";
const version = "1.19.0";
export default async function migration() {
console.log(`Running setup script ${version}...`);
try {
await db.execute(sql`BEGIN`);
await db.execute(sql`
CREATE TABLE "browserGatewayTarget" (
"browserGatewayTargetId" serial PRIMARY KEY NOT NULL,
"resourceId" integer NOT NULL,
"siteId" integer NOT NULL,
"authToken" varchar NOT NULL,
"type" varchar NOT NULL,
"destination" varchar NOT NULL,
"destinationPort" integer NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "clientLabels" (
"clientLabelId" serial PRIMARY KEY NOT NULL,
"clientId" integer NOT NULL,
"labelId" integer NOT NULL,
CONSTRAINT "client_label_uniq" UNIQUE("clientId","labelId")
);
`);
await db.execute(sql`
CREATE TABLE "labels" (
"labelId" serial PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"color" varchar NOT NULL,
"orgId" varchar NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "resourceLabels" (
"resourceLabelId" serial PRIMARY KEY NOT NULL,
"resourceId" integer NOT NULL,
"labelId" integer NOT NULL,
CONSTRAINT "resource_label_uniq" UNIQUE("resourceId","labelId")
);
`);
await db.execute(sql`
CREATE TABLE "resourcePolicies" (
"resourcePolicyId" serial PRIMARY KEY NOT NULL,
"sso" boolean DEFAULT true NOT NULL,
"applyRules" boolean DEFAULT false NOT NULL,
"scope" varchar DEFAULT 'global' NOT NULL,
"emailWhitelistEnabled" boolean DEFAULT false NOT NULL,
"idpId" integer,
"niceId" text NOT NULL,
"name" varchar NOT NULL,
"orgId" varchar NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "resourcePolicyHeaderAuth" (
"headerAuthId" serial PRIMARY KEY NOT NULL,
"headerAuthHash" varchar NOT NULL,
"extendedCompatibility" boolean DEFAULT true NOT NULL,
"resourcePolicyId" integer NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "resourcePolicyPassword" (
"passwordId" serial PRIMARY KEY NOT NULL,
"passwordHash" varchar NOT NULL,
"resourcePolicyId" integer NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "resourcePolicyPincode" (
"pincodeId" serial PRIMARY KEY NOT NULL,
"pincodeHash" varchar NOT NULL,
"digitLength" integer NOT NULL,
"resourcePolicyId" integer NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "resourcePolicyRules" (
"ruleId" serial PRIMARY KEY NOT NULL,
"resourcePolicyId" integer NOT NULL,
"enabled" boolean DEFAULT true NOT NULL,
"priority" integer NOT NULL,
"action" varchar NOT NULL,
"match" varchar NOT NULL,
"value" varchar NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "resourcePolicyWhitelist" (
"id" serial PRIMARY KEY NOT NULL,
"email" varchar NOT NULL,
"resourcePolicyId" integer NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "rolePolicies" (
"roleId" integer NOT NULL,
"resourcePolicyId" integer NOT NULL
);
`);
await db.execute(sql`
CREATE TABLE "siteLabels" (
"siteLabelId" serial PRIMARY KEY NOT NULL,
"siteId" integer NOT NULL,
"labelId" integer NOT NULL,
CONSTRAINT "site_label_uniq" UNIQUE("siteId","labelId")
);
`);
await db.execute(sql`
CREATE TABLE "siteResourceLabels" (
"siteResourceLabelId" serial PRIMARY KEY NOT NULL,
"siteResourceId" integer NOT NULL,
"labelId" integer NOT NULL,
CONSTRAINT "site_resource_label_uniq" UNIQUE("siteResourceId","labelId")
);
`);
await db.execute(sql`
CREATE TABLE "userPolicies" (
"userId" varchar NOT NULL,
"resourcePolicyId" integer NOT NULL
);
`);
await db.execute(
sql`ALTER TABLE "siteResources" ALTER COLUMN "destination" DROP NOT NULL;`
);
await db.execute(
sql`ALTER TABLE "orgs" ADD COLUMN "settingsEnableGlobalNewtAutoUpdate" boolean DEFAULT false NOT NULL;`
);
await db.execute(
sql`ALTER TABLE "resourceAccessToken" ADD COLUMN "path" varchar;`
);
await db.execute(
sql`ALTER TABLE "resources" ADD COLUMN "resourcePolicyId" integer;`
);
await db.execute(
sql`ALTER TABLE "resources" ADD COLUMN "defaultResourcePolicyId" integer;`
);
await db.execute(
sql`ALTER TABLE "resources" ADD COLUMN "mode" text DEFAULT 'http' NOT NULL;`
);
await db.execute(
sql`ALTER TABLE "resources" ADD COLUMN "pamMode" varchar(32) DEFAULT 'passthrough';`
);
await db.execute(
sql`ALTER TABLE "resources" ADD COLUMN "authDaemonMode" varchar(32) DEFAULT 'site';`
);
await db.execute(
sql`ALTER TABLE "resources" ADD COLUMN "authDaemonPort" integer DEFAULT 22123;`
);
await db.execute(
sql`ALTER TABLE "siteResources" ADD COLUMN "pamMode" varchar(32) DEFAULT 'passthrough';`
);
await db.execute(
sql`ALTER TABLE "sites" ADD COLUMN "autoUpdateEnabled" boolean DEFAULT false NOT NULL;`
);
await db.execute(
sql`ALTER TABLE "sites" ADD COLUMN "autoUpdateOverrideOrg" boolean DEFAULT false NOT NULL;`
);
await db.execute(
sql`ALTER TABLE "browserGatewayTarget" ADD CONSTRAINT "browserGatewayTarget_resourceId_resources_resourceId_fk" FOREIGN KEY ("resourceId") REFERENCES "public"."resources"("resourceId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "browserGatewayTarget" ADD CONSTRAINT "browserGatewayTarget_siteId_sites_siteId_fk" FOREIGN KEY ("siteId") REFERENCES "public"."sites"("siteId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "clientLabels" ADD CONSTRAINT "clientLabels_clientId_clients_clientId_fk" FOREIGN KEY ("clientId") REFERENCES "public"."clients"("clientId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "clientLabels" ADD CONSTRAINT "clientLabels_labelId_labels_labelId_fk" FOREIGN KEY ("labelId") REFERENCES "public"."labels"("labelId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "labels" ADD CONSTRAINT "labels_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "resourceLabels" ADD CONSTRAINT "resourceLabels_resourceId_resources_resourceId_fk" FOREIGN KEY ("resourceId") REFERENCES "public"."resources"("resourceId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "resourceLabels" ADD CONSTRAINT "resourceLabels_labelId_labels_labelId_fk" FOREIGN KEY ("labelId") REFERENCES "public"."labels"("labelId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "resourcePolicies" ADD CONSTRAINT "resourcePolicies_idpId_idp_idpId_fk" FOREIGN KEY ("idpId") REFERENCES "public"."idp"("idpId") ON DELETE set null ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "resourcePolicies" ADD CONSTRAINT "resourcePolicies_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "resourcePolicyHeaderAuth" ADD CONSTRAINT "resourcePolicyHeaderAuth_resourcePolicyId_resourcePolicies_resourcePolicyId_fk" FOREIGN KEY ("resourcePolicyId") REFERENCES "public"."resourcePolicies"("resourcePolicyId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "resourcePolicyPassword" ADD CONSTRAINT "resourcePolicyPassword_resourcePolicyId_resourcePolicies_resourcePolicyId_fk" FOREIGN KEY ("resourcePolicyId") REFERENCES "public"."resourcePolicies"("resourcePolicyId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "resourcePolicyPincode" ADD CONSTRAINT "resourcePolicyPincode_resourcePolicyId_resourcePolicies_resourcePolicyId_fk" FOREIGN KEY ("resourcePolicyId") REFERENCES "public"."resourcePolicies"("resourcePolicyId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "resourcePolicyRules" ADD CONSTRAINT "resourcePolicyRules_resourcePolicyId_resourcePolicies_resourcePolicyId_fk" FOREIGN KEY ("resourcePolicyId") REFERENCES "public"."resourcePolicies"("resourcePolicyId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "resourcePolicyWhitelist" ADD CONSTRAINT "resourcePolicyWhitelist_resourcePolicyId_resourcePolicies_resourcePolicyId_fk" FOREIGN KEY ("resourcePolicyId") REFERENCES "public"."resourcePolicies"("resourcePolicyId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "rolePolicies" ADD CONSTRAINT "rolePolicies_roleId_roles_roleId_fk" FOREIGN KEY ("roleId") REFERENCES "public"."roles"("roleId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "rolePolicies" ADD CONSTRAINT "rolePolicies_resourcePolicyId_resourcePolicies_resourcePolicyId_fk" FOREIGN KEY ("resourcePolicyId") REFERENCES "public"."resourcePolicies"("resourcePolicyId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "siteLabels" ADD CONSTRAINT "siteLabels_siteId_sites_siteId_fk" FOREIGN KEY ("siteId") REFERENCES "public"."sites"("siteId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "siteLabels" ADD CONSTRAINT "siteLabels_labelId_labels_labelId_fk" FOREIGN KEY ("labelId") REFERENCES "public"."labels"("labelId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "siteResourceLabels" ADD CONSTRAINT "siteResourceLabels_siteResourceId_siteResources_siteResourceId_fk" FOREIGN KEY ("siteResourceId") REFERENCES "public"."siteResources"("siteResourceId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "siteResourceLabels" ADD CONSTRAINT "siteResourceLabels_labelId_labels_labelId_fk" FOREIGN KEY ("labelId") REFERENCES "public"."labels"("labelId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "userPolicies" ADD CONSTRAINT "userPolicies_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "userPolicies" ADD CONSTRAINT "userPolicies_resourcePolicyId_resourcePolicies_resourcePolicyId_fk" FOREIGN KEY ("resourcePolicyId") REFERENCES "public"."resourcePolicies"("resourcePolicyId") ON DELETE cascade ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "resources" ADD CONSTRAINT "resources_resourcePolicyId_resourcePolicies_resourcePolicyId_fk" FOREIGN KEY ("resourcePolicyId") REFERENCES "public"."resourcePolicies"("resourcePolicyId") ON DELETE set null ON UPDATE no action;`
);
await db.execute(
sql`ALTER TABLE "resources" ADD CONSTRAINT "resources_defaultResourcePolicyId_resourcePolicies_resourcePolicyId_fk" FOREIGN KEY ("defaultResourcePolicyId") REFERENCES "public"."resourcePolicies"("resourcePolicyId") ON DELETE restrict ON UPDATE no action;`
);
await db.execute(sql`ALTER TABLE "resources" DROP COLUMN "http";`);
await db.execute(sql`ALTER TABLE "resources" DROP COLUMN "protocol";`);
await db.execute(sql`COMMIT`);
console.log("Migrated database");
} catch (e) {
await db.execute(sql`ROLLBACK`);
console.log("Unable to migrate database");
console.log(e);
throw e;
}
try {
const traefikPath = path.join(
APP_PATH,
"traefik",
"traefik_config.yml"
);
const schema = z.object({
experimental: z.object({
plugins: z.object({
badger: z.object({
moduleName: z.string(),
version: z.string()
})
})
})
});
const traefikFileContents = fs.readFileSync(traefikPath, "utf8");
const traefikConfig = yaml.load(traefikFileContents) as any;
const parsedConfig = schema.safeParse(traefikConfig);
if (!parsedConfig.success) {
throw new Error(fromZodError(parsedConfig.error).toString());
}
traefikConfig.experimental.plugins.badger.version = "v1.4.1";
const updatedTraefikYaml = yaml.dump(traefikConfig);
fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8");
console.log(
"Updated the version of Badger in your Traefik configuration to v1.4.1"
);
} catch (e) {
console.log(
"We were unable to update the version of Badger in your Traefik configuration. Please update it manually. Check the release notes for this version for more information."
);
console.error(e);
}
console.log(`${version} migration complete`);
}
+355
View File
@@ -0,0 +1,355 @@
import { APP_PATH } from "@server/lib/consts";
import Database from "better-sqlite3";
import z from "zod";
import { fromZodError } from "zod-validation-error";
import fs from "fs";
import yaml from "js-yaml";
import path from "path";
const version = "1.19.0";
export default async function migration() {
console.log(`Running setup script ${version}...`);
const location = path.join(APP_PATH, "db", "db.sqlite");
const db = new Database(location);
try {
db.transaction(() => {
db.prepare(
`
CREATE TABLE 'browserGatewayTarget' (
'browserGatewayTargetId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'resourceId' integer NOT NULL,
'siteId' integer NOT NULL,
'authToken' text NOT NULL,
'type' text NOT NULL,
'destination' text NOT NULL,
'destinationPort' integer NOT NULL,
FOREIGN KEY ('resourceId') REFERENCES 'resources'('resourceId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('siteId') REFERENCES 'sites'('siteId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'clientLabels' (
'clientLabelId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'clientId' integer NOT NULL,
'labelId' integer NOT NULL,
FOREIGN KEY ('clientId') REFERENCES 'clients'('clientId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('labelId') REFERENCES 'labels'('labelId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE UNIQUE INDEX 'client_label_uniq' ON 'clientLabels' ('clientId','labelId');
`
).run();
db.prepare(
`
CREATE TABLE 'labels' (
'labelId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'name' text NOT NULL,
'color' text NOT NULL,
'orgId' text NOT NULL,
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'resourceLabels' (
'resourceLabelId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'resourceId' integer NOT NULL,
'labelId' integer NOT NULL,
FOREIGN KEY ('resourceId') REFERENCES 'resources'('resourceId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('labelId') REFERENCES 'labels'('labelId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE UNIQUE INDEX 'resource_label_uniq' ON 'resourceLabels' ('resourceId','labelId');
`
).run();
db.prepare(
`
CREATE TABLE 'resourcePolicies' (
'resourcePolicyId' integer PRIMARY KEY NOT NULL,
'sso' integer DEFAULT true NOT NULL,
'applyRules' integer DEFAULT false NOT NULL,
'scope' text DEFAULT 'global' NOT NULL,
'emailWhitelistEnabled' integer DEFAULT false NOT NULL,
'niceId' text NOT NULL,
'idpId' integer,
'name' text NOT NULL,
'orgId' text NOT NULL,
FOREIGN KEY ('idpId') REFERENCES 'idp'('idpId') ON UPDATE no action ON DELETE set null,
FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'resourcePolicyHeaderAuth' (
'headerAuthId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'headerAuthHash' text NOT NULL,
'extendedCompatibility' integer DEFAULT true NOT NULL,
'resourcePolicyId' integer NOT NULL,
FOREIGN KEY ('resourcePolicyId') REFERENCES 'resourcePolicies'('resourcePolicyId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'resourcePolicyPassword' (
'passwordId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'passwordHash' text NOT NULL,
'resourcePolicyId' integer NOT NULL,
FOREIGN KEY ('resourcePolicyId') REFERENCES 'resourcePolicies'('resourcePolicyId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'resourcePolicyPincode' (
'pincodeId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'pincodeHash' text NOT NULL,
'digitLength' integer NOT NULL,
'resourcePolicyId' integer NOT NULL,
FOREIGN KEY ('resourcePolicyId') REFERENCES 'resourcePolicies'('resourcePolicyId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'resourcePolicyRules' (
'ruleId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'resourcePolicyId' integer NOT NULL,
'enabled' integer DEFAULT true NOT NULL,
'priority' integer NOT NULL,
'action' text NOT NULL,
'match' text NOT NULL,
'value' text NOT NULL,
FOREIGN KEY ('resourcePolicyId') REFERENCES 'resourcePolicies'('resourcePolicyId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'resourcePolicyWhitelist' (
'id' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'email' text NOT NULL,
'resourcePolicyId' integer NOT NULL,
FOREIGN KEY ('resourcePolicyId') REFERENCES 'resourcePolicies'('resourcePolicyId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'rolePolicies' (
'roleId' integer NOT NULL,
'resourcePolicyId' integer NOT NULL,
FOREIGN KEY ('roleId') REFERENCES 'roles'('roleId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('resourcePolicyId') REFERENCES 'resourcePolicies'('resourcePolicyId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE TABLE 'siteLabels' (
'siteLabelId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'siteId' integer NOT NULL,
'labelId' integer NOT NULL,
FOREIGN KEY ('siteId') REFERENCES 'sites'('siteId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('labelId') REFERENCES 'labels'('labelId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE UNIQUE INDEX 'site_label_uniq' ON 'siteLabels' ('siteId','labelId');
`
).run();
db.prepare(
`
CREATE TABLE 'siteResourceLabels' (
'siteResourceLabelId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
'siteResourceId' integer NOT NULL,
'labelId' integer NOT NULL,
FOREIGN KEY ('siteResourceId') REFERENCES 'siteResources'('siteResourceId') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('labelId') REFERENCES 'labels'('labelId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
CREATE UNIQUE INDEX 'site_resource_label_uniq' ON 'siteResourceLabels' ('siteResourceId','labelId');
`
).run();
db.prepare(
`
CREATE TABLE 'userPolicies' (
'userId' text NOT NULL,
'resourcePolicyId' integer NOT NULL,
FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade,
FOREIGN KEY ('resourcePolicyId') REFERENCES 'resourcePolicies'('resourcePolicyId') ON UPDATE no action ON DELETE cascade
);
`
).run();
db.prepare(
`
ALTER TABLE 'siteResources' ADD COLUMN 'destination2' text;
`
).run();
db.prepare(
`
UPDATE 'siteResources' SET 'destination2' = 'destination';
`
).run();
db.prepare(
`
ALTER TABLE 'siteResources' DROP COLUMN 'destination';
`
).run();
db.prepare(
`
ALTER TABLE 'siteResources' RENAME COLUMN 'destination2' TO 'destination';
`
).run();
db.prepare(
`
ALTER TABLE 'siteResources' ADD COLUMN 'pamMode' text DEFAULT 'passthrough';
`
).run();
db.prepare(
`
ALTER TABLE 'orgs' ADD 'settingsEnableGlobalNewtAutoUpdate' integer DEFAULT false NOT NULL;
`
).run();
db.prepare(
`
ALTER TABLE 'resourceAccessToken' ADD 'path' text;
`
).run();
db.prepare(
`
ALTER TABLE 'resources' ADD 'resourcePolicyId' integer REFERENCES resourcePolicies(resourcePolicyId);
`
).run();
db.prepare(
`
ALTER TABLE 'resources' ADD 'defaultResourcePolicyId' integer REFERENCES resourcePolicies(resourcePolicyId);
`
).run();
db.prepare(
`
ALTER TABLE 'resources' ADD 'mode' text DEFAULT 'http' NOT NULL;
`
).run();
db.prepare(
`
ALTER TABLE 'resources' ADD 'pamMode' text DEFAULT 'passthrough';
`
).run();
db.prepare(
`
ALTER TABLE 'resources' ADD 'authDaemonMode' text DEFAULT 'site';
`
).run();
db.prepare(
`
ALTER TABLE 'resources' ADD 'authDaemonPort' integer DEFAULT 22123;
`
).run();
db.prepare(
`
ALTER TABLE 'resources' DROP COLUMN 'http';
`
).run();
db.prepare(
`
ALTER TABLE 'resources' DROP COLUMN 'protocol';
`
).run();
db.prepare(
`
ALTER TABLE 'sites' ADD 'autoUpdateEnabled' integer DEFAULT false NOT NULL;
`
).run();
db.prepare(
`
ALTER TABLE 'sites' ADD 'autoUpdateOverrideOrg' integer DEFAULT false NOT NULL;
`
).run();
})();
console.log("Migrated database");
} catch (e) {
console.log("Failed to migrate db:", e);
throw e;
}
try {
const traefikPath = path.join(
APP_PATH,
"traefik",
"traefik_config.yml"
);
const schema = z.object({
experimental: z.object({
plugins: z.object({
badger: z.object({
moduleName: z.string(),
version: z.string()
})
})
})
});
const traefikFileContents = fs.readFileSync(traefikPath, "utf8");
const traefikConfig = yaml.load(traefikFileContents) as any;
const parsedConfig = schema.safeParse(traefikConfig);
if (!parsedConfig.success) {
throw new Error(fromZodError(parsedConfig.error).toString());
}
traefikConfig.experimental.plugins.badger.version = "v1.4.1";
const updatedTraefikYaml = yaml.dump(traefikConfig);
fs.writeFileSync(traefikPath, updatedTraefikYaml, "utf8");
console.log(
"Updated the version of Badger in your Traefik configuration to v1.4.1"
);
} catch (e) {
console.log(
"We were unable to update the version of Badger in your Traefik configuration. Please update it manually. Check the release notes for this version for more information."
);
console.error(e);
}
console.log(`${version} migration complete`);
}
@@ -167,6 +167,10 @@ export default function GeneralPage() {
const [isCheckingCache, setIsCheckingCache] = useState(false);
const [isRebuildingCache, setIsRebuildingCache] = useState(false);
// get "imp" from local storage to determine if we should show the verify button (imp = "1" means show)
const showVerifyButton =
typeof window !== "undefined" && localStorage.getItem("imp") === "1";
const handleRebuildCache = async () => {
if (!client.clientId) return;
setIsRebuildingCache(true);
@@ -904,74 +908,77 @@ export default function GeneralPage() {
</SettingsSection>
)}
{/* Hidden cache verification — subtle button, dev/admin diagnostic */}
<div className="mt-8 flex flex-col gap-2 items-start opacity-30 hover:opacity-100 transition-opacity">
<button
type="button"
onClick={handleVerifyCache}
disabled={isCheckingCache}
className="text-xs text-muted-foreground underline disabled:opacity-50"
title="Verify the client's site association cache against current permissions (read-only)"
>
{isCheckingCache
? "Checking cache…"
: "Verify association cache"}
</button>
{cacheCheck && (
<div
className={
"text-xs rounded border px-2 py-1 " +
(cacheCheck.consistent
? "border-green-600 text-green-700"
: "border-red-600 text-red-700")
}
{showVerifyButton && (
<div className="mt-8 flex flex-col gap-2 items-start opacity-30 hover:opacity-100 transition-opacity">
<button
type="button"
onClick={handleVerifyCache}
disabled={isCheckingCache}
className="text-xs text-muted-foreground underline disabled:opacity-50"
title="Verify the client's site association cache against current permissions (read-only)"
>
{cacheCheck.consistent ? (
<span className="flex items-center gap-1">
<CheckCircle2 className="h-3 w-3" />
Cache is consistent
</span>
) : (
<div className="space-y-2">
<div className="flex items-center gap-1 font-semibold">
<XCircle className="h-3 w-3" />
Cache is INCONSISTENT
{isCheckingCache
? "Checking cache…"
: "Verify association cache"}
</button>
{cacheCheck && (
<div
className={
"text-xs rounded border px-2 py-1 " +
(cacheCheck.consistent
? "border-green-600 text-green-700"
: "border-red-600 text-red-700")
}
>
{cacheCheck.consistent ? (
<span className="flex items-center gap-1">
<CheckCircle2 className="h-3 w-3" />
Cache is consistent
</span>
) : (
<div className="space-y-2">
<div className="flex items-center gap-1 font-semibold">
<XCircle className="h-3 w-3" />
Cache is INCONSISTENT
</div>
<div>
Missing site resources: [
{cacheCheck.missingSiteResourceIds.join(
", "
)}
]
</div>
<div>
Extra site resources: [
{cacheCheck.extraSiteResourceIds.join(
", "
)}
]
</div>
<div>
Missing sites: [
{cacheCheck.missingSiteIds.join(", ")}]
</div>
<div>
Extra sites: [
{cacheCheck.extraSiteIds.join(", ")}]
</div>
<button
type="button"
onClick={handleRebuildCache}
disabled={isRebuildingCache}
className="mt-1 text-xs underline font-semibold disabled:opacity-50"
>
{isRebuildingCache
? "Rebuilding…"
: "Rebuild cache now"}
</button>
</div>
<div>
Missing site resources: [
{cacheCheck.missingSiteResourceIds.join(
", "
)}
]
</div>
<div>
Extra site resources: [
{cacheCheck.extraSiteResourceIds.join(", ")}
]
</div>
<div>
Missing sites: [
{cacheCheck.missingSiteIds.join(", ")}]
</div>
<div>
Extra sites: [
{cacheCheck.extraSiteIds.join(", ")}]
</div>
<button
type="button"
onClick={handleRebuildCache}
disabled={isRebuildingCache}
className="mt-1 text-xs underline font-semibold disabled:opacity-50"
>
{isRebuildingCache
? "Rebuilding…"
: "Rebuild cache now"}
</button>
</div>
)}
</div>
)}
</div>
)}
</div>
)}
</div>
)}
</SettingsContainer>
);
}
+1 -1
View File
@@ -49,7 +49,7 @@ type Resource = {
domain: string;
enabled: boolean;
protected: boolean;
mode: string; // "http", "tcp", "udp", "rdp", "vnc", "ssh"
// mode: string; // "http", "tcp", "udp", "rdp", "vnc", "ssh"
// Auth method fields
sso?: boolean;
password?: boolean;