Compare commits

..

29 Commits

Author SHA1 Message Date
Owen 780d767a65 Merge branch 'main' into dev 2026-08-31 17:07:56 -04:00
Owen Schwartz d6d923e972 Merge pull request #3628 from aithal007/perf/add-fk-indexes
perf: add secondary FK indexes to core OSS schemas
2026-08-31 17:05:04 -04:00
Parikshith 41139f2fd0 perf: add secondary FK indexes to core OSS schemas
Add 13 secondary indexes across 10 tables in the SQLite schema and
5 missing indexes to the PostgreSQL schema.

All list endpoints (listSites, listResources, listClients, listRoles,
listTargets, listUserOrgs) filter and join on these FK columns. In the
SQLite schema, no secondary indexes existed at all on these foreign keys,
forcing full sequential scans on every paginated request and session lookup.

SQLite changes (13 new indexes):
  - sites.orgId
  - resources.orgId
  - targets.resourceId, targets.siteId
  - newt.siteId
  - clients.orgId, clients.userId
  - labels.orgId
  - olms.userId
  - session.userId
  - userOrgs.userId, userOrgs.orgId
  - roles.orgId

PG changes (5 new indexes, rest already present):
  - labels.orgId
  - session.userId
  - userOrgs.userId, userOrgs.orgId
  - roles.orgId
  - olms.userId
