mirror of
https://github.com/fosrl/pangolin.git
synced 2026-09-02 17:29:43 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba3b59e8c6 |
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
# FROM node:24.18.1-slim AS base
|
# FROM node:24.18.1-slim AS base
|
||||||
FROM public.ecr.aws/docker/library/node:24.18.1-slim AS base
|
FROM public.ecr.aws/docker/library/node:26.8.1-slim AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ FROM base AS builder
|
|||||||
RUN npm ci --omit=dev
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
# FROM node:24.18.1-slim AS runner
|
# FROM node:24.18.1-slim AS runner
|
||||||
FROM public.ecr.aws/docker/library/node:24.18.1-slim AS runner
|
FROM public.ecr.aws/docker/library/node:26.8.1-slim AS runner
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM node:24.18.1-alpine
|
FROM node:26.8.1-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -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.7.1"
|
version: "v1.4.4"
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: "INFO"
|
level: "INFO"
|
||||||
|
|||||||
+1
-9
@@ -1176,10 +1176,6 @@
|
|||||||
"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)",
|
||||||
@@ -1577,8 +1573,6 @@
|
|||||||
"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.",
|
||||||
@@ -2602,7 +2596,6 @@
|
|||||||
"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}",
|
||||||
@@ -2717,7 +2710,6 @@
|
|||||||
"healthScheme": "Method",
|
"healthScheme": "Method",
|
||||||
"healthSelectScheme": "Select Method",
|
"healthSelectScheme": "Select Method",
|
||||||
"healthCheckPortInvalid": "Port must be between 1 and 65535",
|
"healthCheckPortInvalid": "Port must be between 1 and 65535",
|
||||||
"healthCheckHostnameInvalid": "Hostname must not contain whitespace",
|
|
||||||
"healthCheckPath": "Path",
|
"healthCheckPath": "Path",
|
||||||
"healthHostname": "IP / Host",
|
"healthHostname": "IP / Host",
|
||||||
"healthPort": "Port",
|
"healthPort": "Port",
|
||||||
@@ -3001,7 +2993,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 prefer to connect through this remote exit node.",
|
"remoteExitNodeNetworkingLabelsDescription": "Sites with these labels will be enforced 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",
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ import {
|
|||||||
sites,
|
sites,
|
||||||
clients,
|
clients,
|
||||||
sessions,
|
sessions,
|
||||||
labels,
|
labels
|
||||||
aiProviders,
|
|
||||||
virtualApiKeys
|
|
||||||
} from "./schema";
|
} from "./schema";
|
||||||
|
|
||||||
export const dnsChallenge = pgTable("dnsChallenges", {
|
export const dnsChallenge = pgTable("dnsChallenges", {
|
||||||
@@ -616,87 +614,6 @@ export const trialNotifications = pgTable("trialNotifications", {
|
|||||||
sentAt: bigint("sentAt", { mode: "number" }).notNull()
|
sentAt: bigint("sentAt", { mode: "number" }).notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Logs the aggregated prompt + response for a single AI gateway request, for
|
|
||||||
// session replay. One row per request (not per streaming chunk). `sessionId`
|
|
||||||
// is a fresh random id per row for now - no cross-request correlation yet,
|
|
||||||
// but the column exists so a future pass can link multiple rows into a real
|
|
||||||
// multi-turn session.
|
|
||||||
export const aiSessionLog = pgTable(
|
|
||||||
"aiSessionLog",
|
|
||||||
{
|
|
||||||
id: serial("id").primaryKey(),
|
|
||||||
sessionId: varchar("sessionId").notNull(),
|
|
||||||
orgId: varchar("orgId").references(() => orgs.orgId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
}),
|
|
||||||
providerId: integer("providerId").references(
|
|
||||||
() => aiProviders.providerId,
|
|
||||||
{ onDelete: "set null" }
|
|
||||||
),
|
|
||||||
capability: varchar("capability").notNull(),
|
|
||||||
resourceId: integer("resourceId").references(
|
|
||||||
() => resources.resourceId,
|
|
||||||
{ onDelete: "set null" }
|
|
||||||
),
|
|
||||||
siteResourceId: integer("siteResourceId").references(
|
|
||||||
() => siteResources.siteResourceId,
|
|
||||||
{ onDelete: "set null" }
|
|
||||||
),
|
|
||||||
userId: varchar("userId").references(() => users.userId, {
|
|
||||||
onDelete: "set null"
|
|
||||||
}),
|
|
||||||
virtualApiKeyId: varchar("virtualApiKeyId").references(
|
|
||||||
() => virtualApiKeys.virtualApiKeyId,
|
|
||||||
{ onDelete: "set null" }
|
|
||||||
),
|
|
||||||
requestedModel: varchar("requestedModel"),
|
|
||||||
isStream: boolean("isStream").notNull().default(false),
|
|
||||||
requestBody: text("requestBody"),
|
|
||||||
responseBody: text("responseBody"),
|
|
||||||
// Capability-agnostic message transcript (JSON-encoded
|
|
||||||
// NormalizedAiMessage[] from server/lib/aiMessageNormalization.ts),
|
|
||||||
// computed at write time so search/display never need per-capability
|
|
||||||
// parsing logic. Null when normalization couldn't recognize the
|
|
||||||
// shape - callers fall back to requestBody/responseBody.
|
|
||||||
normalizedRequest: text("normalizedRequest"),
|
|
||||||
normalizedResponse: text("normalizedResponse"),
|
|
||||||
// True if any of the request/response (raw or normalized) fields
|
|
||||||
// were cut short at AI_SESSION_LOG_MAX_BODY_CHARS before storage.
|
|
||||||
truncated: boolean("truncated").notNull().default(false),
|
|
||||||
statusCode: integer("statusCode"),
|
|
||||||
createdAt: bigint("createdAt", { mode: "number" }).notNull() // epoch seconds
|
|
||||||
},
|
|
||||||
(t) => [
|
|
||||||
index("idx_ai_session_log_org_created").on(t.orgId, t.createdAt),
|
|
||||||
index("idx_ai_session_log_org_provider_created").on(
|
|
||||||
t.orgId,
|
|
||||||
t.providerId,
|
|
||||||
t.createdAt
|
|
||||||
),
|
|
||||||
index("idx_ai_session_log_org_resource_created").on(
|
|
||||||
t.orgId,
|
|
||||||
t.resourceId,
|
|
||||||
t.createdAt
|
|
||||||
),
|
|
||||||
index("idx_ai_session_log_org_site_resource_created").on(
|
|
||||||
t.orgId,
|
|
||||||
t.siteResourceId,
|
|
||||||
t.createdAt
|
|
||||||
),
|
|
||||||
index("idx_ai_session_log_org_user_created").on(
|
|
||||||
t.orgId,
|
|
||||||
t.userId,
|
|
||||||
t.createdAt
|
|
||||||
),
|
|
||||||
index("idx_ai_session_log_org_virtual_api_key_created").on(
|
|
||||||
t.orgId,
|
|
||||||
t.virtualApiKeyId,
|
|
||||||
t.createdAt
|
|
||||||
),
|
|
||||||
index("idx_ai_session_log_session").on(t.sessionId)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
export type Approval = InferSelectModel<typeof approvals>;
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
@@ -743,4 +660,3 @@ export type AlertEmailRecipients = InferSelectModel<
|
|||||||
>;
|
>;
|
||||||
export type AlertWebhookActions = InferSelectModel<typeof alertWebhookActions>;
|
export type AlertWebhookActions = InferSelectModel<typeof alertWebhookActions>;
|
||||||
export type TrialNotification = InferSelectModel<typeof trialNotifications>;
|
export type TrialNotification = InferSelectModel<typeof trialNotifications>;
|
||||||
export type AiSessionLog = InferSelectModel<typeof aiSessionLog>;
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { aiSessionLog } from "@server/db/sqlite";
|
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import { InferSelectModel, sql } from "drizzle-orm";
|
import { InferSelectModel, sql } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
@@ -1959,6 +1958,87 @@ export const aiBudgetBreachEvents = pgTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Logs the aggregated prompt + response for a single AI gateway request, for
|
||||||
|
// session replay. One row per request (not per streaming chunk). `sessionId`
|
||||||
|
// is a fresh random id per row for now - no cross-request correlation yet,
|
||||||
|
// but the column exists so a future pass can link multiple rows into a real
|
||||||
|
// multi-turn session.
|
||||||
|
export const aiSessionLog = pgTable(
|
||||||
|
"aiSessionLog",
|
||||||
|
{
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
sessionId: varchar("sessionId").notNull(),
|
||||||
|
orgId: varchar("orgId").references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
providerId: integer("providerId").references(
|
||||||
|
() => aiProviders.providerId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
capability: varchar("capability").notNull(),
|
||||||
|
resourceId: integer("resourceId").references(
|
||||||
|
() => resources.resourceId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
siteResourceId: integer("siteResourceId").references(
|
||||||
|
() => siteResources.siteResourceId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
userId: varchar("userId").references(() => users.userId, {
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
virtualApiKeyId: varchar("virtualApiKeyId").references(
|
||||||
|
() => virtualApiKeys.virtualApiKeyId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
requestedModel: varchar("requestedModel"),
|
||||||
|
isStream: boolean("isStream").notNull().default(false),
|
||||||
|
requestBody: text("requestBody"),
|
||||||
|
responseBody: text("responseBody"),
|
||||||
|
// Capability-agnostic message transcript (JSON-encoded
|
||||||
|
// NormalizedAiMessage[] from server/lib/aiMessageNormalization.ts),
|
||||||
|
// computed at write time so search/display never need per-capability
|
||||||
|
// parsing logic. Null when normalization couldn't recognize the
|
||||||
|
// shape - callers fall back to requestBody/responseBody.
|
||||||
|
normalizedRequest: text("normalizedRequest"),
|
||||||
|
normalizedResponse: text("normalizedResponse"),
|
||||||
|
// True if any of the request/response (raw or normalized) fields
|
||||||
|
// were cut short at AI_SESSION_LOG_MAX_BODY_CHARS before storage.
|
||||||
|
truncated: boolean("truncated").notNull().default(false),
|
||||||
|
statusCode: integer("statusCode"),
|
||||||
|
createdAt: bigint("createdAt", { mode: "number" }).notNull() // epoch seconds
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_ai_session_log_org_created").on(t.orgId, t.createdAt),
|
||||||
|
index("idx_ai_session_log_org_provider_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.providerId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_resource_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.resourceId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_site_resource_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.siteResourceId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_user_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.userId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_virtual_api_key_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.virtualApiKeyId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_session").on(t.sessionId)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const certificates = pgTable("certificates", {
|
export const certificates = pgTable("certificates", {
|
||||||
certId: serial("certId").primaryKey(),
|
certId: serial("certId").primaryKey(),
|
||||||
domain: varchar("domain", { length: 255 }).notNull().unique(),
|
domain: varchar("domain", { length: 255 }).notNull().unique(),
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
uniqueIndex
|
uniqueIndex
|
||||||
} from "drizzle-orm/sqlite-core";
|
} from "drizzle-orm/sqlite-core";
|
||||||
import {
|
import {
|
||||||
aiProviders,
|
|
||||||
clients,
|
clients,
|
||||||
domains,
|
domains,
|
||||||
exitNodes,
|
exitNodes,
|
||||||
@@ -21,8 +20,7 @@ import {
|
|||||||
siteResources,
|
siteResources,
|
||||||
sites,
|
sites,
|
||||||
targetHealthCheck,
|
targetHealthCheck,
|
||||||
users,
|
users
|
||||||
virtualApiKeys
|
|
||||||
} from "./schema";
|
} from "./schema";
|
||||||
|
|
||||||
export const dnsChallenge = sqliteTable("dnsChallenges", {
|
export const dnsChallenge = sqliteTable("dnsChallenges", {
|
||||||
@@ -611,91 +609,6 @@ export const trialNotifications = sqliteTable("trialNotifications", {
|
|||||||
sentAt: integer("sentAt").notNull()
|
sentAt: integer("sentAt").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Logs the aggregated prompt + response for a single AI gateway request, for
|
|
||||||
// session replay. One row per request (not per streaming chunk). `sessionId`
|
|
||||||
// is a fresh random id per row for now - no cross-request correlation yet,
|
|
||||||
// but the column exists so a future pass can link multiple rows into a real
|
|
||||||
// multi-turn session.
|
|
||||||
export const aiSessionLog = sqliteTable(
|
|
||||||
"aiSessionLog",
|
|
||||||
{
|
|
||||||
id: integer("id").primaryKey({ autoIncrement: true }),
|
|
||||||
sessionId: text("sessionId").notNull(),
|
|
||||||
orgId: text("orgId").references(() => orgs.orgId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
}),
|
|
||||||
providerId: integer("providerId").references(
|
|
||||||
() => aiProviders.providerId,
|
|
||||||
{ onDelete: "set null" }
|
|
||||||
),
|
|
||||||
capability: text("capability").notNull(),
|
|
||||||
resourceId: integer("resourceId").references(
|
|
||||||
() => resources.resourceId,
|
|
||||||
{ onDelete: "set null" }
|
|
||||||
),
|
|
||||||
siteResourceId: integer("siteResourceId").references(
|
|
||||||
() => siteResources.siteResourceId,
|
|
||||||
{ onDelete: "set null" }
|
|
||||||
),
|
|
||||||
userId: text("userId").references(() => users.userId, {
|
|
||||||
onDelete: "set null"
|
|
||||||
}),
|
|
||||||
virtualApiKeyId: text("virtualApiKeyId").references(
|
|
||||||
() => virtualApiKeys.virtualApiKeyId,
|
|
||||||
{ onDelete: "set null" }
|
|
||||||
),
|
|
||||||
requestedModel: text("requestedModel"),
|
|
||||||
isStream: integer("isStream", { mode: "boolean" })
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
requestBody: text("requestBody"),
|
|
||||||
responseBody: text("responseBody"),
|
|
||||||
// Capability-agnostic message transcript (JSON-encoded
|
|
||||||
// NormalizedAiMessage[] from server/lib/aiMessageNormalization.ts),
|
|
||||||
// computed at write time so search/display never need per-capability
|
|
||||||
// parsing logic. Null when normalization couldn't recognize the
|
|
||||||
// shape - callers fall back to requestBody/responseBody.
|
|
||||||
normalizedRequest: text("normalizedRequest"),
|
|
||||||
normalizedResponse: text("normalizedResponse"),
|
|
||||||
// True if any of the request/response (raw or normalized) fields
|
|
||||||
// were cut short at AI_SESSION_LOG_MAX_BODY_CHARS before storage.
|
|
||||||
truncated: integer("truncated", { mode: "boolean" })
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
statusCode: integer("statusCode"),
|
|
||||||
createdAt: integer("createdAt").notNull() // epoch seconds
|
|
||||||
},
|
|
||||||
(t) => [
|
|
||||||
index("idx_ai_session_log_org_created").on(t.orgId, t.createdAt),
|
|
||||||
index("idx_ai_session_log_org_provider_created").on(
|
|
||||||
t.orgId,
|
|
||||||
t.providerId,
|
|
||||||
t.createdAt
|
|
||||||
),
|
|
||||||
index("idx_ai_session_log_org_resource_created").on(
|
|
||||||
t.orgId,
|
|
||||||
t.resourceId,
|
|
||||||
t.createdAt
|
|
||||||
),
|
|
||||||
index("idx_ai_session_log_org_site_resource_created").on(
|
|
||||||
t.orgId,
|
|
||||||
t.siteResourceId,
|
|
||||||
t.createdAt
|
|
||||||
),
|
|
||||||
index("idx_ai_session_log_org_user_created").on(
|
|
||||||
t.orgId,
|
|
||||||
t.userId,
|
|
||||||
t.createdAt
|
|
||||||
),
|
|
||||||
index("idx_ai_session_log_org_virtual_api_key_created").on(
|
|
||||||
t.orgId,
|
|
||||||
t.virtualApiKeyId,
|
|
||||||
t.createdAt
|
|
||||||
),
|
|
||||||
index("idx_ai_session_log_session").on(t.sessionId)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
export type Approval = InferSelectModel<typeof approvals>;
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
@@ -734,4 +647,3 @@ export type AlertEmailAction = InferSelectModel<typeof alertEmailActions>;
|
|||||||
export type AlertEmailRecipient = InferSelectModel<typeof alertEmailRecipients>;
|
export type AlertEmailRecipient = InferSelectModel<typeof alertEmailRecipients>;
|
||||||
export type AlertWebhookAction = InferSelectModel<typeof alertWebhookActions>;
|
export type AlertWebhookAction = InferSelectModel<typeof alertWebhookActions>;
|
||||||
export type TrialNotification = InferSelectModel<typeof trialNotifications>;
|
export type TrialNotification = InferSelectModel<typeof trialNotifications>;
|
||||||
export type AiSessionLog = InferSelectModel<typeof aiSessionLog>;
|
|
||||||
|
|||||||
@@ -147,7 +147,9 @@ export const sites = sqliteTable(
|
|||||||
.$type<"pending" | "approved">()
|
.$type<"pending" | "approved">()
|
||||||
.default("approved")
|
.default("approved")
|
||||||
},
|
},
|
||||||
(table) => [index("idx_sites_orgId").on(table.orgId)]
|
(table) => [
|
||||||
|
index("idx_sites_orgId").on(table.orgId)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const resources = sqliteTable(
|
export const resources = sqliteTable(
|
||||||
@@ -190,9 +192,7 @@ export const resources = sqliteTable(
|
|||||||
mode: "boolean"
|
mode: "boolean"
|
||||||
}),
|
}),
|
||||||
applyRules: integer("applyRules", { mode: "boolean" }),
|
applyRules: integer("applyRules", { mode: "boolean" }),
|
||||||
enabled: integer("enabled", { mode: "boolean" })
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
.notNull()
|
|
||||||
.default(true),
|
|
||||||
stickySession: integer("stickySession", { mode: "boolean" })
|
stickySession: integer("stickySession", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
@@ -220,14 +220,10 @@ export const resources = sqliteTable(
|
|||||||
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
|
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
|
||||||
postAuthPath: text("postAuthPath"),
|
postAuthPath: text("postAuthPath"),
|
||||||
health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown"
|
health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown"
|
||||||
wildcard: integer("wildcard", { mode: "boolean" })
|
wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false),
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
mode: text("mode")
|
mode: text("mode")
|
||||||
.default("http")
|
.default("http")
|
||||||
.$type<
|
.$type<"rdp" | "ssh" | "http" | "vnc" | "inference" | "tcp" | "udp">()
|
||||||
"rdp" | "ssh" | "http" | "vnc" | "inference" | "tcp" | "udp"
|
|
||||||
>()
|
|
||||||
.notNull(), // rdp, ssh, http, vnc, inference
|
.notNull(), // rdp, ssh, http, vnc, inference
|
||||||
pamMode: text("pamMode")
|
pamMode: text("pamMode")
|
||||||
.$type<"passthrough" | "push">()
|
.$type<"passthrough" | "push">()
|
||||||
@@ -240,7 +236,9 @@ export const resources = sqliteTable(
|
|||||||
.$type<"pending" | "approved">()
|
.$type<"pending" | "approved">()
|
||||||
.default("approved")
|
.default("approved")
|
||||||
},
|
},
|
||||||
(table) => [index("idx_resources_orgId").on(table.orgId)]
|
(table) => [
|
||||||
|
index("idx_resources_orgId").on(table.orgId)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const resourceAiProviders = sqliteTable(
|
export const resourceAiProviders = sqliteTable(
|
||||||
@@ -290,7 +288,9 @@ export const labels = sqliteTable(
|
|||||||
})
|
})
|
||||||
.notNull()
|
.notNull()
|
||||||
},
|
},
|
||||||
(table) => [index("idx_labels_orgId").on(table.orgId)]
|
(table) => [
|
||||||
|
index("idx_labels_orgId").on(table.orgId)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const launcherViews = sqliteTable("launcherViews", {
|
export const launcherViews = sqliteTable("launcherViews", {
|
||||||
@@ -409,9 +409,7 @@ export const targets = sqliteTable(
|
|||||||
method: text("method"),
|
method: text("method"),
|
||||||
port: integer("port").notNull(),
|
port: integer("port").notNull(),
|
||||||
internalPort: integer("internalPort"),
|
internalPort: integer("internalPort"),
|
||||||
enabled: integer("enabled", { mode: "boolean" })
|
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
||||||
.notNull()
|
|
||||||
.default(true),
|
|
||||||
path: text("path"),
|
path: text("path"),
|
||||||
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
pathMatchType: text("pathMatchType"), // exact, prefix, regex
|
||||||
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target
|
||||||
@@ -707,7 +705,9 @@ export const newts = sqliteTable(
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
(table) => [index("idx_newts_siteId").on(table.siteId)]
|
(table) => [
|
||||||
|
index("idx_newts_siteId").on(table.siteId)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const clients = sqliteTable(
|
export const clients = sqliteTable(
|
||||||
@@ -740,12 +740,8 @@ export const clients = sqliteTable(
|
|||||||
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
online: integer("online", { mode: "boolean" }).notNull().default(false),
|
||||||
// endpoint: text("endpoint"),
|
// endpoint: text("endpoint"),
|
||||||
lastHolePunch: integer("lastHolePunch"),
|
lastHolePunch: integer("lastHolePunch"),
|
||||||
archived: integer("archived", { mode: "boolean" })
|
archived: integer("archived", { mode: "boolean" }).notNull().default(false),
|
||||||
.notNull()
|
blocked: integer("blocked", { mode: "boolean" }).notNull().default(false),
|
||||||
.default(false),
|
|
||||||
blocked: integer("blocked", { mode: "boolean" })
|
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
approvalState: text("approvalState").$type<
|
approvalState: text("approvalState").$type<
|
||||||
"pending" | "approved" | "denied"
|
"pending" | "approved" | "denied"
|
||||||
>()
|
>()
|
||||||
@@ -799,11 +795,11 @@ export const olms = sqliteTable(
|
|||||||
// optionally tied to a user and in this case delete when the user deletes
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
archived: integer("archived", { mode: "boolean" })
|
archived: integer("archived", { mode: "boolean" }).notNull().default(false)
|
||||||
.notNull()
|
|
||||||
.default(false)
|
|
||||||
},
|
},
|
||||||
(table) => [index("idx_olms_userId").on(table.userId)]
|
(table) => [
|
||||||
|
index("idx_olms_userId").on(table.userId)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const currentFingerprint = sqliteTable("currentFingerprint", {
|
export const currentFingerprint = sqliteTable("currentFingerprint", {
|
||||||
@@ -979,7 +975,9 @@ export const sessions = sqliteTable(
|
|||||||
.notNull()
|
.notNull()
|
||||||
.default(false)
|
.default(false)
|
||||||
},
|
},
|
||||||
(table) => [index("idx_sessions_userId").on(table.userId)]
|
(table) => [
|
||||||
|
index("idx_sessions_userId").on(table.userId)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const newtSessions = sqliteTable("newtSession", {
|
export const newtSessions = sqliteTable("newtSession", {
|
||||||
@@ -1009,9 +1007,7 @@ export const userOrgs = sqliteTable(
|
|||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
isOwner: integer("isOwner", { mode: "boolean" })
|
isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false),
|
||||||
.notNull()
|
|
||||||
.default(false),
|
|
||||||
autoProvisioned: integer("autoProvisioned", {
|
autoProvisioned: integer("autoProvisioned", {
|
||||||
mode: "boolean"
|
mode: "boolean"
|
||||||
}).default(false),
|
}).default(false),
|
||||||
@@ -1066,12 +1062,14 @@ export const roles = sqliteTable(
|
|||||||
}).default(false),
|
}).default(false),
|
||||||
sshSudoMode: text("sshSudoMode").default("full"), // "none" | "full" | "commands"
|
sshSudoMode: text("sshSudoMode").default("full"), // "none" | "full" | "commands"
|
||||||
sshSudoCommands: text("sshSudoCommands").default("[]"),
|
sshSudoCommands: text("sshSudoCommands").default("[]"),
|
||||||
sshCreateHomeDir: integer("sshCreateHomeDir", {
|
sshCreateHomeDir: integer("sshCreateHomeDir", { mode: "boolean" }).default(
|
||||||
mode: "boolean"
|
true
|
||||||
}).default(true),
|
),
|
||||||
sshUnixGroups: text("sshUnixGroups").default("[]")
|
sshUnixGroups: text("sshUnixGroups").default("[]")
|
||||||
},
|
},
|
||||||
(table) => [index("idx_roles_orgId").on(table.orgId)]
|
(table) => [
|
||||||
|
index("idx_roles_orgId").on(table.orgId)
|
||||||
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
export const userOrgRoles = sqliteTable(
|
export const userOrgRoles = sqliteTable(
|
||||||
@@ -1999,6 +1997,91 @@ export const aiBudgetBreachEvents = sqliteTable(
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Logs the aggregated prompt + response for a single AI gateway request, for
|
||||||
|
// session replay. One row per request (not per streaming chunk). `sessionId`
|
||||||
|
// is a fresh random id per row for now - no cross-request correlation yet,
|
||||||
|
// but the column exists so a future pass can link multiple rows into a real
|
||||||
|
// multi-turn session.
|
||||||
|
export const aiSessionLog = sqliteTable(
|
||||||
|
"aiSessionLog",
|
||||||
|
{
|
||||||
|
id: integer("id").primaryKey({ autoIncrement: true }),
|
||||||
|
sessionId: text("sessionId").notNull(),
|
||||||
|
orgId: text("orgId").references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
providerId: integer("providerId").references(
|
||||||
|
() => aiProviders.providerId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
capability: text("capability").notNull(),
|
||||||
|
resourceId: integer("resourceId").references(
|
||||||
|
() => resources.resourceId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
siteResourceId: integer("siteResourceId").references(
|
||||||
|
() => siteResources.siteResourceId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
userId: text("userId").references(() => users.userId, {
|
||||||
|
onDelete: "set null"
|
||||||
|
}),
|
||||||
|
virtualApiKeyId: text("virtualApiKeyId").references(
|
||||||
|
() => virtualApiKeys.virtualApiKeyId,
|
||||||
|
{ onDelete: "set null" }
|
||||||
|
),
|
||||||
|
requestedModel: text("requestedModel"),
|
||||||
|
isStream: integer("isStream", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
requestBody: text("requestBody"),
|
||||||
|
responseBody: text("responseBody"),
|
||||||
|
// Capability-agnostic message transcript (JSON-encoded
|
||||||
|
// NormalizedAiMessage[] from server/lib/aiMessageNormalization.ts),
|
||||||
|
// computed at write time so search/display never need per-capability
|
||||||
|
// parsing logic. Null when normalization couldn't recognize the
|
||||||
|
// shape - callers fall back to requestBody/responseBody.
|
||||||
|
normalizedRequest: text("normalizedRequest"),
|
||||||
|
normalizedResponse: text("normalizedResponse"),
|
||||||
|
// True if any of the request/response (raw or normalized) fields
|
||||||
|
// were cut short at AI_SESSION_LOG_MAX_BODY_CHARS before storage.
|
||||||
|
truncated: integer("truncated", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
statusCode: integer("statusCode"),
|
||||||
|
createdAt: integer("createdAt").notNull() // epoch seconds
|
||||||
|
},
|
||||||
|
(t) => [
|
||||||
|
index("idx_ai_session_log_org_created").on(t.orgId, t.createdAt),
|
||||||
|
index("idx_ai_session_log_org_provider_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.providerId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_resource_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.resourceId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_site_resource_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.siteResourceId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_user_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.userId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_org_virtual_api_key_created").on(
|
||||||
|
t.orgId,
|
||||||
|
t.virtualApiKeyId,
|
||||||
|
t.createdAt
|
||||||
|
),
|
||||||
|
index("idx_ai_session_log_session").on(t.sessionId)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const certificates = sqliteTable("certificates", {
|
export const certificates = sqliteTable("certificates", {
|
||||||
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
certId: integer("certId").primaryKey({ autoIncrement: true }),
|
||||||
domain: text("domain").notNull().unique(),
|
domain: text("domain").notNull().unique(),
|
||||||
@@ -2109,6 +2192,7 @@ export type AiModel = InferSelectModel<typeof aiModels>;
|
|||||||
export type AiBudget = InferSelectModel<typeof aiBudgets>;
|
export type AiBudget = InferSelectModel<typeof aiBudgets>;
|
||||||
export type AiUsageRecord = InferSelectModel<typeof aiUsageRecords>;
|
export type AiUsageRecord = InferSelectModel<typeof aiUsageRecords>;
|
||||||
export type AiBudgetBreachEvent = InferSelectModel<typeof aiBudgetBreachEvents>;
|
export type AiBudgetBreachEvent = InferSelectModel<typeof aiBudgetBreachEvents>;
|
||||||
|
export type AiSessionLog = InferSelectModel<typeof aiSessionLog>;
|
||||||
export type ResourceAiProvider = InferSelectModel<typeof resourceAiProviders>;
|
export type ResourceAiProvider = InferSelectModel<typeof resourceAiProviders>;
|
||||||
export type SiteResourceAiProvider = InferSelectModel<
|
export type SiteResourceAiProvider = InferSelectModel<
|
||||||
typeof siteResourceAiProviders
|
typeof siteResourceAiProviders
|
||||||
|
|||||||
@@ -121,13 +121,6 @@ export async function applyBlueprint({
|
|||||||
(hc) => hc.targetId === target.targetId
|
(hc) => hc.targetId === target.targetId
|
||||||
);
|
);
|
||||||
|
|
||||||
// The DB writes for all resources have already committed
|
|
||||||
// by this point, so a push failure for one target (e.g.
|
|
||||||
// a newt rejecting a malformed health check) must not
|
|
||||||
// abort pushing the rest, and must not mark the whole
|
|
||||||
// blueprint as failed when the config was actually
|
|
||||||
// persisted successfully.
|
|
||||||
try {
|
|
||||||
if (["http", "tcp", "udp"].includes(target.mode)) {
|
if (["http", "tcp", "udp"].includes(target.mode)) {
|
||||||
await addProxyTargets(
|
await addProxyTargets(
|
||||||
site.newt.newtId,
|
site.newt.newtId,
|
||||||
@@ -149,11 +142,6 @@ export async function applyBlueprint({
|
|||||||
site.newt.version
|
site.newt.version
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
logger.error(
|
|
||||||
`Failed to push target ${target.targetId} to newt on site ${site.sites.siteId}. Error: ${e}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
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,14 +11,15 @@ 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, isNotNull } from "drizzle-orm";
|
import { eq, and, ne, inArray, or, 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";
|
||||||
@@ -388,23 +389,29 @@ export async function updatePrivateResources(
|
|||||||
.where(eq(userSiteResources.siteResourceId, siteResourceId));
|
.where(eq(userSiteResources.siteResourceId, siteResourceId));
|
||||||
|
|
||||||
if (resourceData.users.length > 0) {
|
if (resourceData.users.length > 0) {
|
||||||
const userIds = await resolveOrgUserIds(
|
// get userIds from username
|
||||||
trx,
|
const usersToUpdate = await trx
|
||||||
orgId,
|
.select()
|
||||||
resourceData.users
|
.from(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)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userIds.length > 0) {
|
const userIds = usersToUpdate.map((user) => user.user.userId);
|
||||||
|
|
||||||
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
|
||||||
const adminRoles = await trx
|
const adminRoles = await trx
|
||||||
@@ -714,23 +721,29 @@ export async function updatePrivateResources(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (resourceData.users.length > 0) {
|
if (resourceData.users.length > 0) {
|
||||||
const userIds = await resolveOrgUserIds(
|
// get userIds from username
|
||||||
trx,
|
const usersToUpdate = await trx
|
||||||
orgId,
|
.select()
|
||||||
resourceData.users
|
.from(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)
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (userIds.length > 0) {
|
const userIds = usersToUpdate.map((user) => user.user.userId);
|
||||||
|
|
||||||
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) {
|
||||||
// get clientIds from niceIds
|
// get clientIds from niceIds
|
||||||
|
|||||||
@@ -46,12 +46,11 @@ 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 } from "drizzle-orm";
|
import { and, asc, eq, isNotNull, ne, or } 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";
|
||||||
@@ -1564,19 +1563,29 @@ 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 findOrgUserByIdentifier(trx, orgId, username);
|
const [user] = await trx
|
||||||
|
.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.userId
|
(rr) => rr.userId === user.user.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!existingUserResource) {
|
if (!existingUserResource) {
|
||||||
await trx.insert(userResources).values({
|
await trx.insert(userResources).values({
|
||||||
userId: user.userId,
|
userId: user.user.userId,
|
||||||
resourceId: resourceId
|
resourceId: resourceId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1946,19 +1955,29 @@ 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 findOrgUserByIdentifier(trx, orgId, username);
|
const [user] = await trx
|
||||||
|
.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.userId
|
(up) => up.userId === user.user.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!existingUserPolicy) {
|
if (!existingUserPolicy) {
|
||||||
await trx.insert(userPolicies).values({
|
await trx.insert(userPolicies).values({
|
||||||
userId: user.userId,
|
userId: user.user.userId,
|
||||||
resourcePolicyId: policyId
|
resourcePolicyId: policyId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
userPolicies,
|
userPolicies,
|
||||||
users
|
users
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and, or } 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,7 +22,6 @@ 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;
|
||||||
@@ -467,7 +466,17 @@ 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 findOrgUserByIdentifier(trx, orgId, username);
|
const [user] = await trx
|
||||||
|
.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(
|
||||||
@@ -477,12 +486,12 @@ async function syncUserPolicies(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const alreadyExists = existingUserPolicies.some(
|
const alreadyExists = existingUserPolicies.some(
|
||||||
(up) => up.userId === user.userId
|
(up) => up.userId === user.user.userId
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!alreadyExists) {
|
if (!alreadyExists) {
|
||||||
await trx.insert(userPolicies).values({
|
await trx.insert(userPolicies).values({
|
||||||
userId: user.userId,
|
userId: user.user.userId,
|
||||||
resourcePolicyId: policyId
|
resourcePolicyId: policyId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -527,7 +536,17 @@ async function addUserPolicies(
|
|||||||
trx: Transaction
|
trx: Transaction
|
||||||
) {
|
) {
|
||||||
for (const username of ssoUsers) {
|
for (const username of ssoUsers) {
|
||||||
const user = await findOrgUserByIdentifier(trx, orgId, username);
|
const [user] = await trx
|
||||||
|
.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(
|
||||||
@@ -537,7 +556,7 @@ async function addUserPolicies(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await trx.insert(userPolicies).values({
|
await trx.insert(userPolicies).values({
|
||||||
userId: user.userId,
|
userId: user.user.userId,
|
||||||
resourcePolicyId: policyId
|
resourcePolicyId: policyId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,34 +29,8 @@ export const SiteSchema = z.object({
|
|||||||
"docker-socket-enabled": z.boolean().optional().default(true)
|
"docker-socket-enabled": z.boolean().optional().default(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
// A malformed hostname (e.g. stray whitespace) is silently accepted here but
|
|
||||||
// fails to parse as a URL when newt builds the health check request, which
|
|
||||||
// takes the target out of the routing pool and breaks the resource entirely
|
|
||||||
// (see #3677). Validate eagerly so blueprints reject it up front instead.
|
|
||||||
const healthCheckHostnameSchema = z
|
|
||||||
.string()
|
|
||||||
.trim()
|
|
||||||
.min(1)
|
|
||||||
.refine((val) => !/\s/.test(val), {
|
|
||||||
message: "Hostname must not contain whitespace"
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(val) => {
|
|
||||||
if (z.union([z.ipv4(), z.ipv6()]).safeParse(val).success) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const hostnameRegex =
|
|
||||||
/^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
|
|
||||||
return hostnameRegex.test(val);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message:
|
|
||||||
"Hostname must be a valid IP address or hostname (no spaces or invalid characters)"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const TargetHealthCheckSchema = z.object({
|
export const TargetHealthCheckSchema = z.object({
|
||||||
hostname: healthCheckHostnameSchema,
|
hostname: z.string(),
|
||||||
port: z.int().min(1).max(65535),
|
port: z.int().min(1).max(65535),
|
||||||
enabled: z.boolean().optional().default(true),
|
enabled: z.boolean().optional().default(true),
|
||||||
path: z.string().optional().default("/"),
|
path: z.string().optional().default("/"),
|
||||||
|
|||||||
@@ -88,27 +88,7 @@ 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({
|
||||||
@@ -154,9 +134,6 @@ 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,10 +16,12 @@ import {
|
|||||||
handleRemoteExitNodePingMessage
|
handleRemoteExitNodePingMessage
|
||||||
} from "#private/routers/remoteExitNode";
|
} from "#private/routers/remoteExitNode";
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import { handleConnectionLogMessage } from "#private/routers/newt";
|
import {
|
||||||
|
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,27 +81,7 @@ 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({
|
||||||
@@ -146,9 +126,6 @@ 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,23 +533,18 @@ 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 matchingUsers = await db
|
const [user] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(
|
.where(eq(users.email, email))
|
||||||
and(
|
.limit(1);
|
||||||
eq(users.email, email.toLowerCase()),
|
|
||||||
eq(users.type, UserType.Internal)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (matchingUsers.length !== 1) {
|
if (!user || user.type !== UserType.Internal) {
|
||||||
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,62 +42,54 @@ export async function setServerAdmin(
|
|||||||
|
|
||||||
const { email, password, setupToken } = parsedBody.data;
|
const { email, password, setupToken } = parsedBody.data;
|
||||||
|
|
||||||
const passwordHash = await hashPassword(password);
|
// Validate setup token
|
||||||
const userId = generateId(15);
|
const [validToken] = await db
|
||||||
|
.select()
|
||||||
await db.transaction(async (trx) => {
|
.from(setupTokens)
|
||||||
const consumed = await trx
|
|
||||||
.update(setupTokens)
|
|
||||||
.set({
|
|
||||||
used: true,
|
|
||||||
dateUsed: moment().toISOString()
|
|
||||||
})
|
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(setupTokens.token, setupToken),
|
eq(setupTokens.token, setupToken),
|
||||||
eq(setupTokens.used, false)
|
eq(setupTokens.used, false)
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
.returning({ tokenId: setupTokens.tokenId });
|
|
||||||
|
|
||||||
if (!consumed.length) {
|
if (!validToken) {
|
||||||
throw createHttpError(
|
return next(
|
||||||
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
"Invalid setup token"
|
"Invalid or expired setup token"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [existingAdmin] = await trx
|
const [existing] = await db
|
||||||
.select({ userId: users.userId })
|
.select()
|
||||||
.from(users)
|
.from(users)
|
||||||
.where(eq(users.serverAdmin, true))
|
.where(eq(users.serverAdmin, true));
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingAdmin) {
|
if (existing) {
|
||||||
throw createHttpError(
|
return next(
|
||||||
|
createHttpError(
|
||||||
HttpCode.BAD_REQUEST,
|
HttpCode.BAD_REQUEST,
|
||||||
"Server admin already exists"
|
"Server admin already exists"
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [existingUser] = await trx
|
const passwordHash = await hashPassword(password);
|
||||||
.select({ userId: users.userId })
|
const userId = generateId(15);
|
||||||
.from(users)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(users.email, email),
|
|
||||||
eq(users.type, UserType.Internal)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (existingUser) {
|
await db.transaction(async (trx) => {
|
||||||
throw createHttpError(
|
// Mark the token as used
|
||||||
HttpCode.BAD_REQUEST,
|
await trx
|
||||||
"A user with that email address already exists"
|
.update(setupTokens)
|
||||||
);
|
.set({
|
||||||
}
|
used: true,
|
||||||
|
dateUsed: moment().toISOString()
|
||||||
|
})
|
||||||
|
.where(eq(setupTokens.tokenId, validToken.tokenId));
|
||||||
|
|
||||||
|
// Create the server admin user
|
||||||
await trx.insert(users).values({
|
await trx.insert(users).values({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
email: email,
|
email: email,
|
||||||
@@ -119,9 +111,6 @@ 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 setup token"
|
message: "Invalid or expired setup token"
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ 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 {
|
||||||
@@ -76,12 +75,6 @@ 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(
|
||||||
@@ -191,9 +184,6 @@ 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
|
||||||
@@ -388,56 +378,18 @@ export default function GeneralPage() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextIdentifierPath =
|
const res = await api.post(
|
||||||
variant === "oidc"
|
`/org/${orgId}/idp/${idpId}/oidc`,
|
||||||
? (data as OidcFormValues).identifierPath
|
payload
|
||||||
: undefined;
|
);
|
||||||
|
|
||||||
if (
|
|
||||||
typeof nextIdentifierPath === "string" &&
|
|
||||||
nextIdentifierPath !== originalIdentifierPath
|
|
||||||
) {
|
|
||||||
setPendingPayload(payload);
|
|
||||||
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 (res.status === 200) {
|
||||||
if (typeof payload.identifierPath === "string") {
|
|
||||||
setOriginalIdentifierPath(payload.identifierPath);
|
|
||||||
}
|
|
||||||
toast({
|
toast({
|
||||||
title: t("success"),
|
title: t("success"),
|
||||||
description: t("idpUpdatedDescription")
|
description: t("idpUpdatedDescription")
|
||||||
});
|
});
|
||||||
router.refresh();
|
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"),
|
||||||
@@ -455,16 +407,6 @@ 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,7 +12,6 @@ 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";
|
||||||
@@ -27,7 +26,6 @@ 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();
|
||||||
@@ -47,14 +45,12 @@ 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);
|
||||||
@@ -180,7 +176,7 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
const handleFilterChange = (
|
const handleFilterChange = (
|
||||||
filterType: keyof typeof filters,
|
filterType: keyof typeof filters,
|
||||||
value: string | string[] | undefined
|
value: string | undefined
|
||||||
) => {
|
) => {
|
||||||
const newFilters = { ...filters, [filterType]: value };
|
const newFilters = { ...filters, [filterType]: value };
|
||||||
setFilters(newFilters);
|
setFilters(newFilters);
|
||||||
@@ -198,13 +194,10 @@ 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]) => {
|
||||||
params.delete(key);
|
if (value) {
|
||||||
if (typeof value === "string") {
|
|
||||||
params.set(key, value);
|
params.set(key, value);
|
||||||
} else if (typeof value !== "undefined" && "length" in value) {
|
} else {
|
||||||
for (const element of value) {
|
params.delete(key);
|
||||||
params.append(key, element);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
router.replace(`?${params.toString()}`, { scroll: false });
|
router.replace(`?${params.toString()}`, { scroll: false });
|
||||||
@@ -212,7 +205,6 @@ 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()
|
||||||
@@ -220,20 +212,13 @@ 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,
|
||||||
...restFilters
|
...filters
|
||||||
};
|
};
|
||||||
|
|
||||||
// axios serializes arrays as `ip[]=…`, which express's query
|
const response = await api.get(`/org/${orgId}/logs/access/export`, {
|
||||||
// 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(
|
|
||||||
`/org/${orgId}/logs/access/export?${sp.toString()}`,
|
|
||||||
{
|
|
||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
params
|
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");
|
||||||
@@ -312,24 +297,7 @@ export default function GeneralPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "ip",
|
accessorKey: "ip",
|
||||||
header: () => (
|
header: () => <span className="px-2">{t("ip")}</span>,
|
||||||
<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
|
||||||
@@ -347,7 +315,7 @@ export default function GeneralPage() {
|
|||||||
options={filterAttributes.locations.map(
|
options={filterAttributes.locations.map(
|
||||||
(location) => ({
|
(location) => ({
|
||||||
value: location,
|
value: location,
|
||||||
label: `${location} ${countryCodeToFlagEmoji(location)}`
|
label: location
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
label={t("location")}
|
label={t("location")}
|
||||||
@@ -366,8 +334,7 @@ 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,8 +23,6 @@ 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();
|
||||||
@@ -49,7 +47,6 @@ 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,
|
||||||
@@ -58,8 +55,7 @@ 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 = () => {
|
||||||
@@ -183,7 +179,7 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
const handleFilterChange = (
|
const handleFilterChange = (
|
||||||
filterType: keyof typeof filters,
|
filterType: keyof typeof filters,
|
||||||
value: string | string[] | undefined
|
value: string | undefined
|
||||||
) => {
|
) => {
|
||||||
const newFilters = { ...filters, [filterType]: value };
|
const newFilters = { ...filters, [filterType]: value };
|
||||||
setFilters(newFilters);
|
setFilters(newFilters);
|
||||||
@@ -201,13 +197,10 @@ 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]) => {
|
||||||
params.delete(key);
|
if (value) {
|
||||||
if (typeof value === "string") {
|
|
||||||
params.set(key, value);
|
params.set(key, value);
|
||||||
} else if (typeof value !== "undefined" && "length" in value) {
|
} else {
|
||||||
for (const element of value) {
|
params.delete(key);
|
||||||
params.append(key, element);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
router.replace(`?${params.toString()}`, { scroll: false });
|
router.replace(`?${params.toString()}`, { scroll: false });
|
||||||
@@ -216,7 +209,6 @@ 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()
|
||||||
@@ -224,15 +216,11 @@ 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,
|
||||||
...restFilters
|
...filters
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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?${sp.toString()}`,
|
`/org/${orgId}/logs/request/export`,
|
||||||
{
|
{
|
||||||
responseType: "blob",
|
responseType: "blob",
|
||||||
params
|
params
|
||||||
@@ -363,24 +351,7 @@ export default function GeneralPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "ip",
|
accessorKey: "ip",
|
||||||
header: ({ column }) => (
|
header: ({ column }) => <span className="px-2">{t("ip")}</span>,
|
||||||
<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
|
||||||
@@ -398,7 +369,7 @@ export default function GeneralPage() {
|
|||||||
options={filterAttributes.locations.map(
|
options={filterAttributes.locations.map(
|
||||||
(location) => ({
|
(location) => ({
|
||||||
value: location,
|
value: location,
|
||||||
label: `${location} ${countryCodeToFlagEmoji(location)}`
|
label: location
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
selectedValue={filters.location}
|
selectedValue={filters.location}
|
||||||
@@ -418,8 +389,7 @@ 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">
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ 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() {
|
||||||
@@ -52,12 +51,6 @@ 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();
|
||||||
@@ -148,9 +141,6 @@ 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) {
|
||||||
@@ -268,56 +258,15 @@ export default function GeneralPage() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextIdentifierPath =
|
|
||||||
variant === "oidc"
|
|
||||||
? (data as OidcFormValues).identifierPath
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (
|
|
||||||
typeof nextIdentifierPath === "string" &&
|
|
||||||
nextIdentifierPath !== originalIdentifierPath
|
|
||||||
) {
|
|
||||||
setPendingPayload(payload);
|
|
||||||
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);
|
const res = await api.post(`/idp/${idpId}/oidc`, payload);
|
||||||
|
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
if (typeof payload.identifierPath === "string") {
|
|
||||||
setOriginalIdentifierPath(payload.identifierPath);
|
|
||||||
}
|
|
||||||
toast({
|
toast({
|
||||||
title: t("success"),
|
title: t("success"),
|
||||||
description: t("idpUpdatedDescription")
|
description: t("idpUpdatedDescription")
|
||||||
});
|
});
|
||||||
router.refresh();
|
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"),
|
||||||
@@ -335,16 +284,6 @@ 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: React.ReactNode;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ColumnFilterButtonProps {
|
interface ColumnFilterButtonProps {
|
||||||
@@ -32,7 +32,6 @@ interface ColumnFilterButtonProps {
|
|||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
label: string;
|
label: string;
|
||||||
allowArbitraryValues?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ColumnFilterButton({
|
export function ColumnFilterButton({
|
||||||
@@ -42,8 +41,7 @@ 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);
|
||||||
|
|
||||||
@@ -103,7 +101,7 @@ export function ColumnFilterButton({
|
|||||||
{options.map((option) => (
|
{options.map((option) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.value}
|
value={option.label}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
onValueChange(
|
onValueChange(
|
||||||
selectedValue === option.value
|
selectedValue === option.value
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ type ColumnMultiFilterButtonProps = {
|
|||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
label: string;
|
label: string;
|
||||||
allowArbitraryValues?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ColumnMultiFilterButton({
|
export function ColumnMultiFilterButton({
|
||||||
@@ -45,26 +44,11 @@ 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]
|
||||||
@@ -80,7 +64,7 @@ export function ColumnMultiFilterButton({
|
|||||||
selectedValues[0]
|
selectedValues[0]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return t("multiSelectFilterCount", {
|
return t("accessUsersRoleFilterCount", {
|
||||||
count: selectedValues.length
|
count: selectedValues.length
|
||||||
});
|
});
|
||||||
}, [selectedValues, options, t]);
|
}, [selectedValues, options, t]);
|
||||||
@@ -124,11 +108,7 @@ export function ColumnMultiFilterButton({
|
|||||||
align="start"
|
align="start"
|
||||||
>
|
>
|
||||||
<Command>
|
<Command>
|
||||||
<CommandInput
|
<CommandInput placeholder={searchPlaceholder} />
|
||||||
placeholder={searchPlaceholder}
|
|
||||||
value={searchQuery}
|
|
||||||
onValueChange={setSearchQuery}
|
|
||||||
/>
|
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>{emptyMessage}</CommandEmpty>
|
<CommandEmpty>{emptyMessage}</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
@@ -143,7 +123,7 @@ export function ColumnMultiFilterButton({
|
|||||||
{t("accessFilterClear")}
|
{t("accessFilterClear")}
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
)}
|
)}
|
||||||
{visibleOptions.map((option) => (
|
{options.map((option) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
key={option.value}
|
key={option.value}
|
||||||
value={option.label}
|
value={option.label}
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ export function HealthCheckCredenza(props: HealthCheckCredenzaProps) {
|
|||||||
.nullable()
|
.nullable()
|
||||||
.optional(),
|
.optional(),
|
||||||
hcScheme: z.string().optional(),
|
hcScheme: z.string().optional(),
|
||||||
|
hcHostname: z.string(),
|
||||||
hcPort: z
|
hcPort: z
|
||||||
.string()
|
.string()
|
||||||
.min(1, { message: t("healthCheckPortInvalid") })
|
.min(1, { message: t("healthCheckPortInvalid") })
|
||||||
@@ -183,11 +184,6 @@ export function HealthCheckCredenza(props: HealthCheckCredenzaProps) {
|
|||||||
{ message: t("healthCheckPortInvalid") }
|
{ message: t("healthCheckPortInvalid") }
|
||||||
),
|
),
|
||||||
hcFollowRedirects: z.boolean(),
|
hcFollowRedirects: z.boolean(),
|
||||||
hcHostname: z
|
|
||||||
.string()
|
|
||||||
.refine((val) => !/\s/.test(val), {
|
|
||||||
message: t("healthCheckHostnameInvalid")
|
|
||||||
}),
|
|
||||||
hcMode: z.string(),
|
hcMode: z.string(),
|
||||||
hcUnhealthyInterval: z.int().positive().min(5),
|
hcUnhealthyInterval: z.int().positive().min(5),
|
||||||
hcTlsServerName: z.string(),
|
hcTlsServerName: z.string(),
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
"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")}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+22
-30
@@ -1,8 +1,3 @@
|
|||||||
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";
|
||||||
@@ -10,21 +5,15 @@ 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,
|
||||||
@@ -45,7 +34,6 @@ 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,
|
||||||
@@ -55,8 +43,9 @@ 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,
|
||||||
@@ -70,6 +59,7 @@ 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 {
|
||||||
@@ -81,8 +71,18 @@ 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,8 +1000,7 @@ 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>;
|
||||||
@@ -1027,8 +1026,7 @@ 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>;
|
||||||
@@ -1141,13 +1139,10 @@ 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, ip, ...rest } = filters;
|
const { page, pageSize, ...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?${sp.toString()}`, {
|
>(`/org/${orgId}/logs/request`, {
|
||||||
params: {
|
params: {
|
||||||
...rest,
|
...rest,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
@@ -1169,13 +1164,10 @@ 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, ip, ...rest } = filters;
|
const { page, pageSize, ...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?${sp.toString()}`, {
|
>(`/org/${orgId}/logs/access`, {
|
||||||
params: {
|
params: {
|
||||||
...rest,
|
...rest,
|
||||||
limit: pageSize,
|
limit: pageSize,
|
||||||
|
|||||||
Reference in New Issue
Block a user