Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] ba3b59e8c6 Bump the docker-dependencies group across 1 directory with 2 updates
Bumps the docker-dependencies group with 2 updates in the / directory: docker/library/node and node.


Updates `docker/library/node` from 24.18.1-slim to 26.8.1-slim

Updates `node` from 24.18.1-alpine to 26.8.1-alpine

---
updated-dependencies:
- dependency-name: docker/library/node
  dependency-version: 26.8.1-slim
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: docker-dependencies
- dependency-name: node
  dependency-version: 26.8.1-alpine
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: docker-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-02 01:34:10 +00:00
22 changed files with 221 additions and 522 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
# 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
@@ -33,7 +33,7 @@ FROM base AS builder
RUN npm ci --omit=dev
# 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
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:24.18.1-alpine
FROM node:26.8.1-alpine
WORKDIR /app
+1 -1
View File
@@ -16,7 +16,7 @@ experimental:
version: "{{.BadgerVersion}}"
crowdsec: # CrowdSec plugin configuration added
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
version: "v1.7.1"
version: "v1.4.4"
log:
level: "INFO"
+1 -8
View File
@@ -1176,10 +1176,6 @@
"idpJmespathAboutDescriptionLink": "Learn more about JMESPath",
"idpJmespathLabel": "Identifier Path",
"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)",
"idpJmespathEmailPathOptionalDescription": "The path to the user's email in the ID token",
"idpJmespathNamePathOptional": "Name Path (Optional)",
@@ -1577,8 +1573,6 @@
"search": "Search…",
"searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"ipFilterSearchPlaceholder": "Enter an IP address…",
"ipFilterEmptyMessage": "Enter an IP address to filter by",
"create": "Create",
"orgs": "Organizations",
"loginError": "An unexpected error occurred. Please try again.",
@@ -2602,7 +2596,6 @@
"createDomainType": "Type:",
"createDomainName": "Name:",
"createDomainValue": "Value:",
"multiSelectFilterCount": "{count} selected",
"createDomainCnameRecords": "CNAME Records",
"createDomainARecords": "A Records",
"createDomainRecordNumber": "Record {number}",
@@ -3000,7 +2993,7 @@
"remoteExitNodeNetworkingSubnetsPlaceholder": "Add a CIDR range (e.g. 10.0.0.0/8)",
"remoteExitNodeNetworkingSubnetsLoadError": "Failed to load subnets",
"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...",
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Search labels...",
"remoteExitNodeNetworkingLabelsLoadError": "Failed to load labels",
-38
View File
@@ -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];
}
+35 -22
View File
@@ -11,14 +11,15 @@ import {
siteNetworks,
siteResources,
Transaction,
userOrgs,
users,
userSiteResources,
networks
} 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 { getOrCreateLabelIds, syncSiteResourceLabels } from "./labels";
import { resolveOrgUserIds } from "./findOrgUser";
import logger from "@server/logger";
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
import { getNextAvailableAliasAddress } from "../ip";
@@ -388,23 +389,29 @@ export async function updatePrivateResources(
.where(eq(userSiteResources.siteResourceId, siteResourceId));
if (resourceData.users.length > 0) {
const userIds = await resolveOrgUserIds(
trx,
orgId,
resourceData.users
// get userIds from username
const usersToUpdate = await trx
.select()
.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
.insert(userSiteResources)
.values(
userIds.map((userId) => ({
userId,
siteResourceId
}))
userIds.map((userId) => ({ userId, siteResourceId }))
);
}
}
// Get all admin role IDs for this org to exclude from deletion
const adminRoles = await trx
@@ -714,23 +721,29 @@ export async function updatePrivateResources(
}
if (resourceData.users.length > 0) {
const userIds = await resolveOrgUserIds(
trx,
orgId,
resourceData.users
// get userIds from username
const usersToUpdate = await trx
.select()
.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
.insert(userSiteResources)
.values(
userIds.map((userId) => ({
userId,
siteResourceId
}))
userIds.map((userId) => ({ userId, siteResourceId }))
);
}
}
if (resourceData.machines.length > 0) {
// get clientIds from niceIds
+27 -8
View File
@@ -46,12 +46,11 @@ import { encrypt } from "@server/lib/crypto";
import logger from "@server/logger";
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
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 { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import { Config, isTargetsOnlyResource, TargetData } from "./types";
import { getOrCreateLabelIds, syncResourceLabels } from "./labels";
import { findOrgUserByIdentifier } from "./findOrgUser";
import { LimitId } from "../billing";
import { usageService } from "../billing/usageService";
import { syncInferenceAiConfig } from "./aiProviders";
@@ -1564,19 +1563,29 @@ async function syncUserResources(
.where(eq(userResources.resourceId, resourceId));
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) {
throw new Error(`User not found: ${username} in org ${orgId}`);
}
const existingUserResource = existingUserResources.find(
(rr) => rr.userId === user.userId
(rr) => rr.userId === user.user.userId
);
if (!existingUserResource) {
await trx.insert(userResources).values({
userId: user.userId,
userId: user.user.userId,
resourceId: resourceId
});
}
@@ -1946,19 +1955,29 @@ async function syncUserPolicies(
.where(eq(userPolicies.resourcePolicyId, policyId));
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) {
throw new Error(`User not found: ${username} in org ${orgId}`);
}
const existingUserPolicy = existingUserPoliciesList.find(
(up) => up.userId === user.userId
(up) => up.userId === user.user.userId
);
if (!existingUserPolicy) {
await trx.insert(userPolicies).values({
userId: user.userId,
userId: user.user.userId,
resourcePolicyId: policyId
});
}
+26 -7
View File
@@ -13,7 +13,7 @@ import {
userPolicies,
users
} from "@server/db";
import { eq, and } from "drizzle-orm";
import { eq, and, or } from "drizzle-orm";
import { Config, ResourcePolicyData } from "./types";
import logger from "@server/logger";
import { getUniqueResourcePolicyName } from "@server/db/names";
@@ -22,7 +22,6 @@ import { idpExistsForOrg } from "@server/lib/idp/idpExistsForOrg";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "../billing/tierMatrix";
import { findOrgUserByIdentifier } from "./findOrgUser";
export type ResourcePoliciesResults = {
resourcePolicyId: number;
@@ -467,7 +466,17 @@ async function syncUserPolicies(
.where(eq(userPolicies.resourcePolicyId, policyId));
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) {
logger.warn(
@@ -477,12 +486,12 @@ async function syncUserPolicies(
}
const alreadyExists = existingUserPolicies.some(
(up) => up.userId === user.userId
(up) => up.userId === user.user.userId
);
if (!alreadyExists) {
await trx.insert(userPolicies).values({
userId: user.userId,
userId: user.user.userId,
resourcePolicyId: policyId
});
}
@@ -527,7 +536,17 @@ async function addUserPolicies(
trx: Transaction
) {
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) {
logger.warn(
@@ -537,7 +556,7 @@ async function addUserPolicies(
}
await trx.insert(userPolicies).values({
userId: user.userId,
userId: user.user.userId,
resourcePolicyId: policyId
});
}
@@ -88,27 +88,7 @@ export const queryAccessAuditLogsQuery = z.object({
.optional()
.default("0")
.transform(Number)
.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"
})
.pipe(z.int().nonnegative())
});
export const queryAccessAuditLogsParams = z.object({
@@ -154,9 +134,6 @@ function getWhere(data: Q) {
data.type ? eq(accessAuditLog.type, data.type) : undefined,
data.action !== undefined
? eq(accessAuditLog.action, data.action)
: undefined,
data.ip && data.ip.length > 0
? inArray(accessAuditLog.ip, data.ip)
: undefined
);
}
+4 -2
View File
@@ -16,10 +16,12 @@ import {
handleRemoteExitNodePingMessage
} from "#private/routers/remoteExitNode";
import { MessageHandler } from "@server/routers/ws";
import { handleConnectionLogMessage } from "#private/routers/newt";
import {
handleConnectionLogMessage,
} from "#private/routers/newt";
export const messageHandlers: Record<string, MessageHandler> = {
"remoteExitNode/register": handleRemoteExitNodeRegisterMessage,
"remoteExitNode/ping": handleRemoteExitNodePingMessage,
"newt/access-log": handleConnectionLogMessage
"newt/access-log": handleConnectionLogMessage,
};
@@ -81,27 +81,7 @@ export const queryAccessAuditLogsQuery = z.strictObject({
.optional()
.default("0")
.transform(Number)
.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"
})
.pipe(z.int().nonnegative())
});
export const queryRequestAuditLogsParams = z.object({
@@ -146,9 +126,6 @@ function getWhere(data: Q) {
data.path ? eq(requestAuditLog.path, data.path) : undefined,
data.action !== undefined
? eq(requestAuditLog.action, data.action)
: undefined,
data.ip && data.ip.length > 0
? inArray(requestAuditLog.ip, data.ip)
: undefined
);
}
+4 -9
View File
@@ -533,23 +533,18 @@ export async function startAuthentication(
// If email is provided, get security keys for that specific user
if (email) {
const matchingUsers = await db
const [user] = await db
.select()
.from(users)
.where(
and(
eq(users.email, email.toLowerCase()),
eq(users.type, UserType.Internal)
)
);
.where(eq(users.email, email))
.limit(1);
if (matchingUsers.length !== 1) {
if (!user || user.type !== UserType.Internal) {
return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid credentials")
);
}
const user = matchingUsers[0];
userId = user.userId;
const userSecurityKeys = await db
+29 -40
View File
@@ -42,62 +42,54 @@ export async function setServerAdmin(
const { email, password, setupToken } = parsedBody.data;
const passwordHash = await hashPassword(password);
const userId = generateId(15);
await db.transaction(async (trx) => {
const consumed = await trx
.update(setupTokens)
.set({
used: true,
dateUsed: moment().toISOString()
})
// Validate setup token
const [validToken] = await db
.select()
.from(setupTokens)
.where(
and(
eq(setupTokens.token, setupToken),
eq(setupTokens.used, false)
)
)
.returning({ tokenId: setupTokens.tokenId });
);
if (!consumed.length) {
throw createHttpError(
if (!validToken) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid setup token"
"Invalid or expired setup token"
)
);
}
const [existingAdmin] = await trx
.select({ userId: users.userId })
const [existing] = await db
.select()
.from(users)
.where(eq(users.serverAdmin, true))
.limit(1);
.where(eq(users.serverAdmin, true));
if (existingAdmin) {
throw createHttpError(
if (existing) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Server admin already exists"
)
);
}
const [existingUser] = await trx
.select({ userId: users.userId })
.from(users)
.where(
and(
eq(users.email, email),
eq(users.type, UserType.Internal)
)
)
.limit(1);
const passwordHash = await hashPassword(password);
const userId = generateId(15);
if (existingUser) {
throw createHttpError(
HttpCode.BAD_REQUEST,
"A user with that email address already exists"
);
}
await db.transaction(async (trx) => {
// Mark the token as used
await trx
.update(setupTokens)
.set({
used: true,
dateUsed: moment().toISOString()
})
.where(eq(setupTokens.tokenId, validToken.tokenId));
// Create the server admin user
await trx.insert(users).values({
userId: userId,
email: email,
@@ -119,9 +111,6 @@ export async function setServerAdmin(
status: HttpCode.OK
});
} catch (e) {
if (createHttpError.isHttpError(e)) {
return next(e);
}
logger.error(e);
return next(
createHttpError(
+1 -1
View File
@@ -48,7 +48,7 @@ export async function validateSetupToken(
return response<ValidateSetupTokenResponse>(res, {
data: {
valid: false,
message: "Invalid setup token"
message: "Invalid or expired setup token"
},
success: true,
error: false,
@@ -46,7 +46,6 @@ import { AxiosResponse } from "axios";
import { ListRolesResponse } from "@server/routers/role";
import AutoProvisionConfigWidget from "@app/components/AutoProvisionConfigWidget";
import IdpAutoProvisionUsersDescription from "@app/components/IdpAutoProvisionUsersDescription";
import IdpIdentifierChangeDialog from "@app/components/IdpIdentifierChangeDialog";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import {
@@ -76,12 +75,6 @@ export default function GeneralPage() {
>([createMappingBuilderRule()]);
const [rawRoleExpression, setRawRoleExpression] = useState("");
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 [redirectUrl, setRedirectUrl] = useState(
@@ -191,9 +184,6 @@ export default function GeneralPage() {
const data = res.data.data;
const roleMapping = data.idpOrg.roleMapping;
const idpVariant = data.idpOidcConfig?.variant || "oidc";
setOriginalIdentifierPath(
data.idpOidcConfig?.identifierPath ?? "sub"
);
setRedirectUrl(res.data.data.redirectUrl);
// Set the variant
@@ -388,56 +378,18 @@ 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(`/org/${orgId}/idp/${idpId}/oidc`, payload);
const res = await api.post(
`/org/${orgId}/idp/${idpId}/oidc`,
payload
);
if (res.status === 200) {
if (typeof payload.identifierPath === "string") {
setOriginalIdentifierPath(payload.identifierPath);
}
toast({
title: t("success"),
description: t("idpUpdatedDescription")
});
router.refresh();
}
}
async function confirmIdentifierChange() {
if (!pendingPayload) {
return;
}
setLoading(true);
try {
await persistIdp(pendingPayload);
setPendingPayload(null);
} catch (e) {
toast({
title: t("error"),
@@ -455,16 +407,6 @@ export default function GeneralPage() {
return (
<>
<IdpIdentifierChangeDialog
open={identifierConfirmOpen}
setOpen={(open) => {
setIdentifierConfirmOpen(open);
if (!open) {
setPendingPayload(null);
}
}}
onConfirm={confirmIdentifierChange}
/>
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
+11 -44
View File
@@ -12,7 +12,6 @@ import { DateTimeValue } from "@app/components/DateTimePicker";
import { ArrowUpRight, Key, User } from "lucide-react";
import Link from "next/link";
import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
import { ColumnMultiFilterButton } from "@app/components/ColumnMultiFilterButton";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { build } from "@server/build";
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
@@ -27,7 +26,6 @@ import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { logQueries } from "@app/lib/queries";
import { useQuery } from "@tanstack/react-query";
import type { QueryAccessAuditLogResponse } from "@server/routers/auditLogs/types";
import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji";
export default function GeneralPage() {
const router = useRouter();
@@ -47,14 +45,12 @@ export default function GeneralPage() {
resourceId?: string;
location?: string;
actor?: string;
ip?: string[];
}>({
action: searchParams.get("action") || undefined,
type: searchParams.get("type") || undefined,
resourceId: searchParams.get("resourceId") || undefined,
location: searchParams.get("location") || undefined,
actor: searchParams.get("actor") || undefined,
ip: searchParams.getAll("ip") || undefined
actor: searchParams.get("actor") || undefined
});
const [currentPage, setCurrentPage] = useState<number>(0);
@@ -180,7 +176,7 @@ export default function GeneralPage() {
const handleFilterChange = (
filterType: keyof typeof filters,
value: string | string[] | undefined
value: string | undefined
) => {
const newFilters = { ...filters, [filterType]: value };
setFilters(newFilters);
@@ -198,13 +194,10 @@ export default function GeneralPage() {
) => {
const params = new URLSearchParams(searchParams);
Object.entries(newFilters).forEach(([key, value]) => {
params.delete(key);
if (typeof value === "string") {
if (value) {
params.set(key, value);
} else if (typeof value !== "undefined" && "length" in value) {
for (const element of value) {
params.append(key, element);
}
} else {
params.delete(key);
}
});
router.replace(`?${params.toString()}`, { scroll: false });
@@ -212,7 +205,6 @@ export default function GeneralPage() {
const exportData = async () => {
try {
const { ip, ...restFilters } = filters;
const params: any = {
timeStart: dateRange.startDate?.date
? new Date(dateRange.startDate.date).toISOString()
@@ -220,20 +212,13 @@ export default function GeneralPage() {
timeEnd: dateRange.endDate?.date
? new Date(dateRange.endDate.date).toISOString()
: 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(
`/org/${orgId}/logs/access/export?${sp.toString()}`,
{
const response = await api.get(`/org/${orgId}/logs/access/export`, {
responseType: "blob",
params
}
);
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement("a");
@@ -312,24 +297,7 @@ export default function GeneralPage() {
},
{
accessorKey: "ip",
header: () => (
<span className="px-2">
<ColumnMultiFilterButton
options={(filters.ip ?? []).map((ip) => ({
label: ip,
value: ip
}))}
label={t("ip")}
allowArbitraryValues
searchPlaceholder={t("ipFilterSearchPlaceholder")}
emptyMessage={t("ipFilterEmptyMessage")}
selectedValues={filters.ip ?? []}
onSelectedValuesChange={(value) =>
handleFilterChange("ip", value)
}
/>
</span>
),
header: () => <span className="px-2">{t("ip")}</span>,
cell: ({ row }) => {
return row.original.ip ? (
row.original.ip
@@ -347,7 +315,7 @@ export default function GeneralPage() {
options={filterAttributes.locations.map(
(location) => ({
value: location,
label: `${location} ${countryCodeToFlagEmoji(location)}`
label: location
})
)}
label={t("location")}
@@ -366,8 +334,7 @@ export default function GeneralPage() {
<span className="flex items-center gap-1">
{row.original.location ? (
<span className="text-muted-foreground text-xs">
{row.original.location}{" "}
{countryCodeToFlagEmoji(row.original.location)}
{row.original.location}
</span>
) : (
<span className="text-muted-foreground text-xs">
+10 -40
View File
@@ -23,8 +23,6 @@ import { useMemo, useState, useTransition } from "react";
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
import type { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types";
import { ColumnFilterButton } from "@app/components/ColumnFilterButton";
import { countryCodeToFlagEmoji } from "@app/lib/countryCodeToFlagEmoji";
import { ColumnMultiFilterButton } from "@app/components/ColumnMultiFilterButton";
export default function GeneralPage() {
const router = useRouter();
@@ -49,7 +47,6 @@ export default function GeneralPage() {
method?: string;
reason?: string;
path?: string;
ip?: string[];
}>({
action: searchParams.get("action") || undefined,
host: searchParams.get("host") || undefined,
@@ -58,8 +55,7 @@ export default function GeneralPage() {
actor: searchParams.get("actor") || undefined,
method: searchParams.get("method") || undefined,
reason: searchParams.get("reason") || undefined,
path: searchParams.get("path") || undefined,
ip: searchParams.getAll("ip") || undefined
path: searchParams.get("path") || undefined
});
const getDefaultDateRange = () => {
@@ -183,7 +179,7 @@ export default function GeneralPage() {
const handleFilterChange = (
filterType: keyof typeof filters,
value: string | string[] | undefined
value: string | undefined
) => {
const newFilters = { ...filters, [filterType]: value };
setFilters(newFilters);
@@ -201,13 +197,10 @@ export default function GeneralPage() {
) => {
const params = new URLSearchParams(searchParams);
Object.entries(newFilters).forEach(([key, value]) => {
params.delete(key);
if (typeof value === "string") {
if (value) {
params.set(key, value);
} else if (typeof value !== "undefined" && "length" in value) {
for (const element of value) {
params.append(key, element);
}
} else {
params.delete(key);
}
});
router.replace(`?${params.toString()}`, { scroll: false });
@@ -216,7 +209,6 @@ export default function GeneralPage() {
const exportData = async () => {
try {
// Prepare query params for export
const { ip, ...restFilters } = filters;
const params: any = {
timeStart: dateRange.startDate?.date
? new Date(dateRange.startDate.date).toISOString()
@@ -224,15 +216,11 @@ export default function GeneralPage() {
timeEnd: dateRange.endDate?.date
? new Date(dateRange.endDate.date).toISOString()
: 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(
`/org/${orgId}/logs/request/export?${sp.toString()}`,
`/org/${orgId}/logs/request/export`,
{
responseType: "blob",
params
@@ -363,24 +351,7 @@ export default function GeneralPage() {
},
{
accessorKey: "ip",
header: ({ column }) => (
<span className="px-2">
<ColumnMultiFilterButton
options={(filters.ip ?? []).map((ip) => ({
label: ip,
value: ip
}))}
label={t("ip")}
allowArbitraryValues
searchPlaceholder={t("ipFilterSearchPlaceholder")}
emptyMessage={t("ipFilterEmptyMessage")}
selectedValues={filters.ip ?? []}
onSelectedValuesChange={(value) =>
handleFilterChange("ip", value)
}
/>
</span>
),
header: ({ column }) => <span className="px-2">{t("ip")}</span>,
cell: ({ row }) => {
return row.original.ip ? (
row.original.ip
@@ -398,7 +369,7 @@ export default function GeneralPage() {
options={filterAttributes.locations.map(
(location) => ({
value: location,
label: `${location} ${countryCodeToFlagEmoji(location)}`
label: location
})
)}
selectedValue={filters.location}
@@ -418,8 +389,7 @@ export default function GeneralPage() {
<span className="flex items-center gap-1">
{row.original.location ? (
<span className="text-muted-foreground text-xs">
{row.original.location}{" "}
{countryCodeToFlagEmoji(row.original.location)}
{row.original.location}
</span>
) : (
<span className="text-muted-foreground text-xs">
@@ -41,7 +41,6 @@ import {
} from "@app/components/InfoSection";
import CopyToClipboard from "@app/components/CopyToClipboard";
import IdpTypeBadge from "@app/components/IdpTypeBadge";
import IdpIdentifierChangeDialog from "@app/components/IdpIdentifierChangeDialog";
import { useTranslations } from "next-intl";
export default function GeneralPage() {
@@ -52,12 +51,6 @@ export default function GeneralPage() {
const [loading, setLoading] = useState(false);
const [initialLoading, setInitialLoading] = useState(true);
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 t = useTranslations();
@@ -148,9 +141,6 @@ export default function GeneralPage() {
| "google"
| "azure") || "oidc";
setVariant(idpVariant);
setOriginalIdentifierPath(
data.idpOidcConfig?.identifierPath ?? "sub"
);
let tenantId = "";
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);
if (res.status === 200) {
if (typeof payload.identifierPath === "string") {
setOriginalIdentifierPath(payload.identifierPath);
}
toast({
title: t("success"),
description: t("idpUpdatedDescription")
});
router.refresh();
}
}
async function confirmIdentifierChange() {
if (!pendingPayload) {
return;
}
setLoading(true);
try {
await persistIdp(pendingPayload);
setPendingPayload(null);
} catch (e) {
toast({
title: t("error"),
@@ -335,16 +284,6 @@ export default function GeneralPage() {
return (
<>
<IdpIdentifierChangeDialog
open={identifierConfirmOpen}
setOpen={(open) => {
setIdentifierConfirmOpen(open);
if (!open) {
setPendingPayload(null);
}
}}
onConfirm={confirmIdentifierChange}
/>
<SettingsContainer>
<SettingsSection>
<SettingsSectionHeader>
+3 -5
View File
@@ -21,7 +21,7 @@ import { useTranslations } from "next-intl";
interface FilterOption {
value: string;
label: React.ReactNode;
label: string;
}
interface ColumnFilterButtonProps {
@@ -32,7 +32,6 @@ interface ColumnFilterButtonProps {
emptyMessage?: string;
className?: string;
label: string;
allowArbitraryValues?: boolean;
}
export function ColumnFilterButton({
@@ -42,8 +41,7 @@ export function ColumnFilterButton({
searchPlaceholder = "Search...",
emptyMessage = "No options found",
className,
label,
allowArbitraryValues
label
}: ColumnFilterButtonProps) {
const [open, setOpen] = useState(false);
@@ -103,7 +101,7 @@ export function ColumnFilterButton({
{options.map((option) => (
<CommandItem
key={option.value}
value={option.value}
value={option.label}
onSelect={() => {
onValueChange(
selectedValue === option.value
+4 -24
View File
@@ -35,7 +35,6 @@ type ColumnMultiFilterButtonProps = {
emptyMessage?: string;
className?: string;
label: string;
allowArbitraryValues?: boolean;
};
export function ColumnMultiFilterButton({
@@ -45,26 +44,11 @@ export function ColumnMultiFilterButton({
searchPlaceholder = "Search...",
emptyMessage = "No options found",
className,
label,
allowArbitraryValues
label
}: ColumnMultiFilterButtonProps) {
const [open, setOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
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(
() => new Set(selectedValues),
[selectedValues]
@@ -80,7 +64,7 @@ export function ColumnMultiFilterButton({
selectedValues[0]
);
}
return t("multiSelectFilterCount", {
return t("accessUsersRoleFilterCount", {
count: selectedValues.length
});
}, [selectedValues, options, t]);
@@ -124,11 +108,7 @@ export function ColumnMultiFilterButton({
align="start"
>
<Command>
<CommandInput
placeholder={searchPlaceholder}
value={searchQuery}
onValueChange={setSearchQuery}
/>
<CommandInput placeholder={searchPlaceholder} />
<CommandList>
<CommandEmpty>{emptyMessage}</CommandEmpty>
<CommandGroup>
@@ -143,7 +123,7 @@ export function ColumnMultiFilterButton({
{t("accessFilterClear")}
</CommandItem>
)}
{visibleOptions.map((option) => (
{options.map((option) => (
<CommandItem
key={option.value}
value={option.label}
@@ -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
View File
@@ -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 { buildLauncherSearchParams } from "@app/lib/launcherSearchParams";
import { build } from "@server/build";
@@ -10,21 +5,15 @@ import {
StatusHistoryResponse,
type BatchedStatusHistoryResponse
} 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 {
QueryRequestAnalyticsResponse,
QueryAiUsageFilterOptionsResponse,
QueryAiUsageOverviewResponse,
QueryAiUsageProvidersResponse,
QueryAiUsageResourcesResponse,
QueryAiUsageUsersRolesResponse,
QueryAiUsageVirtualApiKeysResponse,
QueryRequestAnalyticsResponse
QueryAiUsageVirtualApiKeysResponse
} from "@server/routers/auditLogs";
import type {
QueryAccessAuditLogResponse,
@@ -45,7 +34,6 @@ import type {
import type { GetDomainResponse } from "@server/routers/domain/getDomain";
import { ListHealthChecksResponse } from "@server/routers/healthChecks/types";
import type { ListOrgLabelsResponse } from "@server/routers/labels/types";
import type { ListLauncherAiModelsResponse } from "@server/routers/launcher/listLauncherAiModels";
import type {
LauncherResource,
ListLauncherGroupsResponse,
@@ -55,8 +43,9 @@ import type {
ListLauncherSitesResponse,
ListLauncherViewsResponse
} 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 { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
import type {
GetResourcePoliciesResponse,
GetResourceWhitelistResponse,
@@ -70,6 +59,7 @@ import type {
import type { GetResourceResponse } from "@server/routers/resource/getResource";
import type { GetResourceAuthInfoResponse } from "@server/routers/resource/getResourceAuthInfo";
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 { ListSitesResponse } from "@server/routers/site";
import type {
@@ -81,8 +71,18 @@ import type {
} from "@server/routers/siteResource";
import type { GetSiteResourceResponse } from "@server/routers/siteResource/getSiteResource";
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 { ListMyVirtualApiKeysResponse } from "@server/routers/virtualApiKey/types";
import type ResponseT from "@server/types/Response";
import {
infiniteQueryOptions,
@@ -1000,8 +1000,7 @@ export const httpLogsFiltersSchema = z.object({
actor: z.string().optional().catch(undefined),
method: z.string().optional().catch(undefined),
reason: z.string().optional().catch(undefined),
path: z.string().optional().catch(undefined),
ip: z.array(z.string()).optional().catch(undefined)
path: z.string().optional().catch(undefined)
});
export type HttpLogFilters = z.output<typeof httpLogsFiltersSchema>;
@@ -1027,8 +1026,7 @@ export const accessLogsFiltersSchema = z.object({
action: z.string().optional().catch(undefined),
location: z.string().optional().catch(undefined),
actor: z.string().optional().catch(undefined),
type: z.string().optional().catch(undefined),
ip: z.array(z.string()).optional().catch(undefined)
type: z.string().optional().catch(undefined)
});
export type AccessLogFilters = z.output<typeof accessLogsFiltersSchema>;
@@ -1141,13 +1139,10 @@ export const logQueries = {
queryOptions({
queryKey: ["REQUEST_LOGS", orgId, "ALL", filters] as const,
queryFn: async ({ signal, meta }) => {
const { page, pageSize, ip, ...rest } = filters;
const sp = new URLSearchParams(
(ip ?? []).map((ip) => ["ip", ip])
);
const { page, pageSize, ...rest } = filters;
const res = await meta!.api.get<
AxiosResponse<QueryRequestAuditLogResponse>
>(`/org/${orgId}/logs/request?${sp.toString()}`, {
>(`/org/${orgId}/logs/request`, {
params: {
...rest,
limit: pageSize,
@@ -1169,13 +1164,10 @@ export const logQueries = {
queryOptions({
queryKey: ["ACCESS_LOGS", orgId, "ALL", filters] as const,
queryFn: async ({ signal, meta }) => {
const { page, pageSize, ip, ...rest } = filters;
const sp = new URLSearchParams(
(ip ?? []).map((ip) => ["ip", ip])
);
const { page, pageSize, ...rest } = filters;
const res = await meta!.api.get<
AxiosResponse<QueryAccessAuditLogResponse>
>(`/org/${orgId}/logs/access?${sp.toString()}`, {
>(`/org/${orgId}/logs/access`, {
params: {
...rest,
limit: pageSize,