mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-02 01:09:03 +02:00
Compare commits
41 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f013335f9 | |||
| b4f6ae74d7 | |||
| f1711ee0b0 | |||
| 780d767a65 | |||
| d6d923e972 | |||
| 41139f2fd0 | |||
| ebcdeab414 | |||
| 2cc7d03ace | |||
| c36cf698c1 | |||
| 34671c6b13 | |||
| 8dfc95347f | |||
| 22db0319c2 | |||
| 39722d30af | |||
| 8aef14cf9f | |||
| 0dece5fef1 | |||
| e7f38c089f | |||
| dd0a5a359a | |||
| 1650ece0c3 | |||
| 7f94d99455 | |||
| 1f9e99219d | |||
| bc56a2bed0 | |||
| 49dcc590ce | |||
| da3e3ff33f | |||
| 69d539f107 | |||
| 872e0f9ae1 | |||
| 5b3713a72f | |||
| f02be1fdbf | |||
| 0bf04cf0cd | |||
| 7cda28d685 | |||
| 60bc74c4df | |||
| 48ab6c501f | |||
| d00b9478a2 | |||
| 4ddf36ebcc | |||
| 28b32fe6f7 | |||
| adfb6003d9 | |||
| 6a5ecab013 | |||
| 2c197fab9f | |||
| 65e4fe91b9 | |||
| 52c078a489 | |||
| 195f67c6eb | |||
| 668a04bcd2 |
@@ -99,6 +99,19 @@ Access private resources like SSH servers, databases, RDP, and entire network ra
|
|||||||
|
|
||||||
<img src="public/screenshots/private-resources.png" alt="Private resources" width="100%" />
|
<img src="public/screenshots/private-resources.png" alt="Private resources" width="100%" />
|
||||||
|
|
||||||
|
### Identity-aware AI gateway
|
||||||
|
|
||||||
|
Put an identity-aware proxy in front of public cloud (OpenAI, Anthropic, Gemini, etc.) and self-hosted model servers (Ollama, vLLM, Mistral, etc.) so coding agents and AI clients call a single Pangolin URL. Publish it as a public resource with personal API keys, or keep it private on a client tunnel where the connected client is the credential for keyless access. Budgets, session history, and usage analytics sit in front of every call.
|
||||||
|
|
||||||
|
* Access self-hosted models (vLLM, Ollama, etc) alongside cloud models (OpenAI, Anthropic, etc) in one place
|
||||||
|
* Keyless access by authenticating users with the Pangolin desktop client
|
||||||
|
* Or, provide users with personal API keys
|
||||||
|
* Control costs and token usage by setting budgets
|
||||||
|
* Audit with detailed session history and analytics
|
||||||
|
* Integrate AI clients and coding agents (Claude Code, Codex, OpenCode, etc)
|
||||||
|
|
||||||
|
<img src="public/screenshots/expanded-session-logs.png" alt="AI Session Logs" width="100%" />
|
||||||
|
|
||||||
### Give users and roles access to resources
|
### Give users and roles access to resources
|
||||||
|
|
||||||
Use Pangolin's built-in users or bring your own identity provider and set up role-based access control (RBAC). Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications, services, and routes you explicitly define.
|
Use Pangolin's built-in users or bring your own identity provider and set up role-based access control (RBAC). Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications, services, and routes you explicitly define.
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+8
-1
@@ -1176,6 +1176,10 @@
|
|||||||
"idpJmespathAboutDescriptionLink": "Learn more about JMESPath",
|
"idpJmespathAboutDescriptionLink": "Learn more about JMESPath",
|
||||||
"idpJmespathLabel": "Identifier Path",
|
"idpJmespathLabel": "Identifier Path",
|
||||||
"idpJmespathLabelDescription": "The path to the user identifier in the ID token",
|
"idpJmespathLabelDescription": "The path to the user identifier in the ID token",
|
||||||
|
"idpIdentifierChangeTitle": "Identifier Path Change Warning",
|
||||||
|
"idpIdentifierChangeDescription": "You are about to change the identifier path. This will affect how existing users are mapped. Users who previously signed in through this identity provider may no longer be recognized as the same users.",
|
||||||
|
"idpIdentifierChangeConfirmMessage": "I confirm",
|
||||||
|
"idpIdentifierChangeWarningText": "This will affect how existing users are mapped",
|
||||||
"idpJmespathEmailPathOptional": "Email Path (Optional)",
|
"idpJmespathEmailPathOptional": "Email Path (Optional)",
|
||||||
"idpJmespathEmailPathOptionalDescription": "The path to the user's email in the ID token",
|
"idpJmespathEmailPathOptionalDescription": "The path to the user's email in the ID token",
|
||||||
"idpJmespathNamePathOptional": "Name Path (Optional)",
|
"idpJmespathNamePathOptional": "Name Path (Optional)",
|
||||||
@@ -1573,6 +1577,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 +2602,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}",
|
||||||
@@ -2993,7 +3000,7 @@
|
|||||||
"remoteExitNodeNetworkingSubnetsPlaceholder": "Add a CIDR range (e.g. 10.0.0.0/8)",
|
"remoteExitNodeNetworkingSubnetsPlaceholder": "Add a CIDR range (e.g. 10.0.0.0/8)",
|
||||||
"remoteExitNodeNetworkingSubnetsLoadError": "Failed to load subnets",
|
"remoteExitNodeNetworkingSubnetsLoadError": "Failed to load subnets",
|
||||||
"remoteExitNodeNetworkingLabelsTitle": "Preference Labels",
|
"remoteExitNodeNetworkingLabelsTitle": "Preference Labels",
|
||||||
"remoteExitNodeNetworkingLabelsDescription": "Sites with these labels will be enforced to connect through this remote exit node.",
|
"remoteExitNodeNetworkingLabelsDescription": "Sites with these labels will prefer to connect through this remote exit node.",
|
||||||
"remoteExitNodeNetworkingLabelsButtonText": "Select labels...",
|
"remoteExitNodeNetworkingLabelsButtonText": "Select labels...",
|
||||||
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Search labels...",
|
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Search labels...",
|
||||||
"remoteExitNodeNetworkingLabelsLoadError": "Failed to load labels",
|
"remoteExitNodeNetworkingLabelsLoadError": "Failed to load labels",
|
||||||
|
|||||||
Generated
+70
-26
@@ -7569,6 +7569,72 @@
|
|||||||
"node": ">=14.0.0"
|
"node": ">=14.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@emnapi/wasi-threads": "1.2.1",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": {
|
||||||
|
"version": "1.10.0",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@tybys/wasm-util": "^0.10.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emnapi/core": "^1.7.1",
|
||||||
|
"@emnapi/runtime": "^1.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": {
|
||||||
|
"version": "0.10.1",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": {
|
||||||
|
"version": "2.8.1",
|
||||||
|
"dev": true,
|
||||||
|
"inBundle": true,
|
||||||
|
"license": "0BSD",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz",
|
||||||
@@ -17131,36 +17197,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-adapter": {
|
"node_modules/socket.io-adapter": {
|
||||||
"version": "2.5.6",
|
"version": "2.5.8",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz",
|
||||||
"integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
|
"integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "~4.4.1",
|
"debug": "~4.4.1",
|
||||||
"ws": "~8.18.3"
|
"ws": "~8.21.0"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/socket.io-adapter/node_modules/ws": {
|
|
||||||
"version": "8.18.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
|
||||||
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": ">=5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-parser": {
|
"node_modules/socket.io-parser": {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 790 KiB |
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { and, asc, eq, or } from "drizzle-orm";
|
||||||
|
import { Transaction, User, userOrgs, users } from "@server/db";
|
||||||
|
|
||||||
|
export async function findOrgUserByIdentifier(
|
||||||
|
trx: Transaction,
|
||||||
|
orgId: string,
|
||||||
|
identifier: string
|
||||||
|
): Promise<User | null> {
|
||||||
|
const [match] = await trx
|
||||||
|
.select()
|
||||||
|
.from(users)
|
||||||
|
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
or(eq(users.username, identifier), eq(users.email, identifier)),
|
||||||
|
eq(userOrgs.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.orderBy(asc(users.dateCreated), asc(users.userId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return match?.user ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveOrgUserIds(
|
||||||
|
trx: Transaction,
|
||||||
|
orgId: string,
|
||||||
|
identifiers: string[]
|
||||||
|
): Promise<string[]> {
|
||||||
|
const userIds = new Set<string>();
|
||||||
|
for (const identifier of identifiers) {
|
||||||
|
const user = await findOrgUserByIdentifier(trx, orgId, identifier);
|
||||||
|
if (user) {
|
||||||
|
userIds.add(user.userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...userIds];
|
||||||
|
}
|
||||||
@@ -11,15 +11,14 @@ import {
|
|||||||
siteNetworks,
|
siteNetworks,
|
||||||
siteResources,
|
siteResources,
|
||||||
Transaction,
|
Transaction,
|
||||||
userOrgs,
|
|
||||||
users,
|
|
||||||
userSiteResources,
|
userSiteResources,
|
||||||
networks
|
networks
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { sites } from "@server/db";
|
import { sites } from "@server/db";
|
||||||
import { eq, and, ne, inArray, or, isNotNull } from "drizzle-orm";
|
import { eq, and, ne, inArray, isNotNull } from "drizzle-orm";
|
||||||
import { Config } from "./types";
|
import { Config } from "./types";
|
||||||
import { getOrCreateLabelIds, syncSiteResourceLabels } from "./labels";
|
import { getOrCreateLabelIds, syncSiteResourceLabels } from "./labels";
|
||||||
|
import { resolveOrgUserIds } from "./findOrgUser";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
||||||
import { getNextAvailableAliasAddress } from "../ip";
|
import { getNextAvailableAliasAddress } from "../ip";
|
||||||
@@ -389,28 +388,22 @@ export async function updatePrivateResources(
|
|||||||
.where(eq(userSiteResources.siteResourceId, siteResourceId));
|
.where(eq(userSiteResources.siteResourceId, siteResourceId));
|
||||||
|
|
||||||
if (resourceData.users.length > 0) {
|
if (resourceData.users.length > 0) {
|
||||||
// get userIds from username
|
const userIds = await resolveOrgUserIds(
|
||||||
const usersToUpdate = await trx
|
trx,
|
||||||
.select()
|
orgId,
|
||||||
.from(users)
|
resourceData.users
|
||||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
);
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(
|
|
||||||
inArray(users.username, resourceData.users),
|
|
||||||
inArray(users.email, resourceData.users)
|
|
||||||
),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const userIds = usersToUpdate.map((user) => user.user.userId);
|
if (userIds.length > 0) {
|
||||||
|
await trx
|
||||||
await trx
|
.insert(userSiteResources)
|
||||||
.insert(userSiteResources)
|
.values(
|
||||||
.values(
|
userIds.map((userId) => ({
|
||||||
userIds.map((userId) => ({ userId, siteResourceId }))
|
userId,
|
||||||
);
|
siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all admin role IDs for this org to exclude from deletion
|
// Get all admin role IDs for this org to exclude from deletion
|
||||||
@@ -721,28 +714,22 @@ export async function updatePrivateResources(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (resourceData.users.length > 0) {
|
if (resourceData.users.length > 0) {
|
||||||
// get userIds from username
|
const userIds = await resolveOrgUserIds(
|
||||||
const usersToUpdate = await trx
|
trx,
|
||||||
.select()
|
orgId,
|
||||||
.from(users)
|
resourceData.users
|
||||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
);
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(
|
|
||||||
inArray(users.username, resourceData.users),
|
|
||||||
inArray(users.email, resourceData.users)
|
|
||||||
),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const userIds = usersToUpdate.map((user) => user.user.userId);
|
if (userIds.length > 0) {
|
||||||
|
await trx
|
||||||
await trx
|
.insert(userSiteResources)
|
||||||
.insert(userSiteResources)
|
.values(
|
||||||
.values(
|
userIds.map((userId) => ({
|
||||||
userIds.map((userId) => ({ userId, siteResourceId }))
|
userId,
|
||||||
);
|
siteResourceId
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resourceData.machines.length > 0) {
|
if (resourceData.machines.length > 0) {
|
||||||
|
|||||||
@@ -46,11 +46,12 @@ import { encrypt } from "@server/lib/crypto";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
|
||||||
import { pickPort } from "@server/routers/target/helpers";
|
import { pickPort } from "@server/routers/target/helpers";
|
||||||
import { and, asc, eq, isNotNull, ne, or } from "drizzle-orm";
|
import { and, asc, eq, isNotNull, ne } from "drizzle-orm";
|
||||||
import { tierMatrix } from "../billing/tierMatrix";
|
import { tierMatrix } from "../billing/tierMatrix";
|
||||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||||
import { Config, isTargetsOnlyResource, TargetData } from "./types";
|
import { Config, isTargetsOnlyResource, TargetData } from "./types";
|
||||||
import { getOrCreateLabelIds, syncResourceLabels } from "./labels";
|
import { getOrCreateLabelIds, syncResourceLabels } from "./labels";
|
||||||
|
import { findOrgUserByIdentifier } from "./findOrgUser";
|
||||||
import { LimitId } from "../billing";
|
import { LimitId } from "../billing";
|
||||||
import { usageService } from "../billing/usageService";
|
import { usageService } from "../billing/usageService";
|
||||||
import { syncInferenceAiConfig } from "./aiProviders";
|
import { syncInferenceAiConfig } from "./aiProviders";
|
||||||
@@ -1563,29 +1564,19 @@ async function syncUserResources(
|
|||||||
.where(eq(userResources.resourceId, resourceId));
|
.where(eq(userResources.resourceId, resourceId));
|
||||||
|
|
||||||
for (const username of ssoUsers) {
|
for (const username of ssoUsers) {
|
||||||
const [user] = await trx
|
const user = await findOrgUserByIdentifier(trx, orgId, username);
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(eq(users.username, username), eq(users.email, username)),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`User not found: ${username} in org ${orgId}`);
|
throw new Error(`User not found: ${username} in org ${orgId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingUserResource = existingUserResources.find(
|
const existingUserResource = existingUserResources.find(
|
||||||
(rr) => rr.userId === user.user.userId
|
(rr) => rr.userId === user.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!existingUserResource) {
|
if (!existingUserResource) {
|
||||||
await trx.insert(userResources).values({
|
await trx.insert(userResources).values({
|
||||||
userId: user.user.userId,
|
userId: user.userId,
|
||||||
resourceId: resourceId
|
resourceId: resourceId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1955,29 +1946,19 @@ async function syncUserPolicies(
|
|||||||
.where(eq(userPolicies.resourcePolicyId, policyId));
|
.where(eq(userPolicies.resourcePolicyId, policyId));
|
||||||
|
|
||||||
for (const username of ssoUsers) {
|
for (const username of ssoUsers) {
|
||||||
const [user] = await trx
|
const user = await findOrgUserByIdentifier(trx, orgId, username);
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(eq(users.username, username), eq(users.email, username)),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(`User not found: ${username} in org ${orgId}`);
|
throw new Error(`User not found: ${username} in org ${orgId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingUserPolicy = existingUserPoliciesList.find(
|
const existingUserPolicy = existingUserPoliciesList.find(
|
||||||
(up) => up.userId === user.user.userId
|
(up) => up.userId === user.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!existingUserPolicy) {
|
if (!existingUserPolicy) {
|
||||||
await trx.insert(userPolicies).values({
|
await trx.insert(userPolicies).values({
|
||||||
userId: user.user.userId,
|
userId: user.userId,
|
||||||
resourcePolicyId: policyId
|
resourcePolicyId: policyId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
userPolicies,
|
userPolicies,
|
||||||
users
|
users
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq, and, or } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { Config, ResourcePolicyData } from "./types";
|
import { Config, ResourcePolicyData } from "./types";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { getUniqueResourcePolicyName } from "@server/db/names";
|
import { getUniqueResourcePolicyName } from "@server/db/names";
|
||||||
@@ -22,6 +22,7 @@ import { idpExistsForOrg } from "@server/lib/idp/idpExistsForOrg";
|
|||||||
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
|
||||||
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
import { tierMatrix } from "../billing/tierMatrix";
|
import { tierMatrix } from "../billing/tierMatrix";
|
||||||
|
import { findOrgUserByIdentifier } from "./findOrgUser";
|
||||||
|
|
||||||
export type ResourcePoliciesResults = {
|
export type ResourcePoliciesResults = {
|
||||||
resourcePolicyId: number;
|
resourcePolicyId: number;
|
||||||
@@ -466,17 +467,7 @@ async function syncUserPolicies(
|
|||||||
.where(eq(userPolicies.resourcePolicyId, policyId));
|
.where(eq(userPolicies.resourcePolicyId, policyId));
|
||||||
|
|
||||||
for (const username of ssoUsers) {
|
for (const username of ssoUsers) {
|
||||||
const [user] = await trx
|
const user = await findOrgUserByIdentifier(trx, orgId, username);
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(eq(users.username, username), eq(users.email, username)),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -486,12 +477,12 @@ async function syncUserPolicies(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const alreadyExists = existingUserPolicies.some(
|
const alreadyExists = existingUserPolicies.some(
|
||||||
(up) => up.userId === user.user.userId
|
(up) => up.userId === user.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!alreadyExists) {
|
if (!alreadyExists) {
|
||||||
await trx.insert(userPolicies).values({
|
await trx.insert(userPolicies).values({
|
||||||
userId: user.user.userId,
|
userId: user.userId,
|
||||||
resourcePolicyId: policyId
|
resourcePolicyId: policyId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -536,17 +527,7 @@ async function addUserPolicies(
|
|||||||
trx: Transaction
|
trx: Transaction
|
||||||
) {
|
) {
|
||||||
for (const username of ssoUsers) {
|
for (const username of ssoUsers) {
|
||||||
const [user] = await trx
|
const user = await findOrgUserByIdentifier(trx, orgId, username);
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.innerJoin(userOrgs, eq(users.userId, userOrgs.userId))
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
or(eq(users.username, username), eq(users.email, username)),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -556,7 +537,7 @@ async function addUserPolicies(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await trx.insert(userPolicies).values({
|
await trx.insert(userPolicies).values({
|
||||||
userId: user.user.userId,
|
userId: user.userId,
|
||||||
resourcePolicyId: policyId
|
resourcePolicyId: policyId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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 (
|
||||||
|
|||||||
+50
-1
@@ -3,6 +3,8 @@ import config from "./config";
|
|||||||
import { getHostMeta } from "./hostMeta";
|
import { getHostMeta } from "./hostMeta";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import {
|
import {
|
||||||
|
aiProviders,
|
||||||
|
aiUsageRecords,
|
||||||
alertRules,
|
alertRules,
|
||||||
apiKeys,
|
apiKeys,
|
||||||
blueprints,
|
blueprints,
|
||||||
@@ -11,7 +13,16 @@ import {
|
|||||||
siteResources
|
siteResources
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { sites, users, orgs, resources, clients, idp } from "@server/db";
|
import { sites, users, orgs, resources, clients, idp } from "@server/db";
|
||||||
import { eq, count, notInArray, and, isNotNull, isNull } from "drizzle-orm";
|
import {
|
||||||
|
eq,
|
||||||
|
count,
|
||||||
|
countDistinct,
|
||||||
|
notInArray,
|
||||||
|
and,
|
||||||
|
isNotNull,
|
||||||
|
isNull,
|
||||||
|
gte
|
||||||
|
} from "drizzle-orm";
|
||||||
import { APP_VERSION } from "./consts";
|
import { APP_VERSION } from "./consts";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import { UserType } from "@server/types/UserTypes";
|
import { UserType } from "@server/types/UserTypes";
|
||||||
@@ -172,6 +183,25 @@ class TelemetryClient {
|
|||||||
.select({ count: count() })
|
.select({ count: count() })
|
||||||
.from(blueprints);
|
.from(blueprints);
|
||||||
|
|
||||||
|
const [aiProvidersCount] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(aiProviders);
|
||||||
|
const [orgsWithAiProviders] = await db
|
||||||
|
.select({ count: countDistinct(aiProviders.orgId) })
|
||||||
|
.from(aiProviders);
|
||||||
|
|
||||||
|
const usageWindowStart =
|
||||||
|
Math.floor(Date.now() / 1000) -
|
||||||
|
this.collectionIntervalDays * 24 * 60 * 60;
|
||||||
|
const [aiUsageRecordsRecent] = await db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(aiUsageRecords)
|
||||||
|
.where(gte(aiUsageRecords.createdAt, usageWindowStart));
|
||||||
|
const [orgsWithRecentAiUsage] = await db
|
||||||
|
.select({ count: countDistinct(aiUsageRecords.orgId) })
|
||||||
|
.from(aiUsageRecords)
|
||||||
|
.where(gte(aiUsageRecords.createdAt, usageWindowStart));
|
||||||
|
|
||||||
const supporterKey = config.getSupporterData();
|
const supporterKey = config.getSupporterData();
|
||||||
|
|
||||||
const allPrivateResources = await db.select().from(siteResources);
|
const allPrivateResources = await db.select().from(siteResources);
|
||||||
@@ -182,6 +212,7 @@ class TelemetryClient {
|
|||||||
let numPrivResourceCidr = 0;
|
let numPrivResourceCidr = 0;
|
||||||
let numPrivResourceHttp = 0;
|
let numPrivResourceHttp = 0;
|
||||||
let numPrivResourceSsh = 0;
|
let numPrivResourceSsh = 0;
|
||||||
|
let numPrivResourceInference = 0;
|
||||||
for (const res of allPrivateResources) {
|
for (const res of allPrivateResources) {
|
||||||
if (res.mode === "host") {
|
if (res.mode === "host") {
|
||||||
numPrivResourceHosts += 1;
|
numPrivResourceHosts += 1;
|
||||||
@@ -191,6 +222,8 @@ class TelemetryClient {
|
|||||||
numPrivResourceHttp += 1;
|
numPrivResourceHttp += 1;
|
||||||
} else if (res.mode === "ssh") {
|
} else if (res.mode === "ssh") {
|
||||||
numPrivResourceSsh += 1;
|
numPrivResourceSsh += 1;
|
||||||
|
} else if (res.mode === "inference") {
|
||||||
|
numPrivResourceInference += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.alias) {
|
if (res.alias) {
|
||||||
@@ -211,6 +244,11 @@ class TelemetryClient {
|
|||||||
numPrivateResourceCidr: numPrivResourceCidr,
|
numPrivateResourceCidr: numPrivResourceCidr,
|
||||||
numPrivateResourceHttp: numPrivResourceHttp,
|
numPrivateResourceHttp: numPrivResourceHttp,
|
||||||
numPrivateResourceSsh: numPrivResourceSsh,
|
numPrivateResourceSsh: numPrivResourceSsh,
|
||||||
|
numPrivateResourceInference: numPrivResourceInference,
|
||||||
|
numAiProviders: aiProvidersCount.count,
|
||||||
|
numOrgsWithAiProviders: orgsWithAiProviders.count,
|
||||||
|
numAiUsageRecordsRecent: aiUsageRecordsRecent.count,
|
||||||
|
numOrgsWithRecentAiUsage: orgsWithRecentAiUsage.count,
|
||||||
numAlertRules: numAlertRules.count,
|
numAlertRules: numAlertRules.count,
|
||||||
numUserDevices: userDevicesCount.count,
|
numUserDevices: userDevicesCount.count,
|
||||||
numMachineClients: machineClients.count,
|
numMachineClients: machineClients.count,
|
||||||
@@ -323,6 +361,17 @@ class TelemetryClient {
|
|||||||
num_resources_non_http: stats.resources.filter(
|
num_resources_non_http: stats.resources.filter(
|
||||||
(r) => r.mode !== "http"
|
(r) => r.mode !== "http"
|
||||||
).length,
|
).length,
|
||||||
|
num_resources_ai_gateway: stats.resources.filter(
|
||||||
|
(r) => r.mode === "inference"
|
||||||
|
).length,
|
||||||
|
num_private_resources_ai_gateway:
|
||||||
|
stats.numPrivateResourceInference,
|
||||||
|
num_ai_providers: stats.numAiProviders,
|
||||||
|
num_orgs_with_ai_providers: stats.numOrgsWithAiProviders,
|
||||||
|
num_ai_usage_records_recent:
|
||||||
|
stats.numAiUsageRecordsRecent,
|
||||||
|
num_orgs_with_recent_ai_usage:
|
||||||
|
stats.numOrgsWithRecentAiUsage,
|
||||||
num_newt_sites: stats.sites.filter((s) => s.type === "newt")
|
num_newt_sites: stats.sites.filter((s) => s.type === "newt")
|
||||||
.length,
|
.length,
|
||||||
num_local_sites: stats.sites.filter(
|
num_local_sites: stats.sites.filter(
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,6 +46,7 @@ import { AxiosResponse } from "axios";
|
|||||||
import { ListRolesResponse } from "@server/routers/role";
|
import { ListRolesResponse } from "@server/routers/role";
|
||||||
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
|
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
|
||||||
import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription";
|
import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription";
|
||||||
|
import IdpIdentifierChangeDialog from "@app/components/IdpIdentifierChangeDialog";
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import {
|
import {
|
||||||
@@ -75,6 +76,12 @@ export default function GeneralPage() {
|
|||||||
>([createMappingBuilderRule()]);
|
>([createMappingBuilderRule()]);
|
||||||
const [rawRoleExpression, setRawRoleExpression] = useState("");
|
const [rawRoleExpression, setRawRoleExpression] = useState("");
|
||||||
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
|
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
|
||||||
|
const [originalIdentifierPath, setOriginalIdentifierPath] = useState("");
|
||||||
|
const [identifierConfirmOpen, setIdentifierConfirmOpen] = useState(false);
|
||||||
|
const [pendingPayload, setPendingPayload] = useState<Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
const dashboardRedirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
|
const dashboardRedirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
|
||||||
const [redirectUrl, setRedirectUrl] = useState(
|
const [redirectUrl, setRedirectUrl] = useState(
|
||||||
@@ -184,6 +191,9 @@ export default function GeneralPage() {
|
|||||||
const data = res.data.data;
|
const data = res.data.data;
|
||||||
const roleMapping = data.idpOrg.roleMapping;
|
const roleMapping = data.idpOrg.roleMapping;
|
||||||
const idpVariant = data.idpOidcConfig?.variant || "oidc";
|
const idpVariant = data.idpOidcConfig?.variant || "oidc";
|
||||||
|
setOriginalIdentifierPath(
|
||||||
|
data.idpOidcConfig?.identifierPath ?? "sub"
|
||||||
|
);
|
||||||
setRedirectUrl(res.data.data.redirectUrl);
|
setRedirectUrl(res.data.data.redirectUrl);
|
||||||
|
|
||||||
// Set the variant
|
// Set the variant
|
||||||
@@ -378,18 +388,56 @@ export default function GeneralPage() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await api.post(
|
const nextIdentifierPath =
|
||||||
`/org/${orgId}/idp/${idpId}/oidc`,
|
variant === "oidc"
|
||||||
payload
|
? (data as OidcFormValues).identifierPath
|
||||||
);
|
: undefined;
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (
|
||||||
toast({
|
typeof nextIdentifierPath === "string" &&
|
||||||
title: t("success"),
|
nextIdentifierPath !== originalIdentifierPath
|
||||||
description: t("idpUpdatedDescription")
|
) {
|
||||||
});
|
setPendingPayload(payload);
|
||||||
router.refresh();
|
setIdentifierConfirmOpen(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await persistIdp(payload);
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: formatAxiosError(e),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistIdp(payload: Record<string, unknown>) {
|
||||||
|
const res = await api.post(`/org/${orgId}/idp/${idpId}/oidc`, payload);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
if (typeof payload.identifierPath === "string") {
|
||||||
|
setOriginalIdentifierPath(payload.identifierPath);
|
||||||
|
}
|
||||||
|
toast({
|
||||||
|
title: t("success"),
|
||||||
|
description: t("idpUpdatedDescription")
|
||||||
|
});
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmIdentifierChange() {
|
||||||
|
if (!pendingPayload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await persistIdp(pendingPayload);
|
||||||
|
setPendingPayload(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
title: t("error"),
|
title: t("error"),
|
||||||
@@ -407,6 +455,16 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<IdpIdentifierChangeDialog
|
||||||
|
open={identifierConfirmOpen}
|
||||||
|
setOpen={(open) => {
|
||||||
|
setIdentifierConfirmOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setPendingPayload(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onConfirm={confirmIdentifierChange}
|
||||||
|
/>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ import {
|
|||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
|
||||||
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import {
|
import {
|
||||||
createBrowserGatewayTargetFormSchema,
|
createBrowserGatewayTargetFormSchema,
|
||||||
@@ -59,7 +57,6 @@ import {
|
|||||||
selectedSiteSchema,
|
selectedSiteSchema,
|
||||||
type SshSettingsFormValues
|
type SshSettingsFormValues
|
||||||
} from "@app/lib/browserGatewayTargetFormSchema";
|
} from "@app/lib/browserGatewayTargetFormSchema";
|
||||||
import { DockerManager, DockerState } from "@app/lib/docker";
|
|
||||||
import { orgQueries } from "@app/lib/queries";
|
import { orgQueries } from "@app/lib/queries";
|
||||||
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
@@ -328,19 +325,20 @@ export default function Page() {
|
|||||||
const rawResourcesAllowed =
|
const rawResourcesAllowed =
|
||||||
env.flags.allowRawResources &&
|
env.flags.allowRawResources &&
|
||||||
(build !== "saas" || remoteExitNodes.length > 0);
|
(build !== "saas" || remoteExitNodes.length > 0);
|
||||||
const enterpriseModesAllowed =
|
|
||||||
!env.flags.disableEnterpriseFeatures;
|
|
||||||
|
|
||||||
const availableTypes = useMemo((): NewResourceType[] => {
|
const availableTypes = useMemo((): NewResourceType[] => {
|
||||||
const base: NewResourceType[] = ["http", "inference"];
|
const base: NewResourceType[] = [
|
||||||
if (enterpriseModesAllowed) {
|
"http",
|
||||||
base.push("ssh", "rdp", "vnc");
|
"inference",
|
||||||
}
|
"ssh",
|
||||||
|
"rdp",
|
||||||
|
"vnc"
|
||||||
|
];
|
||||||
if (rawResourcesAllowed) {
|
if (rawResourcesAllowed) {
|
||||||
base.push("tcp", "udp");
|
base.push("tcp", "udp");
|
||||||
}
|
}
|
||||||
return base;
|
return base;
|
||||||
}, [enterpriseModesAllowed, rawResourcesAllowed]);
|
}, [rawResourcesAllowed]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!availableTypes.includes(resourceType)) {
|
if (!availableTypes.includes(resourceType)) {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import {
|
|||||||
} from "@app/components/InfoSection";
|
} from "@app/components/InfoSection";
|
||||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||||
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
import IdpTypeBadge from "@app/components/IdpTypeBadge";
|
||||||
|
import IdpIdentifierChangeDialog from "@app/components/IdpIdentifierChangeDialog";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
@@ -51,6 +52,12 @@ export default function GeneralPage() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [initialLoading, setInitialLoading] = useState(true);
|
const [initialLoading, setInitialLoading] = useState(true);
|
||||||
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
|
const [variant, setVariant] = useState<"oidc" | "google" | "azure">("oidc");
|
||||||
|
const [originalIdentifierPath, setOriginalIdentifierPath] = useState("");
|
||||||
|
const [identifierConfirmOpen, setIdentifierConfirmOpen] = useState(false);
|
||||||
|
const [pendingPayload, setPendingPayload] = useState<Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
const redirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
|
const redirectUrl = `${env.app.dashboardUrl}/auth/idp/${idpId}/oidc/callback`;
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -141,6 +148,9 @@ export default function GeneralPage() {
|
|||||||
| "google"
|
| "google"
|
||||||
| "azure") || "oidc";
|
| "azure") || "oidc";
|
||||||
setVariant(idpVariant);
|
setVariant(idpVariant);
|
||||||
|
setOriginalIdentifierPath(
|
||||||
|
data.idpOidcConfig?.identifierPath ?? "sub"
|
||||||
|
);
|
||||||
|
|
||||||
let tenantId = "";
|
let tenantId = "";
|
||||||
if (idpVariant === "azure" && data.idpOidcConfig?.authUrl) {
|
if (idpVariant === "azure" && data.idpOidcConfig?.authUrl) {
|
||||||
@@ -258,15 +268,56 @@ export default function GeneralPage() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await api.post(`/idp/${idpId}/oidc`, payload);
|
const nextIdentifierPath =
|
||||||
|
variant === "oidc"
|
||||||
|
? (data as OidcFormValues).identifierPath
|
||||||
|
: undefined;
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (
|
||||||
toast({
|
typeof nextIdentifierPath === "string" &&
|
||||||
title: t("success"),
|
nextIdentifierPath !== originalIdentifierPath
|
||||||
description: t("idpUpdatedDescription")
|
) {
|
||||||
});
|
setPendingPayload(payload);
|
||||||
router.refresh();
|
setIdentifierConfirmOpen(true);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await persistIdp(payload);
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: formatAxiosError(e),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistIdp(payload: Record<string, unknown>) {
|
||||||
|
const res = await api.post(`/idp/${idpId}/oidc`, payload);
|
||||||
|
|
||||||
|
if (res.status === 200) {
|
||||||
|
if (typeof payload.identifierPath === "string") {
|
||||||
|
setOriginalIdentifierPath(payload.identifierPath);
|
||||||
|
}
|
||||||
|
toast({
|
||||||
|
title: t("success"),
|
||||||
|
description: t("idpUpdatedDescription")
|
||||||
|
});
|
||||||
|
router.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmIdentifierChange() {
|
||||||
|
if (!pendingPayload) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
await persistIdp(pendingPayload);
|
||||||
|
setPendingPayload(null);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
title: t("error"),
|
title: t("error"),
|
||||||
@@ -284,6 +335,16 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<IdpIdentifierChangeDialog
|
||||||
|
open={identifierConfirmOpen}
|
||||||
|
setOpen={(open) => {
|
||||||
|
setIdentifierConfirmOpen(open);
|
||||||
|
if (!open) {
|
||||||
|
setPendingPayload(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onConfirm={confirmIdentifierChange}
|
||||||
|
/>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<SettingsSectionHeader>
|
<SettingsSectionHeader>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
type IdpIdentifierChangeDialogProps = {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
onConfirm: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function IdpIdentifierChangeDialog({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
onConfirm
|
||||||
|
}: IdpIdentifierChangeDialogProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={open}
|
||||||
|
setOpen={setOpen}
|
||||||
|
dialog={
|
||||||
|
<div className="space-y-2">
|
||||||
|
<p>{t("idpIdentifierChangeDescription")}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
buttonText={t("saveGeneralSettings")}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
string={t("idpIdentifierChangeConfirmMessage")}
|
||||||
|
title={t("idpIdentifierChangeTitle")}
|
||||||
|
warningText={t("idpIdentifierChangeWarningText")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
+30
-22
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user