Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 6a35f6e961 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-08-31 01:35:49 +00:00
21 changed files with 451 additions and 694 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"
-3
View File
@@ -1573,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.",
@@ -2598,7 +2596,6 @@
"createDomainType": "Type:",
"createDomainName": "Name:",
"createDomainValue": "Value:",
"multiSelectFilterCount": "{count} selected",
"createDomainCnameRecords": "CNAME Records",
"createDomainARecords": "A Records",
"createDomainRecordNumber": "Record {number}",
+9 -31
View File
@@ -262,9 +262,7 @@ export const resourceAiModels = pgTable(
(t) => [primaryKey({ columns: [t.resourceId, t.modelId] })]
);
export const labels = pgTable(
"labels",
{
export const labels = pgTable("labels", {
labelId: serial("labelId").primaryKey(),
name: varchar("name").notNull(),
color: varchar("color").notNull(),
@@ -273,9 +271,7 @@ export const labels = pgTable(
onDelete: "cascade"
})
.notNull()
},
(t) => [index("idx_labels_orgid").on(t.orgId)]
);
});
export const launcherViews = pgTable("launcherViews", {
viewId: serial("viewId").primaryKey(),
@@ -697,9 +693,7 @@ export const twoFactorBackupCodes = pgTable("twoFactorBackupCodes", {
codeHash: varchar("codeHash").notNull()
});
export const sessions = pgTable(
"session",
{
export const sessions = pgTable("session", {
sessionId: varchar("id").primaryKey(),
userId: varchar("userId")
.notNull()
@@ -707,9 +701,7 @@ export const sessions = pgTable(
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
issuedAt: bigint("issuedAt", { mode: "number" }),
deviceAuthUsed: boolean("deviceAuthUsed").notNull().default(false)
},
(t) => [index("idx_sessions_userid").on(t.userId)]
);
});
export const newtSessions = pgTable("newtSession", {
sessionId: varchar("id").primaryKey(),
@@ -719,9 +711,7 @@ export const newtSessions = pgTable("newtSession", {
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
});
export const userOrgs = pgTable(
"userOrgs",
{
export const userOrgs = pgTable("userOrgs", {
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
@@ -733,12 +723,7 @@ export const userOrgs = pgTable(
isOwner: boolean("isOwner").notNull().default(false),
autoProvisioned: boolean("autoProvisioned").default(false),
pamUsername: varchar("pamUsername") // cleaned username for ssh and such
},
(t) => [
index("idx_userOrgs_userid").on(t.userId),
index("idx_userOrgs_orgid").on(t.orgId)
]
);
});
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
codeId: serial("id").primaryKey(),
@@ -766,9 +751,7 @@ export const actions = pgTable("actions", {
description: varchar("description")
});
export const roles = pgTable(
"roles",
{
export const roles = pgTable("roles", {
roleId: serial("roleId").primaryKey(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
@@ -783,9 +766,7 @@ export const roles = pgTable(
sshSudoCommands: text("sshSudoCommands").default("[]"),
sshCreateHomeDir: boolean("sshCreateHomeDir").default(true),
sshUnixGroups: text("sshUnixGroups").default("[]")
},
(t) => [index("idx_roles_orgid").on(t.orgId)]
);
});
export const userOrgRoles = pgTable(
"userOrgRoles",
@@ -1428,10 +1409,7 @@ export const olms = pgTable(
}),
archived: boolean("archived").notNull().default(false)
},
(t) => [
index("idx_olms_clientid").on(t.clientId),
index("idx_olms_userid").on(t.userId)
]
(t) => [index("idx_olms_clientid").on(t.clientId)]
);
export const currentFingerprint = pgTable("currentFingerprint", {
+28 -97
View File
@@ -99,9 +99,7 @@ export const orgDomains = sqliteTable("orgDomains", {
.references(() => domains.domainId, { onDelete: "cascade" })
});
export const sites = sqliteTable(
"sites",
{
export const sites = sqliteTable("sites", {
siteId: integer("siteId").primaryKey({ autoIncrement: true }),
orgId: text("orgId")
.references(() => orgs.orgId, {
@@ -143,18 +141,10 @@ export const sites = sqliteTable(
})
.notNull()
.default(false),
status: text("status")
.$type<"pending" | "approved">()
.default("approved")
},
(table) => [
index("idx_sites_orgId").on(table.orgId)
]
);
status: text("status").$type<"pending" | "approved">().default("approved")
});
export const resources = sqliteTable(
"resources",
{
export const resources = sqliteTable("resources", {
resourceId: integer("resourceId").primaryKey({ autoIncrement: true }),
resourcePolicyId: integer("resourcePolicyId").references(
() => resourcePolicies.resourcePolicyId,
@@ -232,14 +222,8 @@ export const resources = sqliteTable(
.$type<"site" | "remote" | "native">()
.default("site"),
authDaemonPort: integer("authDaemonPort").default(22123),
status: text("status")
.$type<"pending" | "approved">()
.default("approved")
},
(table) => [
index("idx_resources_orgId").on(table.orgId)
]
);
status: text("status").$type<"pending" | "approved">().default("approved")
});
export const resourceAiProviders = sqliteTable(
"resourceAiProviders",
@@ -276,9 +260,7 @@ export const resourceAiModels = sqliteTable(
(t) => [primaryKey({ columns: [t.resourceId, t.modelId] })]
);
export const labels = sqliteTable(
"labels",
{
export const labels = sqliteTable("labels", {
labelId: integer("labelId").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
color: text("color").notNull(),
@@ -287,11 +269,7 @@ export const labels = sqliteTable(
onDelete: "cascade"
})
.notNull()
},
(table) => [
index("idx_labels_orgId").on(table.orgId)
]
);
});
export const launcherViews = sqliteTable("launcherViews", {
viewId: integer("viewId").primaryKey({ autoIncrement: true }),
@@ -388,18 +366,14 @@ export const clientLabels = sqliteTable(
(t) => [unique("client_label_uniq").on(t.clientId, t.labelId)]
);
export const targets = sqliteTable(
"targets",
{
export const targets = sqliteTable("targets", {
targetId: integer("targetId").primaryKey({ autoIncrement: true }),
resourceId: integer("resourceId").references(
() => resources.resourceId,
{ onDelete: "cascade" }
),
providerId: integer("providerId").references(
() => aiProviders.providerId,
{ onDelete: "cascade" }
),
resourceId: integer("resourceId").references(() => resources.resourceId, {
onDelete: "cascade"
}),
providerId: integer("providerId").references(() => aiProviders.providerId, {
onDelete: "cascade"
}),
siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade"
@@ -420,12 +394,7 @@ export const targets = sqliteTable(
.notNull()
.default("http"),
authToken: text("authToken")
},
(table) => [
index("idx_targets_resourceId").on(table.resourceId),
index("idx_targets_siteId").on(table.siteId)
]
);
});
export const targetHealthCheck = sqliteTable("targetHealthCheck", {
targetHealthCheckId: integer("targetHealthCheckId").primaryKey({
@@ -694,9 +663,7 @@ export const setupTokens = sqliteTable("setupTokens", {
dateUsed: text("dateUsed")
});
export const newts = sqliteTable(
"newt",
{
export const newts = sqliteTable("newt", {
newtId: text("id").primaryKey(),
secretHash: text("secretHash").notNull(),
dateCreated: text("dateCreated").notNull(),
@@ -704,15 +671,9 @@ export const newts = sqliteTable(
siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade"
})
},
(table) => [
index("idx_newts_siteId").on(table.siteId)
]
);
});
export const clients = sqliteTable(
"clients",
{
export const clients = sqliteTable("clients", {
clientId: integer("clientId").primaryKey({ autoIncrement: true }),
orgId: text("orgId")
.references(() => orgs.orgId, {
@@ -745,12 +706,7 @@ export const clients = sqliteTable(
approvalState: text("approvalState").$type<
"pending" | "approved" | "denied"
>()
},
(table) => [
index("idx_clients_orgId").on(table.orgId),
index("idx_clients_userId").on(table.userId)
]
);
});
export const clientSitesAssociationsCache = sqliteTable(
"clientSitesAssociationsCache",
@@ -778,9 +734,7 @@ export const clientSiteResourcesAssociationsCache = sqliteTable(
}
);
export const olms = sqliteTable(
"olms",
{
export const olms = sqliteTable("olms", {
olmId: text("id").primaryKey(),
secretHash: text("secretHash").notNull(),
dateCreated: text("dateCreated").notNull(),
@@ -796,11 +750,7 @@ export const olms = sqliteTable(
onDelete: "cascade"
}),
archived: integer("archived", { mode: "boolean" }).notNull().default(false)
},
(table) => [
index("idx_olms_userId").on(table.userId)
]
);
});
export const currentFingerprint = sqliteTable("currentFingerprint", {
fingerprintId: integer("id").primaryKey({ autoIncrement: true }),
@@ -962,9 +912,7 @@ export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", {
codeHash: text("codeHash").notNull()
});
export const sessions = sqliteTable(
"session",
{
export const sessions = sqliteTable("session", {
sessionId: text("id").primaryKey(),
userId: text("userId")
.notNull()
@@ -974,11 +922,7 @@ export const sessions = sqliteTable(
deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" })
.notNull()
.default(false)
},
(table) => [
index("idx_sessions_userId").on(table.userId)
]
);
});
export const newtSessions = sqliteTable("newtSession", {
sessionId: text("id").primaryKey(),
@@ -996,9 +940,7 @@ export const olmSessions = sqliteTable("clientSession", {
expiresAt: integer("expiresAt").notNull()
});
export const userOrgs = sqliteTable(
"userOrgs",
{
export const userOrgs = sqliteTable("userOrgs", {
userId: text("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
@@ -1012,12 +954,7 @@ export const userOrgs = sqliteTable(
mode: "boolean"
}).default(false),
pamUsername: text("pamUsername") // cleaned username for ssh and such
},
(table) => [
index("idx_userOrgs_userId").on(table.userId),
index("idx_userOrgs_orgId").on(table.orgId)
]
);
});
export const emailVerificationCodes = sqliteTable("emailVerificationCodes", {
codeId: integer("id").primaryKey({ autoIncrement: true }),
@@ -1045,9 +982,7 @@ export const actions = sqliteTable("actions", {
description: text("description")
});
export const roles = sqliteTable(
"roles",
{
export const roles = sqliteTable("roles", {
roleId: integer("roleId").primaryKey({ autoIncrement: true }),
orgId: text("orgId")
.references(() => orgs.orgId, {
@@ -1066,11 +1001,7 @@ export const roles = sqliteTable(
true
),
sshUnixGroups: text("sshUnixGroups").default("[]")
},
(table) => [
index("idx_roles_orgId").on(table.orgId)
]
);
});
export const userOrgRoles = sqliteTable(
"userOrgRoles",
+2 -2
View File
@@ -101,7 +101,7 @@ export const AuthSchema = z.object({
export const RuleSchema = z
.object({
action: z.enum(["allow", "deny", "pass"]),
match: z.enum(["cidr", "path", "ip", "country", "country_is_not", "asn", "region"]),
match: z.enum(["cidr", "path", "ip", "country", "asn", "region"]),
value: z.coerce.string(),
priority: z.int().optional(),
enabled: z.boolean().optional().default(true)
@@ -136,7 +136,7 @@ export const RuleSchema = z
)
.refine(
(rule) => {
if (rule.match === "country" || rule.match === "country_is_not") {
if (rule.match === "country") {
if (!hasMaxmindCountryDb) {
return false;
}
+11 -6
View File
@@ -31,6 +31,7 @@ export async function validateAndConstructDomain(
subdomain?: string | null
): Promise<DomainValidationResult> {
try {
// Query domain with organization access check
const [domainRes] = await db
.select()
.from(domains)
@@ -41,10 +42,6 @@ export async function validateAndConstructDomain(
eq(orgDomains.orgId, orgId),
eq(orgDomains.domainId, domainId)
)
)
.leftJoin(
domainNamespaces,
eq(domainNamespaces.domainId, domainId)
);
// Check if domain exists
@@ -55,7 +52,7 @@ export async function validateAndConstructDomain(
};
}
if (!domainRes.orgDomains && !domainRes.domainNamespaces) {
if (!domainRes.orgDomains) {
return {
success: false,
error: `Organization does not have access to domain with ID ${domainId}`
@@ -86,12 +83,20 @@ export async function validateAndConstructDomain(
}
// Wildcard subdomains are not allowed on namespace (provided/free) domains
if (isWildcard && domainRes.domainNamespaces) {
if (isWildcard) {
const [namespaceDomain] = await db
.select()
.from(domainNamespaces)
.where(eq(domainNamespaces.domainId, domainId))
.limit(1);
if (namespaceDomain) {
return {
success: false,
error: "Wildcard subdomains are not supported for provided or free domains. Use a specific subdomain instead."
};
}
}
if (
isWildcard &&
@@ -68,11 +68,6 @@ export async function verifyApiKeyAccessTokenAccess(
);
}
if (apiKey.isRoot) {
// Root keys can access any access token in any org
return next();
}
if (!resource.orgId) {
return next(
createHttpError(
+13 -9
View File
@@ -149,8 +149,12 @@ LQIDAQAB
}
// Count used sites and users for license comparison
const [siteCountRes] = await db.select({ value: count() }).from(sites);
const [userCountRes] = await db.select({ value: count() }).from(users);
const [siteCountRes] = await db
.select({ value: count() })
.from(sites);
const [userCountRes] = await db
.select({ value: count() })
.from(users);
const status: LicenseStatus = {
hostId: this.hostMeta.hostMetaId,
@@ -272,13 +276,10 @@ LQIDAQAB
logger.error(
`Allowing failure. Will retry one more time at next run interval.`
);
// Fall back to last known good status if we have
// one cached; otherwise return the freshly built
// status (with defaults) rather than undefined.
const lastKnownStatus = this.statusCache.get(
// return last known good status
return this.statusCache.get(
this.statusKey
) as LicenseStatus | undefined;
return lastKnownStatus ?? status;
) as LicenseStatus;
} else {
// Subsequent failures: fail abruptly
throw e;
@@ -367,7 +368,10 @@ LQIDAQAB
}
// Only consider quantity if defined and >= 0 (quantity = users, quantity_2 = sites)
if (cached.quantity_2 !== undefined && cached.quantity_2 >= 0) {
if (
cached.quantity_2 !== undefined &&
cached.quantity_2 >= 0
) {
status.maxSites =
(status.maxSites ?? 0) + cached.quantity_2;
}
@@ -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,
+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">
+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}
+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,