2026-08-31 17:04:01 -04:00
Owen Schwartz ebcdeab414 Merge pull request #3597 from shleeable/patch-11
Fix: License.check can fail silently
2026-08-31 17:02:53 -04:00
Owen 2cc7d03ace Update to fall back but still use cache 2026-08-31 17:02:03 -04:00
Owen Schwartz c36cf698c1 Merge pull request #3627 from fosrl/feat/ip-filtering
feat: Add IP column filtering to tables
2026-08-31 16:59:34 -04:00
Shlee 34671c6b13 Update license.ts 2026-08-31 16:57:36 -04:00
Owen Schwartz 8dfc95347f Merge pull request #3655 from moverq1337/fix/access-token-delete-root-key
Fix access token deletion for root API keys
2026-08-31 16:54:18 -04:00
Owen Schwartz 22db0319c2 Merge pull request #3654 from iMord0/crowdsec-v1.7.1
Crowdsec v1.7.1
2026-08-31 16:50:59 -04:00
miloschwartz 39722d30af resolve security-key login only for a unique internal user 2026-08-31 14:39:36 -04:00
miloschwartz 8aef14cf9f harden initial server-admin setup against duplicate users and races 2026-08-31 12:36:42 -04:00
Owen Schwartz 0dece5fef1 Merge pull request #3665 from fosrl/dev
Fix domain namespaces and country is not in blueprints
2026-08-31 11:03:06 -04:00
Owen e7f38c089f Merge branch 'main' into dev 2026-08-31 11:02:01 -04:00
miloschwartz dd0a5a359a check for namespace domain before blocking org check 2026-08-31 10:59:50 -04:00
moverq1337 1650ece0c3 Fix root API key access in verifyApiKeyAccessTokenAccess 2026-08-28 22:45:31 +03:00
iMord0 7f94d99455 Update CrowdSec plugin version
Updated CrowdSec plugin version from v1.4.4 to v1.7.1
2026-08-28 20:51:51 +02:00
iMord0 1f9e99219d Updated CrowdSec plugin version
Updated CrowdSec plugin version from v1.4.4 to v1.7.8
2026-08-28 20:42:26 +02:00
Owen Schwartz bc56a2bed0 Merge pull request #3647 from argueta-xyz/country-is-not-rule-via-blueprint
Allow COUNTRY_IS_NOT rules to be created via Blueprints
2026-08-28 10:52:24 -04:00
Alejandro Argueta 7cda28d685 Update RuleSchema to allow COUNTRY_IS_NOT rules to be created via blueprints as well. 2026-08-27 14:18:48 -07:00
Fred KISSIE d00b9478a2 Merge branch 'dev' into feat/ip-filtering 2026-08-24 22:06:55 +02:00
Fred KISSIE 4ddf36ebcc 💄 some last UI fixes 2026-08-24 20:39:30 +02:00
Fred KISSIE 28b32fe6f7 🏷️ fix types 2026-08-21 23:26:18 +02:00
Fred KISSIE adfb6003d9 Merge branch 'dev' into feat/ip-filtering 2026-08-21 23:24:09 +02:00
Fred KISSIE 6a5ecab013 Implement IP filtering for admin access logs 2026-08-21 22:49:45 +02:00
Fred KISSIE 2c197fab9f IP filtering on request log table finished 2026-08-21 21:04:40 +02:00
Fred KISSIE 65e4fe91b9 🚧 wip: add ip is column filter 2026-08-20 23:59:31 +02:00
Fred KISSIE 52c078a489 💄 ui 2026-08-18 23:28:31 +02:00
Fred KISSIE 195f67c6eb 💄 QoL for location column 2026-08-18 21:08:16 +02:00
Fred KISSIE 668a04bcd2 🚧 wip: IP column filtering 2026-08-14 21:52:16 +02:00
21 changed files with 4214 additions and 2637 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ experimental:
version: "{{.BadgerVersion}}" version: "{{.BadgerVersion}}"
crowdsec: # CrowdSec plugin configuration added crowdsec: # CrowdSec plugin configuration added
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin" moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
version: "v1.4.4" version: "v1.7.1"
log: log:
level: "INFO" level: "INFO"
+3
View File
@@ -1573,6 +1573,8 @@
"search": "Search…", "search": "Search…",
"searchPlaceholder": "Search...", "searchPlaceholder": "Search...",
"emptySearchOptions": "No options found", "emptySearchOptions": "No options found",
"ipFilterSearchPlaceholder": "Enter an IP address…",
"ipFilterEmptyMessage": "Enter an IP address to filter by",
"create": "Create", "create": "Create",
"orgs": "Organizations", "orgs": "Organizations",
"loginError": "An unexpected error occurred. Please try again.", "loginError": "An unexpected error occurred. Please try again.",
@@ -2596,6 +2598,7 @@
"createDomainType": "Type:", "createDomainType": "Type:",
"createDomainName": "Name:", "createDomainName": "Name:",
"createDomainValue": "Value:", "createDomainValue": "Value:",
"multiSelectFilterCount": "{count} selected",
"createDomainCnameRecords": "CNAME Records", "createDomainCnameRecords": "CNAME Records",
"createDomainARecords": "A Records", "createDomainARecords": "A Records",
"createDomainRecordNumber": "Record {number}", "createDomainRecordNumber": "Record {number}",
+3447 -2113
View File
File diff suppressed because it is too large Load Diff
+75 -75
View File
@@ -32,49 +32,49 @@
"format": "prettier --write ." "format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "9.1.0", "@asteasolutions/zod-to-openapi": "8.5.0",
"@aws-sdk/client-s3": "3.1120.0", "@aws-sdk/client-s3": "3.1056.0",
"@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz", "@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz",
"@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.1.tgz", "@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.1.tgz",
"@headlessui/react": "2.2.10", "@headlessui/react": "2.2.10",
"@hookform/resolvers": "5.9.1", "@hookform/resolvers": "5.4.0",
"@monaco-editor/react": "4.7.0", "@monaco-editor/react": "4.7.0",
"@node-rs/argon2": "2.1.0", "@node-rs/argon2": "2.0.2",
"@novnc/novnc": "^1.7.0", "@novnc/novnc": "^1.7.0",
"@oslojs/crypto": "1.0.1", "@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0", "@oslojs/encoding": "1.1.0",
"@radix-ui/react-avatar": "1.2.6", "@radix-ui/react-avatar": "1.1.11",
"@radix-ui/react-checkbox": "1.3.11", "@radix-ui/react-checkbox": "1.3.3",
"@radix-ui/react-collapsible": "1.1.20", "@radix-ui/react-collapsible": "1.1.12",
"@radix-ui/react-dialog": "1.1.23", "@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-dropdown-menu": "2.1.24", "@radix-ui/react-dropdown-menu": "2.1.16",
"@radix-ui/react-icons": "1.3.2", "@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-label": "2.1.15", "@radix-ui/react-label": "2.1.8",
"@radix-ui/react-popover": "1.1.23", "@radix-ui/react-popover": "1.1.15",
"@radix-ui/react-progress": "1.1.16", "@radix-ui/react-progress": "1.1.8",
"@radix-ui/react-radio-group": "1.4.7", "@radix-ui/react-radio-group": "1.3.8",
"@radix-ui/react-scroll-area": "1.2.18", "@radix-ui/react-scroll-area": "1.2.10",
"@radix-ui/react-select": "2.3.7", "@radix-ui/react-select": "2.2.6",
"@radix-ui/react-separator": "1.1.15", "@radix-ui/react-separator": "1.1.8",
"@radix-ui/react-slot": "1.3.3", "@radix-ui/react-slot": "1.2.4",
"@radix-ui/react-switch": "1.3.7", "@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-tabs": "1.1.21", "@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.23", "@radix-ui/react-toast": "1.2.15",
"@radix-ui/react-tooltip": "1.2.16", "@radix-ui/react-tooltip": "1.2.8",
"@react-email/body": "0.3.0", "@react-email/body": "0.3.0",
"@react-email/components": "1.0.12", "@react-email/components": "1.0.12",
"@react-email/render": "2.1.0", "@react-email/render": "2.0.8",
"@react-email/tailwind": "2.0.7", "@react-email/tailwind": "2.0.7",
"@simplewebauthn/browser": "13.3.0", "@simplewebauthn/browser": "13.3.0",
"@simplewebauthn/server": "13.3.3", "@simplewebauthn/server": "13.3.1",
"@tailwindcss/forms": "0.5.11", "@tailwindcss/forms": "0.5.11",
"@tanstack/react-query": "5.102.8", "@tanstack/react-query": "5.100.14",
"@tanstack/react-table": "9.2.3", "@tanstack/react-table": "8.21.3",
"@xterm/addon-fit": "^0.11.0", "@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0", "@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^6.0.0",
"arctic": "3.7.0", "arctic": "3.7.0",
"axios": "1.20.0", "axios": "1.18.0",
"better-sqlite3": "11.9.1", "better-sqlite3": "11.9.1",
"canvas-confetti": "1.9.4", "canvas-confetti": "1.9.4",
"class-variance-authority": "0.7.1", "class-variance-authority": "0.7.1",
@@ -86,62 +86,62 @@
"d3": "7.9.0", "d3": "7.9.0",
"drizzle-orm": "0.45.2", "drizzle-orm": "0.45.2",
"express": "5.2.1", "express": "5.2.1",
"express-rate-limit": "8.6.2", "express-rate-limit": "8.5.2",
"glob": "13.0.6", "glob": "13.0.6",
"gpt-tokenizer": "^4.0.0", "gpt-tokenizer": "^3.4.0",
"helmet": "8.3.0", "helmet": "8.2.0",
"http-errors": "2.0.1", "http-errors": "2.0.1",
"input-otp": "1.5.0", "input-otp": "1.4.2",
"ioredis": "6.0.0", "ioredis": "5.11.0",
"jmespath": "0.16.0", "jmespath": "0.16.0",
"js-yaml": "5.4.1", "js-yaml": "4.3.1",
"jsonwebtoken": "9.0.3", "jsonwebtoken": "9.0.3",
"lucide-react": "1.34.0", "lucide-react": "1.17.0",
"maxmind": "5.0.7", "maxmind": "5.0.6",
"moment": "2.30.1", "moment": "2.30.1",
"next": "16.3.3", "next": "16.3.1",
"next-intl": "4.14.0", "next-intl": "4.13.0",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"nextjs-toploader": "3.9.17", "nextjs-toploader": "3.9.17",
"node-cache": "5.1.2", "node-cache": "5.1.2",
"nodemailer": "9.0.6", "nodemailer": "9.0.1",
"oslo": "1.2.1", "oslo": "1.2.1",
"pg": "8.23.0", "pg": "8.21.0",
"posthog-node": "5.51.4", "posthog-node": "5.35.6",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.2.8", "react": "19.2.6",
"react-day-picker": "10.0.1", "react-day-picker": "9.14.0",
"react-dom": "19.2.8", "react-dom": "19.2.6",
"react-easy-sort": "1.8.0", "react-easy-sort": "1.8.0",
"react-hook-form": "7.86.0", "react-hook-form": "7.76.1",
"react-icons": "5.7.0", "react-icons": "5.6.0",
"recharts": "3.10.1", "recharts": "3.8.1",
"reodotdev": "1.1.0", "reodotdev": "1.1.0",
"semver": "7.8.5", "semver": "7.8.1",
"sshpk": "1.18.0", "sshpk": "1.18.0",
"stripe": "22.6.0", "stripe": "22.2.0",
"swagger-ui-express": "5.0.1", "swagger-ui-express": "5.0.1",
"tailwind-merge": "3.6.0", "tailwind-merge": "3.6.0",
"topojson-client": "3.1.0", "topojson-client": "3.1.0",
"tw-animate-css": "1.4.0", "tw-animate-css": "1.4.0",
"use-debounce": "10.1.1", "use-debounce": "10.1.1",
"uuid": "14.0.2", "uuid": "14.0.0",
"vaul": "1.1.2", "vaul": "1.1.2",
"visionscarto-world-atlas": "1.0.0", "visionscarto-world-atlas": "1.0.0",
"winston": "3.19.0", "winston": "3.19.0",
"winston-daily-rotate-file": "5.0.0", "winston-daily-rotate-file": "5.0.0",
"ws": "8.21.3", "ws": "8.21.0",
"yaml": "2.9.0", "yaml": "2.9.0",
"yargs": "18.1.0", "yargs": "18.0.0",
"zod": "4.4.3", "zod": "4.4.3",
"zod-validation-error": "5.0.0" "zod-validation-error": "5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "2.22.0", "@dotenvx/dotenvx": "1.69.1",
"@esbuild-plugins/tsconfig-paths": "0.1.2", "@esbuild-plugins/tsconfig-paths": "0.1.2",
"@react-email/ui": "^6.9.3", "@react-email/ui": "^6.9.2",
"@tailwindcss/postcss": "4.3.3", "@tailwindcss/postcss": "4.3.0",
"@tanstack/react-query-devtools": "5.102.8", "@tanstack/react-query-devtools": "5.100.14",
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"@types/cookie-parser": "1.4.10", "@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19", "@types/cors": "2.8.19",
@@ -152,36 +152,36 @@
"@types/jmespath": "0.15.2", "@types/jmespath": "0.15.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "9.0.10", "@types/jsonwebtoken": "9.0.10",
"@types/node": "26.4.0", "@types/node": "25.9.1",
"@types/nodemailer": "8.0.1", "@types/nodemailer": "8.0.0",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@types/pg": "8.23.1", "@types/pg": "8.20.0",
"@types/react": "19.2.18", "@types/react": "19.2.15",
"@types/react-dom": "19.2.5", "@types/react-dom": "19.2.3",
"@types/semver": "7.8.0", "@types/semver": "7.7.1",
"@types/sshpk": "1.17.5", "@types/sshpk": "1.17.4",
"@types/swagger-ui-express": "4.1.8", "@types/swagger-ui-express": "4.1.8",
"@types/topojson-client": "3.1.5", "@types/topojson-client": "3.1.5",
"@types/ws": "8.18.1", "@types/ws": "8.18.1",
"@types/yargs": "17.0.35", "@types/yargs": "17.0.35",
"babel-plugin-react-compiler": "1.0.0", "babel-plugin-react-compiler": "1.0.0",
"drizzle-kit": "0.31.10", "drizzle-kit": "0.31.10",
"esbuild": "0.28.2", "esbuild": "0.28.0",
"esbuild-node-externals": "2.0.0", "esbuild-node-externals": "1.22.0",
"eslint": "10.9.1", "eslint": "10.4.0",
"eslint-config-next": "16.3.3", "eslint-config-next": "16.2.6",
"postcss": "8.5.26", "postcss": "8.5.23",
"prettier": "3.9.6", "prettier": "3.8.3",
"react-email": "6.9.3", "react-email": "6.5.0",
"tailwindcss": "4.3.3", "tailwindcss": "4.3.0",
"tsc-alias": "1.9.2", "tsc-alias": "1.8.17",
"tsx": "4.23.12", "tsx": "4.22.3",
"typescript": "7.0.2", "typescript": "6.0.3",
"typescript-eslint": "8.68.0" "typescript-eslint": "8.60.0"
}, },
"overrides": { "overrides": {
"esbuild": "0.28.2", "esbuild": "0.28.0",
"dompurify": "3.4.0", "dompurify": "3.4.0",
"postcss": "8.5.26" "postcss": "8.5.23"
} }
} }
+71 -49
View File
@@ -262,16 +262,20 @@ export const resourceAiModels = pgTable(
(t) => [primaryKey({ columns: [t.resourceId, t.modelId] })] (t) => [primaryKey({ columns: [t.resourceId, t.modelId] })]
); );
export const labels = pgTable("labels", { export const labels = pgTable(
labelId: serial("labelId").primaryKey(), "labels",
name: varchar("name").notNull(), {
color: varchar("color").notNull(), labelId: serial("labelId").primaryKey(),
orgId: varchar("orgId") name: varchar("name").notNull(),
.references(() => orgs.orgId, { color: varchar("color").notNull(),
onDelete: "cascade" orgId: varchar("orgId")
}) .references(() => orgs.orgId, {
.notNull() onDelete: "cascade"
}); })
.notNull()
},
(t) => [index("idx_labels_orgid").on(t.orgId)]
);
export const launcherViews = pgTable("launcherViews", { export const launcherViews = pgTable("launcherViews", {
viewId: serial("viewId").primaryKey(), viewId: serial("viewId").primaryKey(),
@@ -693,15 +697,19 @@ export const twoFactorBackupCodes = pgTable("twoFactorBackupCodes", {
codeHash: varchar("codeHash").notNull() codeHash: varchar("codeHash").notNull()
}); });
export const sessions = pgTable("session", { export const sessions = pgTable(
sessionId: varchar("id").primaryKey(), "session",
userId: varchar("userId") {
.notNull() sessionId: varchar("id").primaryKey(),
.references(() => users.userId, { onDelete: "cascade" }), userId: varchar("userId")
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(), .notNull()
issuedAt: bigint("issuedAt", { mode: "number" }), .references(() => users.userId, { onDelete: "cascade" }),
deviceAuthUsed: boolean("deviceAuthUsed").notNull().default(false) expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
}); issuedAt: bigint("issuedAt", { mode: "number" }),
deviceAuthUsed: boolean("deviceAuthUsed").notNull().default(false)
},
(t) => [index("idx_sessions_userid").on(t.userId)]
);
export const newtSessions = pgTable("newtSession", { export const newtSessions = pgTable("newtSession", {
sessionId: varchar("id").primaryKey(), sessionId: varchar("id").primaryKey(),
@@ -711,19 +719,26 @@ export const newtSessions = pgTable("newtSession", {
expiresAt: bigint("expiresAt", { mode: "number" }).notNull() expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
}); });
export const userOrgs = pgTable("userOrgs", { export const userOrgs = pgTable(
userId: varchar("userId") "userOrgs",
.notNull() {
.references(() => users.userId, { onDelete: "cascade" }), userId: varchar("userId")
orgId: varchar("orgId") .notNull()
.references(() => orgs.orgId, { .references(() => users.userId, { onDelete: "cascade" }),
onDelete: "cascade" orgId: varchar("orgId")
}) .references(() => orgs.orgId, {
.notNull(), onDelete: "cascade"
isOwner: boolean("isOwner").notNull().default(false), })
autoProvisioned: boolean("autoProvisioned").default(false), .notNull(),
pamUsername: varchar("pamUsername") // cleaned username for ssh and such isOwner: boolean("isOwner").notNull().default(false),
}); autoProvisioned: boolean("autoProvisioned").default(false),
pamUsername: varchar("pamUsername") // cleaned username for ssh and such
},
(t) => [
index("idx_userOrgs_userid").on(t.userId),
index("idx_userOrgs_orgid").on(t.orgId)
]
);
export const emailVerificationCodes = pgTable("emailVerificationCodes", { export const emailVerificationCodes = pgTable("emailVerificationCodes", {
codeId: serial("id").primaryKey(), codeId: serial("id").primaryKey(),
@@ -751,22 +766,26 @@ export const actions = pgTable("actions", {
description: varchar("description") description: varchar("description")
}); });
export const roles = pgTable("roles", { export const roles = pgTable(
roleId: serial("roleId").primaryKey(), "roles",
orgId: varchar("orgId") {
.references(() => orgs.orgId, { roleId: serial("roleId").primaryKey(),
onDelete: "cascade" orgId: varchar("orgId")
}) .references(() => orgs.orgId, {
.notNull(), onDelete: "cascade"
isAdmin: boolean("isAdmin"), })
name: varchar("name").notNull(), .notNull(),
description: varchar("description"), isAdmin: boolean("isAdmin"),
requireDeviceApproval: boolean("requireDeviceApproval").default(false), name: varchar("name").notNull(),
sshSudoMode: varchar("sshSudoMode", { length: 32 }).default("full"), // "none" | "full" | "commands" description: varchar("description"),
sshSudoCommands: text("sshSudoCommands").default("[]"), requireDeviceApproval: boolean("requireDeviceApproval").default(false),
sshCreateHomeDir: boolean("sshCreateHomeDir").default(true), sshSudoMode: varchar("sshSudoMode", { length: 32 }).default("full"), // "none" | "full" | "commands"
sshUnixGroups: text("sshUnixGroups").default("[]") sshSudoCommands: text("sshSudoCommands").default("[]"),
}); sshCreateHomeDir: boolean("sshCreateHomeDir").default(true),
sshUnixGroups: text("sshUnixGroups").default("[]")
},
(t) => [index("idx_roles_orgid").on(t.orgId)]
);
export const userOrgRoles = pgTable( export const userOrgRoles = pgTable(
"userOrgRoles", "userOrgRoles",
@@ -1409,7 +1428,10 @@ export const olms = pgTable(
}), }),
archived: boolean("archived").notNull().default(false) archived: boolean("archived").notNull().default(false)
}, },
(t) => [index("idx_olms_clientid").on(t.clientId)] (t) => [
index("idx_olms_clientid").on(t.clientId),
index("idx_olms_userid").on(t.userId)
]
); );
export const currentFingerprint = pgTable("currentFingerprint", { export const currentFingerprint = pgTable("currentFingerprint", {
+336 -267
View File
@@ -99,131 +99,147 @@ export const orgDomains = sqliteTable("orgDomains", {
.references(() => domains.domainId, { onDelete: "cascade" }) .references(() => domains.domainId, { onDelete: "cascade" })
}); });
export const sites = sqliteTable("sites", { export const sites = sqliteTable(
siteId: integer("siteId").primaryKey({ autoIncrement: true }), "sites",
orgId: text("orgId") {
.references(() => orgs.orgId, { siteId: integer("siteId").primaryKey({ autoIncrement: true }),
onDelete: "cascade" orgId: text("orgId")
}) .references(() => orgs.orgId, {
.notNull(), onDelete: "cascade"
niceId: text("niceId").notNull(), })
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, { .notNull(),
onDelete: "set null" niceId: text("niceId").notNull(),
}), exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
networkId: integer("networkId").references(() => networks.networkId, { onDelete: "set null"
onDelete: "set null" }),
}), networkId: integer("networkId").references(() => networks.networkId, {
name: text("name").notNull(), onDelete: "set null"
pubKey: text("pubKey"), }),
exitNodeSubnet: text("exitNodeSubnet"), name: text("name").notNull(),
megabytesIn: integer("bytesIn").default(0), pubKey: text("pubKey"),
megabytesOut: integer("bytesOut").default(0), exitNodeSubnet: text("exitNodeSubnet"),
lastBandwidthUpdate: text("lastBandwidthUpdate"), megabytesIn: integer("bytesIn").default(0),
type: text("type").notNull(), // "newt" or "wireguard" megabytesOut: integer("bytesOut").default(0),
online: integer("online", { mode: "boolean" }).notNull().default(false), lastBandwidthUpdate: text("lastBandwidthUpdate"),
lastPing: integer("lastPing"), type: text("type").notNull(), // "newt" or "wireguard"
online: integer("online", { mode: "boolean" }).notNull().default(false),
lastPing: integer("lastPing"),
// 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
endpoint: text("endpoint"), // this is how to reach gerbil externally - gets put into the wireguard config endpoint: text("endpoint"), // this is how to reach gerbil externally - gets put into the wireguard config
localEndpoints: text("localEndpoints"), // JSON encoded list of string ips on the local machine to try to connect to localEndpoints: text("localEndpoints"), // JSON encoded list of string ips on the local machine to try to connect to
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),
autoUpdateEnabled: integer("autoUpdateEnabled", { mode: "boolean" }) autoUpdateEnabled: integer("autoUpdateEnabled", { mode: "boolean" })
.notNull() .notNull()
.default(false), .default(false),
autoUpdateOverrideOrg: integer("autoUpdateOverrideOrg", { autoUpdateOverrideOrg: integer("autoUpdateOverrideOrg", {
mode: "boolean" mode: "boolean"
})
.notNull()
.default(false),
status: text("status").$type<"pending" | "approved">().default("approved")
});
export const resources = sqliteTable("resources", {
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
resourcePolicyId: integer("resourcePolicyId").references(
() => resourcePolicies.resourcePolicyId,
{ onDelete: "set null" }
),
defaultResourcePolicyId: integer("defaultResourcePolicyId").references(
() => resourcePolicies.resourcePolicyId,
{
onDelete: "restrict"
}
),
resourceGuid: text("resourceGuid", { length: 36 })
.unique()
.notNull()
.$defaultFn(() => randomUUID()),
orgId: text("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
}) })
.notNull(), .notNull()
niceId: text("niceId").notNull(), .default(false),
name: text("name").notNull(), status: text("status")
subdomain: text("subdomain"), .$type<"pending" | "approved">()
fullDomain: text("fullDomain"), .default("approved")
domainId: text("domainId").references(() => domains.domainId, { },
onDelete: "set null" (table) => [
}), index("idx_sites_orgId").on(table.orgId)
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false), ]
blockAccess: integer("blockAccess", { mode: "boolean" }) );
.notNull()
.default(false), export const resources = sqliteTable(
proxyPort: integer("proxyPort"), "resources",
sso: integer("sso", { mode: "boolean" }), {
emailWhitelistEnabled: integer("emailWhitelistEnabled", { resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
mode: "boolean" resourcePolicyId: integer("resourcePolicyId").references(
}), () => resourcePolicies.resourcePolicyId,
applyRules: integer("applyRules", { mode: "boolean" }), { onDelete: "set null" }
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), ),
stickySession: integer("stickySession", { mode: "boolean" }) defaultResourcePolicyId: integer("defaultResourcePolicyId").references(
.notNull() () => resourcePolicies.resourcePolicyId,
.default(false), {
tlsServerName: text("tlsServerName"), onDelete: "restrict"
setHostHeader: text("setHostHeader"), }
enableProxy: integer("enableProxy", { mode: "boolean" }).default(true), ),
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { resourceGuid: text("resourceGuid", { length: 36 })
onDelete: "set null" .unique()
}), .notNull()
headers: text("headers"), // comma-separated list of headers to add to the request .$defaultFn(() => randomUUID()),
proxyProtocol: integer("proxyProtocol", { mode: "boolean" }) orgId: text("orgId")
.notNull() .references(() => orgs.orgId, {
.default(false), onDelete: "cascade"
proxyProtocolVersion: integer("proxyProtocolVersion").default(1), })
maintenanceModeEnabled: integer("maintenanceModeEnabled", { .notNull(),
mode: "boolean" niceId: text("niceId").notNull(),
}) name: text("name").notNull(),
.notNull() subdomain: text("subdomain"),
.default(false), fullDomain: text("fullDomain"),
maintenanceModeType: text("maintenanceModeType", { domainId: text("domainId").references(() => domains.domainId, {
enum: ["forced", "automatic"] onDelete: "set null"
}).default("forced"), // "forced" = always show, "automatic" = only when down }),
maintenanceTitle: text("maintenanceTitle"), ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
maintenanceMessage: text("maintenanceMessage"), blockAccess: integer("blockAccess", { mode: "boolean" })
maintenanceEstimatedTime: text("maintenanceEstimatedTime"), .notNull()
postAuthPath: text("postAuthPath"), .default(false),
health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown" proxyPort: integer("proxyPort"),
wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false), sso: integer("sso", { mode: "boolean" }),
mode: text("mode") emailWhitelistEnabled: integer("emailWhitelistEnabled", {
.default("http") mode: "boolean"
.$type<"rdp" | "ssh" | "http" | "vnc" | "inference" | "tcp" | "udp">() }),
.notNull(), // rdp, ssh, http, vnc, inference applyRules: integer("applyRules", { mode: "boolean" }),
pamMode: text("pamMode") enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
.$type<"passthrough" | "push">() stickySession: integer("stickySession", { mode: "boolean" })
.default("passthrough"), .notNull()
authDaemonMode: text("authDaemonMode") .default(false),
.$type<"site" | "remote" | "native">() tlsServerName: text("tlsServerName"),
.default("site"), setHostHeader: text("setHostHeader"),
authDaemonPort: integer("authDaemonPort").default(22123), enableProxy: integer("enableProxy", { mode: "boolean" }).default(true),
status: text("status").$type<"pending" | "approved">().default("approved") skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
}); onDelete: "set null"
}),
headers: text("headers"), // comma-separated list of headers to add to the request
proxyProtocol: integer("proxyProtocol", { mode: "boolean" })
.notNull()
.default(false),
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
maintenanceModeEnabled: integer("maintenanceModeEnabled", {
mode: "boolean"
})
.notNull()
.default(false),
maintenanceModeType: text("maintenanceModeType", {
enum: ["forced", "automatic"]
}).default("forced"), // "forced" = always show, "automatic" = only when down
maintenanceTitle: text("maintenanceTitle"),
maintenanceMessage: text("maintenanceMessage"),
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
postAuthPath: text("postAuthPath"),
health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown"
wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false),
mode: text("mode")
.default("http")
.$type<"rdp" | "ssh" | "http" | "vnc" | "inference" | "tcp" | "udp">()
.notNull(), // rdp, ssh, http, vnc, inference
pamMode: text("pamMode")
.$type<"passthrough" | "push">()
.default("passthrough"),
authDaemonMode: text("authDaemonMode")
.$type<"site" | "remote" | "native">()
.default("site"),
authDaemonPort: integer("authDaemonPort").default(22123),
status: text("status")
.$type<"pending" | "approved">()
.default("approved")
},
(table) => [
index("idx_resources_orgId").on(table.orgId)
]
);
export const resourceAiProviders = sqliteTable( export const resourceAiProviders = sqliteTable(
"resourceAiProviders", "resourceAiProviders",
@@ -260,16 +276,22 @@ export const resourceAiModels = sqliteTable(
(t) => [primaryKey({ columns: [t.resourceId, t.modelId] })] (t) => [primaryKey({ columns: [t.resourceId, t.modelId] })]
); );
export const labels = sqliteTable("labels", { export const labels = sqliteTable(
labelId: integer("labelId").primaryKey({ autoIncrement: true }), "labels",
name: text("name").notNull(), {
color: text("color").notNull(), labelId: integer("labelId").primaryKey({ autoIncrement: true }),
orgId: text("orgId") name: text("name").notNull(),
.references(() => orgs.orgId, { color: text("color").notNull(),
onDelete: "cascade" orgId: text("orgId")
}) .references(() => orgs.orgId, {
.notNull() onDelete: "cascade"
}); })
.notNull()
},
(table) => [
index("idx_labels_orgId").on(table.orgId)
]
);
export const launcherViews = sqliteTable("launcherViews", { export const launcherViews = sqliteTable("launcherViews", {
viewId: integer("viewId").primaryKey({ autoIncrement: true }), viewId: integer("viewId").primaryKey({ autoIncrement: true }),
@@ -366,35 +388,44 @@ export const clientLabels = sqliteTable(
(t) => [unique("client_label_uniq").on(t.clientId, t.labelId)] (t) => [unique("client_label_uniq").on(t.clientId, t.labelId)]
); );
export const targets = sqliteTable("targets", { export const targets = sqliteTable(
targetId: integer("targetId").primaryKey({ autoIncrement: true }), "targets",
resourceId: integer("resourceId").references(() => resources.resourceId, { {
onDelete: "cascade" targetId: integer("targetId").primaryKey({ autoIncrement: true }),
}), resourceId: integer("resourceId").references(
providerId: integer("providerId").references(() => aiProviders.providerId, { () => resources.resourceId,
onDelete: "cascade" { onDelete: "cascade" }
}), ),
siteId: integer("siteId") providerId: integer("providerId").references(
.references(() => sites.siteId, { () => aiProviders.providerId,
onDelete: "cascade" { onDelete: "cascade" }
}) ),
.notNull(), siteId: integer("siteId")
ip: text("ip").notNull(), .references(() => sites.siteId, {
method: text("method"), onDelete: "cascade"
port: integer("port").notNull(), })
internalPort: integer("internalPort"), .notNull(),
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), ip: text("ip").notNull(),
path: text("path"), method: text("method"),
pathMatchType: text("pathMatchType"), // exact, prefix, regex port: integer("port").notNull(),
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target internalPort: integer("internalPort"),
rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
priority: integer("priority").notNull().default(100), path: text("path"),
mode: text("mode") pathMatchType: text("pathMatchType"), // exact, prefix, regex
.$type<"http" | "tcp" | "udp" | "ssh" | "rdp" | "vnc">() rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
.notNull() rewritePathType: text("rewritePathType"), // exact, prefix, regex, stripPrefix
.default("http"), priority: integer("priority").notNull().default(100),
authToken: text("authToken") mode: text("mode")
}); .$type<"http" | "tcp" | "udp" | "ssh" | "rdp" | "vnc">()
.notNull()
.default("http"),
authToken: text("authToken")
},
(table) => [
index("idx_targets_resourceId").on(table.resourceId),
index("idx_targets_siteId").on(table.siteId)
]
);
export const targetHealthCheck = sqliteTable("targetHealthCheck", { export const targetHealthCheck = sqliteTable("targetHealthCheck", {
targetHealthCheckId: integer("targetHealthCheckId").primaryKey({ targetHealthCheckId: integer("targetHealthCheckId").primaryKey({
@@ -663,50 +694,63 @@ export const setupTokens = sqliteTable("setupTokens", {
dateUsed: text("dateUsed") dateUsed: text("dateUsed")
}); });
export const newts = sqliteTable("newt", { export const newts = sqliteTable(
newtId: text("id").primaryKey(), "newt",
secretHash: text("secretHash").notNull(), {
dateCreated: text("dateCreated").notNull(), newtId: text("id").primaryKey(),
version: text("version"), secretHash: text("secretHash").notNull(),
siteId: integer("siteId").references(() => sites.siteId, { dateCreated: text("dateCreated").notNull(),
onDelete: "cascade" version: text("version"),
}) siteId: integer("siteId").references(() => sites.siteId, {
});
export const clients = sqliteTable("clients", {
clientId: integer("clientId").primaryKey({ autoIncrement: true }),
orgId: text("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade" onDelete: "cascade"
}) })
.notNull(), },
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, { (table) => [
onDelete: "set null" index("idx_newts_siteId").on(table.siteId)
}), ]
userId: text("userId").references(() => users.userId, { );
// optionally tied to a user and in this case delete when the user deletes
onDelete: "cascade" export const clients = sqliteTable(
}), "clients",
niceId: text("niceId").notNull(), {
name: text("name").notNull(), clientId: integer("clientId").primaryKey({ autoIncrement: true }),
pubKey: text("pubKey"), orgId: text("orgId")
olmId: text("olmId"), // to lock it to a specific olm optionally .references(() => orgs.orgId, {
subnet: text("subnet").notNull(), onDelete: "cascade"
exitNodeSubnet: text("exitNodeSubnet"), // this is the subnet when connecting to an exit node })
megabytesIn: integer("bytesIn"), .notNull(),
megabytesOut: integer("bytesOut"), exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
lastBandwidthUpdate: text("lastBandwidthUpdate"), onDelete: "set null"
lastPing: integer("lastPing"), }),
type: text("type").notNull(), // "olm" userId: text("userId").references(() => users.userId, {
online: integer("online", { mode: "boolean" }).notNull().default(false), // optionally tied to a user and in this case delete when the user deletes
// endpoint: text("endpoint"), onDelete: "cascade"
lastHolePunch: integer("lastHolePunch"), }),
archived: integer("archived", { mode: "boolean" }).notNull().default(false), niceId: text("niceId").notNull(),
blocked: integer("blocked", { mode: "boolean" }).notNull().default(false), name: text("name").notNull(),
approvalState: text("approvalState").$type< pubKey: text("pubKey"),
"pending" | "approved" | "denied" olmId: text("olmId"), // to lock it to a specific olm optionally
>() subnet: text("subnet").notNull(),
}); exitNodeSubnet: text("exitNodeSubnet"), // this is the subnet when connecting to an exit node
megabytesIn: integer("bytesIn"),
megabytesOut: integer("bytesOut"),
lastBandwidthUpdate: text("lastBandwidthUpdate"),
lastPing: integer("lastPing"),
type: text("type").notNull(), // "olm"
online: integer("online", { mode: "boolean" }).notNull().default(false),
// endpoint: text("endpoint"),
lastHolePunch: integer("lastHolePunch"),
archived: integer("archived", { mode: "boolean" }).notNull().default(false),
blocked: integer("blocked", { mode: "boolean" }).notNull().default(false),
approvalState: text("approvalState").$type<
"pending" | "approved" | "denied"
>()
},
(table) => [
index("idx_clients_orgId").on(table.orgId),
index("idx_clients_userId").on(table.userId)
]
);
export const clientSitesAssociationsCache = sqliteTable( export const clientSitesAssociationsCache = sqliteTable(
"clientSitesAssociationsCache", "clientSitesAssociationsCache",
@@ -734,23 +778,29 @@ export const clientSiteResourcesAssociationsCache = sqliteTable(
} }
); );
export const olms = sqliteTable("olms", { export const olms = sqliteTable(
olmId: text("id").primaryKey(), "olms",
secretHash: text("secretHash").notNull(), {
dateCreated: text("dateCreated").notNull(), olmId: text("id").primaryKey(),
version: text("version"), secretHash: text("secretHash").notNull(),
agent: text("agent"), dateCreated: text("dateCreated").notNull(),
name: text("name"), version: text("version"),
clientId: integer("clientId").references(() => clients.clientId, { agent: text("agent"),
// we will switch this depending on the current org it wants to connect to name: text("name"),
onDelete: "set null" clientId: integer("clientId").references(() => clients.clientId, {
}), // we will switch this depending on the current org it wants to connect to
userId: text("userId").references(() => users.userId, { onDelete: "set null"
// optionally tied to a user and in this case delete when the user deletes }),
onDelete: "cascade" userId: text("userId").references(() => users.userId, {
}), // optionally tied to a user and in this case delete when the user deletes
archived: integer("archived", { mode: "boolean" }).notNull().default(false) onDelete: "cascade"
}); }),
archived: integer("archived", { mode: "boolean" }).notNull().default(false)
},
(table) => [
index("idx_olms_userId").on(table.userId)
]
);
export const currentFingerprint = sqliteTable("currentFingerprint", { export const currentFingerprint = sqliteTable("currentFingerprint", {
fingerprintId: integer("id").primaryKey({ autoIncrement: true }), fingerprintId: integer("id").primaryKey({ autoIncrement: true }),
@@ -912,17 +962,23 @@ export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
codeHash: text("codeHash").notNull() codeHash: text("codeHash").notNull()
}); });
export const sessions = sqliteTable("session", { export const sessions = sqliteTable(
sessionId: text("id").primaryKey(), "session",
userId: text("userId") {
.notNull() sessionId: text("id").primaryKey(),
.references(() => users.userId, { onDelete: "cascade" }), userId: text("userId")
expiresAt: integer("expiresAt").notNull(), .notNull()
issuedAt: integer("issuedAt"), .references(() => users.userId, { onDelete: "cascade" }),
deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" }) expiresAt: integer("expiresAt").notNull(),
.notNull() issuedAt: integer("issuedAt"),
.default(false) deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" })
}); .notNull()
.default(false)
},
(table) => [
index("idx_sessions_userId").on(table.userId)
]
);
export const newtSessions = sqliteTable("newtSession", { export const newtSessions = sqliteTable("newtSession", {
sessionId: text("id").primaryKey(), sessionId: text("id").primaryKey(),
@@ -940,21 +996,28 @@ export const olmSessions = sqliteTable("clientSession", {
expiresAt: integer("expiresAt").notNull() expiresAt: integer("expiresAt").notNull()
}); });
export const userOrgs = sqliteTable("userOrgs", { export const userOrgs = sqliteTable(
userId: text("userId") "userOrgs",
.notNull() {
.references(() => users.userId, { onDelete: "cascade" }), userId: text("userId")
orgId: text("orgId") .notNull()
.references(() => orgs.orgId, { .references(() => users.userId, { onDelete: "cascade" }),
onDelete: "cascade" orgId: text("orgId")
}) .references(() => orgs.orgId, {
.notNull(), onDelete: "cascade"
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false), })
autoProvisioned: integer("autoProvisioned", { .notNull(),
mode: "boolean" isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false),
}).default(false), autoProvisioned: integer("autoProvisioned", {
pamUsername: text("pamUsername") // cleaned username for ssh and such mode: "boolean"
}); }).default(false),
pamUsername: text("pamUsername") // cleaned username for ssh and such
},
(table) => [
index("idx_userOrgs_userId").on(table.userId),
index("idx_userOrgs_orgId").on(table.orgId)
]
);
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", { export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
codeId: integer("id").primaryKey({ autoIncrement: true }), codeId: integer("id").primaryKey({ autoIncrement: true }),
@@ -982,26 +1045,32 @@ export const actions = sqliteTable("actions", {
description: text("description") description: text("description")
}); });
export const roles = sqliteTable("roles", { export const roles = sqliteTable(
roleId: integer("roleId").primaryKey({ autoIncrement: true }), "roles",
orgId: text("orgId") {
.references(() => orgs.orgId, { roleId: integer("roleId").primaryKey({ autoIncrement: true }),
onDelete: "cascade" orgId: text("orgId")
}) .references(() => orgs.orgId, {
.notNull(), onDelete: "cascade"
isAdmin: integer("isAdmin", { mode: "boolean" }), })
name: text("name").notNull(), .notNull(),
description: text("description"), isAdmin: integer("isAdmin", { mode: "boolean" }),
requireDeviceApproval: integer("requireDeviceApproval", { name: text("name").notNull(),
mode: "boolean" description: text("description"),
}).default(false), requireDeviceApproval: integer("requireDeviceApproval", {
sshSudoMode: text("sshSudoMode").default("full"), // "none" | "full" | "commands" mode: "boolean"
sshSudoCommands: text("sshSudoCommands").default("[]"), }).default(false),
sshCreateHomeDir: integer("sshCreateHomeDir", { mode: "boolean" }).default( sshSudoMode: text("sshSudoMode").default("full"), // "none" | "full" | "commands"
true sshSudoCommands: text("sshSudoCommands").default("[]"),
), sshCreateHomeDir: integer("sshCreateHomeDir", { mode: "boolean" }).default(
sshUnixGroups: text("sshUnixGroups").default("[]") true
}); ),
sshUnixGroups: text("sshUnixGroups").default("[]")
},
(table) => [
index("idx_roles_orgId").on(table.orgId)
]
);
export const userOrgRoles = sqliteTable( export const userOrgRoles = sqliteTable(
"userOrgRoles", "userOrgRoles",
+2 -2
View File
@@ -101,7 +101,7 @@ export const AuthSchema = z.object({
export const RuleSchema = z export const RuleSchema = z
.object({ .object({
action: z.enum(["allow", "deny", "pass"]), action: z.enum(["allow", "deny", "pass"]),
match: z.enum(["cidr", "path", "ip", "country", "asn", "region"]), match: z.enum(["cidr", "path", "ip", "country", "country_is_not", "asn", "region"]),
value: z.coerce.string(), value: z.coerce.string(),
priority: z.int().optional(), priority: z.int().optional(),
enabled: z.boolean().optional().default(true) enabled: z.boolean().optional().default(true)
@@ -136,7 +136,7 @@ export const RuleSchema = z
) )
.refine( .refine(
(rule) => { (rule) => {
if (rule.match === "country") { if (rule.match === "country" || rule.match === "country_is_not") {
if (!hasMaxmindCountryDb) { if (!hasMaxmindCountryDb) {
return false; return false;
} }
+10 -15
View File
@@ -31,7 +31,6 @@ export async function validateAndConstructDomain(
subdomain?: string | null subdomain?: string | null
): Promise<DomainValidationResult> { ): Promise<DomainValidationResult> {
try { try {
// Query domain with organization access check
const [domainRes] = await db const [domainRes] = await db
.select() .select()
.from(domains) .from(domains)
@@ -42,6 +41,10 @@ export async function validateAndConstructDomain(
eq(orgDomains.orgId, orgId), eq(orgDomains.orgId, orgId),
eq(orgDomains.domainId, domainId) eq(orgDomains.domainId, domainId)
) )
)
.leftJoin(
domainNamespaces,
eq(domainNamespaces.domainId, domainId)
); );
// Check if domain exists // Check if domain exists
@@ -52,7 +55,7 @@ export async function validateAndConstructDomain(
}; };
} }
if (!domainRes.orgDomains) { if (!domainRes.orgDomains && !domainRes.domainNamespaces) {
return { return {
success: false, success: false,
error: `Organization does not have access to domain with ID ${domainId}` error: `Organization does not have access to domain with ID ${domainId}`
@@ -83,19 +86,11 @@ export async function validateAndConstructDomain(
} }
// Wildcard subdomains are not allowed on namespace (provided/free) domains // Wildcard subdomains are not allowed on namespace (provided/free) domains
if (isWildcard) { if (isWildcard && domainRes.domainNamespaces) {
const [namespaceDomain] = await db return {
.select() success: false,
.from(domainNamespaces) error: "Wildcard subdomains are not supported for provided or free domains. Use a specific subdomain instead."
.where(eq(domainNamespaces.domainId, domainId)) };
.limit(1);
if (namespaceDomain) {
return {
success: false,
error: "Wildcard subdomains are not supported for provided or free domains. Use a specific subdomain instead."
};
}
} }
if ( if (
@@ -68,6 +68,11 @@ export async function verifyApiKeyAccessTokenAccess(
); );
} }
if (apiKey.isRoot) {
// Root keys can access any access token in any org
return next();
}
if (!resource.orgId) { if (!resource.orgId) {
return next( return next(
createHttpError( createHttpError(
+10 -14
View File
@@ -149,12 +149,8 @@ LQIDAQAB
} }
// Count used sites and users for license comparison // Count used sites and users for license comparison
const [siteCountRes] = await db const [siteCountRes] = await db.select({ value: count() }).from(sites);
.select({ value: count() }) const [userCountRes] = await db.select({ value: count() }).from(users);
.from(sites);
const [userCountRes] = await db
.select({ value: count() })
.from(users);
const status: LicenseStatus = { const status: LicenseStatus = {
hostId: this.hostMeta.hostMetaId, hostId: this.hostMeta.hostMetaId,
@@ -276,10 +272,13 @@ LQIDAQAB
logger.error( logger.error(
`Allowing failure. Will retry one more time at next run interval.` `Allowing failure. Will retry one more time at next run interval.`
); );
// return last known good status // Fall back to last known good status if we have
return this.statusCache.get( // one cached; otherwise return the freshly built
// status (with defaults) rather than undefined.
const lastKnownStatus = this.statusCache.get(
this.statusKey this.statusKey
) as LicenseStatus; ) as LicenseStatus | undefined;
return lastKnownStatus ?? status;
} else { } else {
// Subsequent failures: fail abruptly // Subsequent failures: fail abruptly
throw e; throw e;
@@ -368,10 +367,7 @@ LQIDAQAB
} }
// Only consider quantity if defined and >= 0 (quantity = users, quantity_2 = sites) // Only consider quantity if defined and >= 0 (quantity = users, quantity_2 = sites)
if ( if (cached.quantity_2 !== undefined && cached.quantity_2 >= 0) {
cached.quantity_2 !== undefined &&
cached.quantity_2 >= 0
) {
status.maxSites = status.maxSites =
(status.maxSites ?? 0) + cached.quantity_2; (status.maxSites ?? 0) + cached.quantity_2;
} }
@@ -561,7 +557,7 @@ LQIDAQAB
// Calculate exponential backoff delay // Calculate exponential backoff delay
const retryDelay = Math.floor( const retryDelay = Math.floor(
initialRetryDelay * initialRetryDelay *
Math.pow(exponentialFactor, attempt - 1) Math.pow(exponentialFactor, attempt - 1)
); );
logger.debug( logger.debug(
@@ -88,7 +88,27 @@ export const queryAccessAuditLogsQuery = z.object({
.optional() .optional()
.default("0") .default("0")
.transform(Number) .transform(Number)
.pipe(z.int().nonnegative()) .pipe(z.int().nonnegative()),
ip: z
.preprocess((val) => {
if (val === undefined || val === null || val === "") {
return undefined;
}
if (Array.isArray(val)) {
return val;
}
// the array is returned as this
if (typeof val === "string") {
return val.split(",");
}
return undefined;
}, z.array(z.string()))
.optional()
.catch([])
.openapi({
type: "array",
description: "Filter by IP adresses"
})
}); });
export const queryAccessAuditLogsParams = z.object({ export const queryAccessAuditLogsParams = z.object({
@@ -134,6 +154,9 @@ function getWhere(data: Q) {
data.type ? eq(accessAuditLog.type, data.type) : undefined, data.type ? eq(accessAuditLog.type, data.type) : undefined,
data.action !== undefined data.action !== undefined
? eq(accessAuditLog.action, data.action) ? eq(accessAuditLog.action, data.action)
: undefined,
data.ip && data.ip.length > 0
? inArray(accessAuditLog.ip, data.ip)
: undefined : undefined
); );
} }
+2 -4
View File
@@ -16,12 +16,10 @@ import {
handleRemoteExitNodePingMessage handleRemoteExitNodePingMessage
} from "#private/routers/remoteExitNode"; } from "#private/routers/remoteExitNode";
import { MessageHandler } from "@server/routers/ws"; import { MessageHandler } from "@server/routers/ws";
import { import { handleConnectionLogMessage } from "#private/routers/newt";
handleConnectionLogMessage,
} from "#private/routers/newt";
export const messageHandlers: Record<string, MessageHandler> = { export const messageHandlers: Record<string, MessageHandler> = {
"remoteExitNode/register": handleRemoteExitNodeRegisterMessage, "remoteExitNode/register": handleRemoteExitNodeRegisterMessage,
"remoteExitNode/ping": handleRemoteExitNodePingMessage, "remoteExitNode/ping": handleRemoteExitNodePingMessage,
"newt/access-log": handleConnectionLogMessage, "newt/access-log": handleConnectionLogMessage
}; };
@@ -81,7 +81,27 @@ export const queryAccessAuditLogsQuery = z.strictObject({
.optional() .optional()
.default("0") .default("0")
.transform(Number) .transform(Number)
.pipe(z.int().nonnegative()) .pipe(z.int().nonnegative()),
ip: z
.preprocess((val) => {
if (val === undefined || val === null || val === "") {
return undefined;
}
if (Array.isArray(val)) {
return val;
}
// the array is returned as this
if (typeof val === "string") {
return val.split(",");
}
return undefined;
}, z.array(z.string()))
.optional()
.catch([])
.openapi({
type: "array",
description: "Filter by IP adresses"
})
}); });
export const queryRequestAuditLogsParams = z.object({ export const queryRequestAuditLogsParams = z.object({
@@ -126,6 +146,9 @@ function getWhere(data: Q) {
data.path ? eq(requestAuditLog.path, data.path) : undefined, data.path ? eq(requestAuditLog.path, data.path) : undefined,
data.action !== undefined data.action !== undefined
? eq(requestAuditLog.action, data.action) ? eq(requestAuditLog.action, data.action)
: undefined,
data.ip && data.ip.length > 0
? inArray(requestAuditLog.ip, data.ip)
: undefined : undefined
); );
} }
+9 -4
View File
@@ -533,18 +533,23 @@ export async function startAuthentication(
// If email is provided, get security keys for that specific user // If email is provided, get security keys for that specific user
if (email) { if (email) {
const [user] = await db const matchingUsers = await db
.select() .select()
.from(users) .from(users)
.where(eq(users.email, email)) .where(
.limit(1); and(
eq(users.email, email.toLowerCase()),
eq(users.type, UserType.Internal)
)
);
if (!user || user.type !== UserType.Internal) { if (matchingUsers.length !== 1) {
return next( return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid credentials") createHttpError(HttpCode.BAD_REQUEST, "Invalid credentials")
); );
} }
const user = matchingUsers[0];
userId = user.userId; userId = user.userId;
const userSecurityKeys = await db const userSecurityKeys = await db
+49 -38
View File
@@ -42,54 +42,62 @@ export async function setServerAdmin(
const { email, password, setupToken } = parsedBody.data; const { email, password, setupToken } = parsedBody.data;
// Validate setup token
const [validToken] = await db
.select()
.from(setupTokens)
.where(
and(
eq(setupTokens.token, setupToken),
eq(setupTokens.used, false)
)
);
if (!validToken) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid or expired setup token"
)
);
}
const [existing] = await db
.select()
.from(users)
.where(eq(users.serverAdmin, true));
if (existing) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Server admin already exists"
)
);
}
const passwordHash = await hashPassword(password); const passwordHash = await hashPassword(password);
const userId = generateId(15); const userId = generateId(15);
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
// Mark the token as used const consumed = await trx
await trx
.update(setupTokens) .update(setupTokens)
.set({ .set({
used: true, used: true,
dateUsed: moment().toISOString() dateUsed: moment().toISOString()
}) })
.where(eq(setupTokens.tokenId, validToken.tokenId)); .where(
and(
eq(setupTokens.token, setupToken),
eq(setupTokens.used, false)
)
)
.returning({ tokenId: setupTokens.tokenId });
if (!consumed.length) {
throw createHttpError(
HttpCode.BAD_REQUEST,
"Invalid setup token"
);
}
const [existingAdmin] = await trx
.select({ userId: users.userId })
.from(users)
.where(eq(users.serverAdmin, true))
.limit(1);
if (existingAdmin) {
throw createHttpError(
HttpCode.BAD_REQUEST,
"Server admin already exists"
);
}
const [existingUser] = await trx
.select({ userId: users.userId })
.from(users)
.where(
and(
eq(users.email, email),
eq(users.type, UserType.Internal)
)
)
.limit(1);
if (existingUser) {
throw createHttpError(
HttpCode.BAD_REQUEST,
"A user with that email address already exists"
);
}
// Create the server admin user
await trx.insert(users).values({ await trx.insert(users).values({
userId: userId, userId: userId,
email: email, email: email,
@@ -111,6 +119,9 @@ export async function setServerAdmin(
status: HttpCode.OK status: HttpCode.OK
}); });
} catch (e) { } catch (e) {
if (createHttpError.isHttpError(e)) {
return next(e);
}
logger.error(e); logger.error(e);
return next( return next(
createHttpError( createHttpError(
+1 -1
View File
@@ -48,7 +48,7 @@ export async function validateSetupToken(
return response<ValidateSetupTokenResponse>(res, { return response<ValidateSetupTokenResponse>(res, {
data: { data: {
valid: false, valid: false,
message: "Invalid or expired setup token" message: "Invalid setup token"
}, },
success: true, success: true,
error: false, error: false,
+46 -13
View File
@@ -12,6 +12,7 @@ import { DateTimeValue } from "@app/components/DateTimePicker";
import { ArrowUpRight, Key, User } from "lucide-react"; import { ArrowUpRight, Key, User } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { ColumnFilterButton } from "@app/components/ColumnFilterButton"; import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
import { ColumnMultiFilterButton } from "@app/components/ColumnMultiFilterButton";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { build } from "@server/build"; import { build } from "@server/build";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo"; import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
@@ -26,6 +27,7 @@ import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { logQueries } from "@app/lib/queries"; import { logQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import type { QueryAccessAuditLogResponse } from "@server/routers/auditLogs/types"; import type { QueryAccessAuditLogResponse } from "@server/routers/auditLogs/types";
import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji";
export default function GeneralPage() { export default function GeneralPage() {
const router = useRouter(); const router = useRouter();
@@ -45,12 +47,14 @@ export default function GeneralPage() {
resourceId?: string; resourceId?: string;
location?: string; location?: string;
actor?: string; actor?: string;
ip?: string[];
}>({ }>({
action: searchParams.get("action") || undefined, action: searchParams.get("action") || undefined,
type: searchParams.get("type") || undefined, type: searchParams.get("type") || undefined,
resourceId: searchParams.get("resourceId") || undefined, resourceId: searchParams.get("resourceId") || undefined,
location: searchParams.get("location") || undefined, location: searchParams.get("location") || undefined,
actor: searchParams.get("actor") || undefined actor: searchParams.get("actor") || undefined,
ip: searchParams.getAll("ip") || undefined
}); });
const [currentPage, setCurrentPage] = useState<number>(0); const [currentPage, setCurrentPage] = useState<number>(0);
@@ -176,7 +180,7 @@ export default function GeneralPage() {
const handleFilterChange = ( const handleFilterChange = (
filterType: keyof typeof filters, filterType: keyof typeof filters,
value: string | undefined value: string | string[] | undefined
) => { ) => {
const newFilters = { ...filters, [filterType]: value }; const newFilters = { ...filters, [filterType]: value };
setFilters(newFilters); setFilters(newFilters);
@@ -194,10 +198,13 @@ export default function GeneralPage() {
) => { ) => {
const params = new URLSearchParams(searchParams); const params = new URLSearchParams(searchParams);
Object.entries(newFilters).forEach(([key, value]) => { Object.entries(newFilters).forEach(([key, value]) => {
if (value) { params.delete(key);
if (typeof value === "string") {
params.set(key, value); params.set(key, value);
} else { } else if (typeof value !== "undefined" && "length" in value) {
params.delete(key); for (const element of value) {
params.append(key, element);
}
} }
}); });
router.replace(`?${params.toString()}`, { scroll: false }); router.replace(`?${params.toString()}`, { scroll: false });
@@ -205,6 +212,7 @@ export default function GeneralPage() {
const exportData = async () => { const exportData = async () => {
try { try {
const { ip, ...restFilters } = filters;
const params: any = { const params: any = {
timeStart: dateRange.startDate?.date timeStart: dateRange.startDate?.date
? new Date(dateRange.startDate.date).toISOString() ? new Date(dateRange.startDate.date).toISOString()
@@ -212,13 +220,20 @@ export default function GeneralPage() {
timeEnd: dateRange.endDate?.date timeEnd: dateRange.endDate?.date
? new Date(dateRange.endDate.date).toISOString() ? new Date(dateRange.endDate.date).toISOString()
: undefined, : undefined,
...filters ...restFilters
}; };
const response = await api.get(`/org/${orgId}/logs/access/export`, { // axios serializes arrays as `ip[]=…`, which express's query
responseType: "blob", // parser does not read back as `ip`, so pass them in the URL
params const sp = new URLSearchParams((ip ?? []).map((ip) => ["ip", ip]));
});
const response = await api.get(
`/org/${orgId}/logs/access/export?${sp.toString()}`,
{
responseType: "blob",
params
}
);
const url = window.URL.createObjectURL(new Blob([response.data])); const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a"); const link = document.createElement("a");
@@ -297,7 +312,24 @@ export default function GeneralPage() {
}, },
{ {
accessorKey: "ip", accessorKey: "ip",
header: () => <span className="px-2">{t("ip")}</span>, header: () => (
<span className="px-2">
<ColumnMultiFilterButton
options={(filters.ip ?? []).map((ip) => ({
label: ip,
value: ip
}))}
label={t("ip")}
allowArbitraryValues
searchPlaceholder={t("ipFilterSearchPlaceholder")}
emptyMessage={t("ipFilterEmptyMessage")}
selectedValues={filters.ip ?? []}
onSelectedValuesChange={(value) =>
handleFilterChange("ip", value)
}
/>
</span>
),
cell: ({ row }) => { cell: ({ row }) => {
return row.original.ip ? ( return row.original.ip ? (
row.original.ip row.original.ip
@@ -315,7 +347,7 @@ export default function GeneralPage() {
options={filterAttributes.locations.map( options={filterAttributes.locations.map(
(location) => ({ (location) => ({
value: location, value: location,
label: location label: `${location} ${countryCodeToFlagEmoji(location)}`
}) })
)} )}
label={t("location")} label={t("location")}
@@ -334,7 +366,8 @@ export default function GeneralPage() {
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
{row.original.location ? ( {row.original.location ? (
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{row.original.location} {row.original.location}{" "}
{countryCodeToFlagEmoji(row.original.location)}
</span> </span>
) : ( ) : (
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
+40 -10
View File
@@ -23,6 +23,8 @@ import { useMemo, useState, useTransition } from "react";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize"; import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
import type { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types"; import type { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types";
import { ColumnFilterButton } from "@app/components/ColumnFilterButton"; import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji";
import { ColumnMultiFilterButton } from "@app/components/ColumnMultiFilterButton";
export default function GeneralPage() { export default function GeneralPage() {
const router = useRouter(); const router = useRouter();
@@ -47,6 +49,7 @@ export default function GeneralPage() {
method?: string; method?: string;
reason?: string; reason?: string;
path?: string; path?: string;
ip?: string[];
}>({ }>({
action: searchParams.get("action") || undefined, action: searchParams.get("action") || undefined,
host: searchParams.get("host") || undefined, host: searchParams.get("host") || undefined,
@@ -55,7 +58,8 @@ export default function GeneralPage() {
actor: searchParams.get("actor") || undefined, actor: searchParams.get("actor") || undefined,
method: searchParams.get("method") || undefined, method: searchParams.get("method") || undefined,
reason: searchParams.get("reason") || undefined, reason: searchParams.get("reason") || undefined,
path: searchParams.get("path") || undefined path: searchParams.get("path") || undefined,
ip: searchParams.getAll("ip") || undefined
}); });
const getDefaultDateRange = () => { const getDefaultDateRange = () => {
@@ -179,7 +183,7 @@ export default function GeneralPage() {
const handleFilterChange = ( const handleFilterChange = (
filterType: keyof typeof filters, filterType: keyof typeof filters,
value: string | undefined value: string | string[] | undefined
) => { ) => {
const newFilters = { ...filters, [filterType]: value }; const newFilters = { ...filters, [filterType]: value };
setFilters(newFilters); setFilters(newFilters);
@@ -197,10 +201,13 @@ export default function GeneralPage() {
) => { ) => {
const params = new URLSearchParams(searchParams); const params = new URLSearchParams(searchParams);
Object.entries(newFilters).forEach(([key, value]) => { Object.entries(newFilters).forEach(([key, value]) => {
if (value) { params.delete(key);
if (typeof value === "string") {
params.set(key, value); params.set(key, value);
} else { } else if (typeof value !== "undefined" && "length" in value) {
params.delete(key); for (const element of value) {
params.append(key, element);
}
} }
}); });
router.replace(`?${params.toString()}`, { scroll: false }); router.replace(`?${params.toString()}`, { scroll: false });
@@ -209,6 +216,7 @@ export default function GeneralPage() {
const exportData = async () => { const exportData = async () => {
try { try {
// Prepare query params for export // Prepare query params for export
const { ip, ...restFilters } = filters;
const params: any = { const params: any = {
timeStart: dateRange.startDate?.date timeStart: dateRange.startDate?.date
? new Date(dateRange.startDate.date).toISOString() ? new Date(dateRange.startDate.date).toISOString()
@@ -216,11 +224,15 @@ export default function GeneralPage() {
timeEnd: dateRange.endDate?.date timeEnd: dateRange.endDate?.date
? new Date(dateRange.endDate.date).toISOString() ? new Date(dateRange.endDate.date).toISOString()
: undefined, : undefined,
...filters ...restFilters
}; };
// axios serializes arrays as `ip[]=…`, which express's query
// parser does not read back as `ip`, so pass them in the URL
const sp = new URLSearchParams((ip ?? []).map((ip) => ["ip", ip]));
const response = await api.get( const response = await api.get(
`/org/${orgId}/logs/request/export`, `/org/${orgId}/logs/request/export?${sp.toString()}`,
{ {
responseType: "blob", responseType: "blob",
params params
@@ -351,7 +363,24 @@ export default function GeneralPage() {
}, },
{ {
accessorKey: "ip", accessorKey: "ip",
header: ({ column }) => <span className="px-2">{t("ip")}</span>, header: ({ column }) => (
<span className="px-2">
<ColumnMultiFilterButton
options={(filters.ip ?? []).map((ip) => ({
label: ip,
value: ip
}))}
label={t("ip")}
allowArbitraryValues
searchPlaceholder={t("ipFilterSearchPlaceholder")}
emptyMessage={t("ipFilterEmptyMessage")}
selectedValues={filters.ip ?? []}
onSelectedValuesChange={(value) =>
handleFilterChange("ip", value)
}
/>
</span>
),
cell: ({ row }) => { cell: ({ row }) => {
return row.original.ip ? ( return row.original.ip ? (
row.original.ip row.original.ip
@@ -369,7 +398,7 @@ export default function GeneralPage() {
options={filterAttributes.locations.map( options={filterAttributes.locations.map(
(location) => ({ (location) => ({
value: location, value: location,
label: location label: `${location} ${countryCodeToFlagEmoji(location)}`
}) })
)} )}
selectedValue={filters.location} selectedValue={filters.location}
@@ -389,7 +418,8 @@ export default function GeneralPage() {
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
{row.original.location ? ( {row.original.location ? (
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
{row.original.location} {row.original.location}{" "}
{countryCodeToFlagEmoji(row.original.location)}
</span> </span>
) : ( ) : (
<span className="text-muted-foreground text-xs"> <span className="text-muted-foreground text-xs">
+5 -3
View File
@@ -21,7 +21,7 @@ import { useTranslations } from "next-intl";
interface FilterOption { interface FilterOption {
value: string; value: string;
label: string; label: React.ReactNode;
} }
interface ColumnFilterButtonProps { interface ColumnFilterButtonProps {
@@ -32,6 +32,7 @@ interface ColumnFilterButtonProps {
emptyMessage?: string; emptyMessage?: string;
className?: string; className?: string;
label: string; label: string;
allowArbitraryValues?: boolean;
} }
export function ColumnFilterButton({ export function ColumnFilterButton({
@@ -41,7 +42,8 @@ export function ColumnFilterButton({
searchPlaceholder = "Search...", searchPlaceholder = "Search...",
emptyMessage = "No options found", emptyMessage = "No options found",
className, className,
label label,
allowArbitraryValues
}: ColumnFilterButtonProps) { }: ColumnFilterButtonProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@@ -101,7 +103,7 @@ export function ColumnFilterButton({
{options.map((option) => ( {options.map((option) => (
<CommandItem <CommandItem
key={option.value} key={option.value}
value={option.label} value={option.value}
onSelect={() => { onSelect={() => {
onValueChange( onValueChange(
selectedValue === option.value selectedValue === option.value
+24 -4
View File
@@ -35,6 +35,7 @@ type ColumnMultiFilterButtonProps = {
emptyMessage?: string; emptyMessage?: string;
className?: string; className?: string;
label: string; label: string;
allowArbitraryValues?: boolean;
}; };
export function ColumnMultiFilterButton({ export function ColumnMultiFilterButton({
@@ -44,11 +45,26 @@ export function ColumnMultiFilterButton({
searchPlaceholder = "Search...", searchPlaceholder = "Search...",
emptyMessage = "No options found", emptyMessage = "No options found",
className, className,
label label,
allowArbitraryValues
}: ColumnMultiFilterButtonProps) { }: ColumnMultiFilterButtonProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const t = useTranslations(); const t = useTranslations();
const visibleOptions = useMemo<FilterOption[]>(() => {
const newOptions = [...options];
if (allowArbitraryValues && searchQuery.trim().length > 0) {
newOptions.push({
label: searchQuery,
value: searchQuery
});
}
return newOptions;
}, [options, allowArbitraryValues, searchQuery]);
const selectedSet = useMemo( const selectedSet = useMemo(
() => new Set(selectedValues), () => new Set(selectedValues),
[selectedValues] [selectedValues]
@@ -64,7 +80,7 @@ export function ColumnMultiFilterButton({
selectedValues[0] selectedValues[0]
); );
} }
return t("accessUsersRoleFilterCount", { return t("multiSelectFilterCount", {
count: selectedValues.length count: selectedValues.length
}); });
}, [selectedValues, options, t]); }, [selectedValues, options, t]);
@@ -108,7 +124,11 @@ export function ColumnMultiFilterButton({
align="start" align="start"
> >
<Command> <Command>
<CommandInput placeholder={searchPlaceholder} /> <CommandInput
placeholder={searchPlaceholder}
value={searchQuery}
onValueChange={setSearchQuery}
/>
<CommandList> <CommandList>
<CommandEmpty>{emptyMessage}</CommandEmpty> <CommandEmpty>{emptyMessage}</CommandEmpty>
<CommandGroup> <CommandGroup>
@@ -123,7 +143,7 @@ export function ColumnMultiFilterButton({
{t("accessFilterClear")} {t("accessFilterClear")}
</CommandItem> </CommandItem>
)} )}
{options.map((option) => ( {visibleOptions.map((option) => (
<CommandItem <CommandItem
key={option.value} key={option.value}
value={option.label} value={option.label}
+30 -22
View File
@@ -1,3 +1,8 @@
import {
getAiBudgetScopeListPath,
type AiBudgetScope
} from "@app/lib/aiBudgetScope";
import type { AiProviderType } from "@app/lib/aiProviderDefaults";
import type { LauncherQueryFilters } from "@app/lib/launcherSearchParams"; import type { LauncherQueryFilters } from "@app/lib/launcherSearchParams";
import { buildLauncherSearchParams } from "@app/lib/launcherSearchParams"; import { buildLauncherSearchParams } from "@app/lib/launcherSearchParams";
import { build } from "@server/build"; import { build } from "@server/build";
@@ -5,15 +10,21 @@ import {
StatusHistoryResponse, StatusHistoryResponse,
type BatchedStatusHistoryResponse type BatchedStatusHistoryResponse
} from "@server/lib/statusHistory"; } from "@server/lib/statusHistory";
import type { ListAiBudgetsByScopeResponse } from "@server/routers/aiBudget/types";
import type {
ListAiModelsResponse,
ListAiProvidersResponse,
ListCatalogModelsResponse
} from "@server/routers/aiProvider/types";
import type { ListAlertRulesResponse } from "@server/routers/alertRule/types"; import type { ListAlertRulesResponse } from "@server/routers/alertRule/types";
import type { import type {
QueryRequestAnalyticsResponse,
QueryAiUsageFilterOptionsResponse, QueryAiUsageFilterOptionsResponse,
QueryAiUsageOverviewResponse, QueryAiUsageOverviewResponse,
QueryAiUsageProvidersResponse, QueryAiUsageProvidersResponse,
QueryAiUsageResourcesResponse, QueryAiUsageResourcesResponse,
QueryAiUsageUsersRolesResponse, QueryAiUsageUsersRolesResponse,
QueryAiUsageVirtualApiKeysResponse QueryAiUsageVirtualApiKeysResponse,
QueryRequestAnalyticsResponse
} from "@server/routers/auditLogs"; } from "@server/routers/auditLogs";
import type { import type {
QueryAccessAuditLogResponse, QueryAccessAuditLogResponse,
@@ -34,6 +45,7 @@ import type {
import type { GetDomainResponse } from "@server/routers/domain/getDomain"; import type { GetDomainResponse } from "@server/routers/domain/getDomain";
import { ListHealthChecksResponse } from "@server/routers/healthChecks/types"; import { ListHealthChecksResponse } from "@server/routers/healthChecks/types";
import type { ListOrgLabelsResponse } from "@server/routers/labels/types"; import type { ListOrgLabelsResponse } from "@server/routers/labels/types";
import type { ListLauncherAiModelsResponse } from "@server/routers/launcher/listLauncherAiModels";
import type { import type {
LauncherResource, LauncherResource,
ListLauncherGroupsResponse, ListLauncherGroupsResponse,
@@ -43,9 +55,8 @@ import type {
ListLauncherSitesResponse, ListLauncherSitesResponse,
ListLauncherViewsResponse ListLauncherViewsResponse
} from "@server/routers/launcher/types"; } from "@server/routers/launcher/types";
import type { ListLauncherAiModelsResponse } from "@server/routers/launcher/listLauncherAiModels";
import type { ListMyVirtualApiKeysResponse } from "@server/routers/virtualApiKey/types";
import type { GetResourcePolicyResponse } from "@server/routers/policy"; import type { GetResourcePolicyResponse } from "@server/routers/policy";
import type { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
import type { import type {
GetResourcePoliciesResponse, GetResourcePoliciesResponse,
GetResourceWhitelistResponse, GetResourceWhitelistResponse,
@@ -59,7 +70,6 @@ import type {
import type { GetResourceResponse } from "@server/routers/resource/getResource"; import type { GetResourceResponse } from "@server/routers/resource/getResource";
import type { GetResourceAuthInfoResponse } from "@server/routers/resource/getResourceAuthInfo"; import type { GetResourceAuthInfoResponse } from "@server/routers/resource/getResourceAuthInfo";
import type { ListResourcePoliciesResponse } from "@server/routers/resource/types"; import type { ListResourcePoliciesResponse } from "@server/routers/resource/types";
import type { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
import type { ListRolesResponse } from "@server/routers/role"; import type { ListRolesResponse } from "@server/routers/role";
import type { ListSitesResponse } from "@server/routers/site"; import type { ListSitesResponse } from "@server/routers/site";
import type { import type {
@@ -71,18 +81,8 @@ import type {
} from "@server/routers/siteResource"; } from "@server/routers/siteResource";
import type { GetSiteResourceResponse } from "@server/routers/siteResource/getSiteResource"; import type { GetSiteResourceResponse } from "@server/routers/siteResource/getSiteResource";
import type { ListTargetsResponse } from "@server/routers/target"; import type { ListTargetsResponse } from "@server/routers/target";
import type {
ListAiModelsResponse,
ListAiProvidersResponse,
ListCatalogModelsResponse
} from "@server/routers/aiProvider/types";
import type { AiProviderType } from "@app/lib/aiProviderDefaults";
import type { ListAiBudgetsByScopeResponse } from "@server/routers/aiBudget/types";
import {
getAiBudgetScopeListPath,
type AiBudgetScope
} from "@app/lib/aiBudgetScope";
import type { ListUsersResponse } from "@server/routers/user"; import type { ListUsersResponse } from "@server/routers/user";
import type { ListMyVirtualApiKeysResponse } from "@server/routers/virtualApiKey/types";
import type ResponseT from "@server/types/Response"; import type ResponseT from "@server/types/Response";
import { import {
infiniteQueryOptions, infiniteQueryOptions,
@@ -1000,7 +1000,8 @@ export const httpLogsFiltersSchema = z.object({
actor: z.string().optional().catch(undefined), actor: z.string().optional().catch(undefined),
method: z.string().optional().catch(undefined), method: z.string().optional().catch(undefined),
reason: z.string().optional().catch(undefined), reason: z.string().optional().catch(undefined),
path: z.string().optional().catch(undefined) path: z.string().optional().catch(undefined),
ip: z.array(z.string()).optional().catch(undefined)
}); });
export type HttpLogFilters = z.output<typeof httpLogsFiltersSchema>; export type HttpLogFilters = z.output<typeof httpLogsFiltersSchema>;
@@ -1026,7 +1027,8 @@ export const accessLogsFiltersSchema = z.object({
action: z.string().optional().catch(undefined), action: z.string().optional().catch(undefined),
location: z.string().optional().catch(undefined), location: z.string().optional().catch(undefined),
actor: z.string().optional().catch(undefined), actor: z.string().optional().catch(undefined),
type: z.string().optional().catch(undefined) type: z.string().optional().catch(undefined),
ip: z.array(z.string()).optional().catch(undefined)
}); });
export type AccessLogFilters = z.output<typeof accessLogsFiltersSchema>; export type AccessLogFilters = z.output<typeof accessLogsFiltersSchema>;
@@ -1139,10 +1141,13 @@ export const logQueries = {
queryOptions({ queryOptions({
queryKey: ["REQUEST_LOGS", orgId, "ALL", filters] as const, queryKey: ["REQUEST_LOGS", orgId, "ALL", filters] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
const { page, pageSize, ...rest } = filters; const { page, pageSize, ip, ...rest } = filters;
const sp = new URLSearchParams(
(ip ?? []).map((ip) => ["ip", ip])
);
const res = await meta!.api.get< const res = await meta!.api.get<
AxiosResponse<QueryRequestAuditLogResponse> AxiosResponse<QueryRequestAuditLogResponse>
>(`/org/${orgId}/logs/request`, { >(`/org/${orgId}/logs/request?${sp.toString()}`, {
params: { params: {
...rest, ...rest,
limit: pageSize, limit: pageSize,
@@ -1164,10 +1169,13 @@ export const logQueries = {
queryOptions({ queryOptions({
queryKey: ["ACCESS_LOGS", orgId, "ALL", filters] as const, queryKey: ["ACCESS_LOGS", orgId, "ALL", filters] as const,
queryFn: async ({ signal, meta }) => { queryFn: async ({ signal, meta }) => {
const { page, pageSize, ...rest } = filters; const { page, pageSize, ip, ...rest } = filters;
const sp = new URLSearchParams(
(ip ?? []).map((ip) => ["ip", ip])
);
const res = await meta!.api.get< const res = await meta!.api.get<
AxiosResponse<QueryAccessAuditLogResponse> AxiosResponse<QueryAccessAuditLogResponse>
>(`/org/${orgId}/logs/access`, { >(`/org/${orgId}/logs/access?${sp.toString()}`, {
params: { params: {
...rest, ...rest,
limit: pageSize, limit: pageSize,