From 4d792350efa672bf988f386ddc4418c40b9d7b23 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 12 Feb 2026 02:53:04 +0100 Subject: [PATCH 001/771] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20add=20resource?= =?UTF-8?q?=20policy=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 51 ++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 3c9574704..8363796cf 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -187,7 +187,9 @@ export const targetHealthCheck = pgTable("targetHealthCheck", { hcFollowRedirects: boolean("hcFollowRedirects").default(true), hcMethod: varchar("hcMethod").default("GET"), hcStatus: integer("hcStatus"), // http code - hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy" + hcHealth: text("hcHealth") + .$type<"unknown" | "healthy" | "unhealthy">() + .default("unknown"), // "unknown", "healthy", "unhealthy" hcTlsServerName: text("hcTlsServerName") }); @@ -217,7 +219,7 @@ export const siteResources = pgTable("siteResources", { .references(() => orgs.orgId, { onDelete: "cascade" }), niceId: varchar("niceId").notNull(), name: varchar("name").notNull(), - mode: varchar("mode").notNull(), // "host" | "cidr" | "port" + mode: varchar("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port" protocol: varchar("protocol"), // only for port mode proxyPort: integer("proxyPort"), // only for port mode destinationPort: integer("destinationPort"), // only for port mode @@ -417,7 +419,10 @@ export const roleResources = pgTable("roleResources", { .references(() => roles.roleId, { onDelete: "cascade" }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) + .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + // .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); export const userResources = pgTable("userResources", { @@ -426,7 +431,10 @@ export const userResources = pgTable("userResources", { .references(() => users.userId, { onDelete: "cascade" }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) + .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + // .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); export const userInvites = pgTable("userInvites", { @@ -448,7 +456,10 @@ export const resourcePincode = pgTable("resourcePincode", { .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), pincodeHash: varchar("pincodeHash").notNull(), - digitLength: integer("digitLength").notNull() + digitLength: integer("digitLength").notNull(), + resourcePolicyId: integer("resourcePolicyId") + // .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); export const resourcePassword = pgTable("resourcePassword", { @@ -456,7 +467,10 @@ export const resourcePassword = pgTable("resourcePassword", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), - passwordHash: varchar("passwordHash").notNull() + passwordHash: varchar("passwordHash").notNull(), + resourcePolicyId: integer("resourcePolicyId") + // .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); export const resourceHeaderAuth = pgTable("resourceHeaderAuth", { @@ -464,7 +478,10 @@ export const resourceHeaderAuth = pgTable("resourceHeaderAuth", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), - headerAuthHash: varchar("headerAuthHash").notNull() + headerAuthHash: varchar("headerAuthHash").notNull(), + resourcePolicyId: integer("resourcePolicyId") + // .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); export const resourceHeaderAuthExtendedCompatibility = pgTable( @@ -476,6 +493,9 @@ export const resourceHeaderAuthExtendedCompatibility = pgTable( resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + // .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), extendedCompatibilityIsActivated: boolean( "extendedCompatibilityIsActivated" ) @@ -570,6 +590,9 @@ export const resourceRules = pgTable("resourceRules", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + // .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), enabled: boolean("enabled").notNull().default(true), priority: integer("priority").notNull(), action: varchar("action").notNull(), // ACCEPT, DROP, PASS @@ -577,6 +600,19 @@ export const resourceRules = pgTable("resourceRules", { value: varchar("value").notNull() }); +export const resourcePolicies = pgTable("resourcePolicies", { + resourcePolicyId: serial('resourcePolicyId').primaryKey(), + idpId: integer("idpId").references(() => idp.idpId, { + onDelete: "set null" + }), + name: varchar("name").notNull(), + orgId: varchar("orgId") + .references(() => orgs.orgId, { + onDelete: "cascade" + }) + .notNull(), +}); + export const supporterKey = pgTable("supporterKey", { keyId: serial("keyId").primaryKey(), key: varchar("key").notNull(), @@ -1043,3 +1079,4 @@ export type SecurityKey = InferSelectModel; export type WebauthnChallenge = InferSelectModel; export type DeviceWebAuthCode = InferSelectModel; export type RequestAuditLog = InferSelectModel; +export type ResourcePolicy = InferSelectModel; From 3cb9e02533c01ef30dabc022c26e3d03e92f0ff7 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 12 Feb 2026 02:56:45 +0100 Subject: [PATCH 002/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20make=20`resourcePo?= =?UTF-8?q?licyId`=20non=20nullable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 8363796cf..864aa7eb2 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -421,7 +421,7 @@ export const roleResources = pgTable("roleResources", { .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), resourcePolicyId: integer("resourcePolicyId") - // .notNull() + .notNull() .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); @@ -433,7 +433,7 @@ export const userResources = pgTable("userResources", { .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), resourcePolicyId: integer("resourcePolicyId") - // .notNull() + .notNull() .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); @@ -458,7 +458,7 @@ export const resourcePincode = pgTable("resourcePincode", { pincodeHash: varchar("pincodeHash").notNull(), digitLength: integer("digitLength").notNull(), resourcePolicyId: integer("resourcePolicyId") - // .notNull() + .notNull() .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); @@ -469,7 +469,7 @@ export const resourcePassword = pgTable("resourcePassword", { .references(() => resources.resourceId, { onDelete: "cascade" }), passwordHash: varchar("passwordHash").notNull(), resourcePolicyId: integer("resourcePolicyId") - // .notNull() + .notNull() .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); @@ -480,7 +480,7 @@ export const resourceHeaderAuth = pgTable("resourceHeaderAuth", { .references(() => resources.resourceId, { onDelete: "cascade" }), headerAuthHash: varchar("headerAuthHash").notNull(), resourcePolicyId: integer("resourcePolicyId") - // .notNull() + .notNull() .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); @@ -494,7 +494,7 @@ export const resourceHeaderAuthExtendedCompatibility = pgTable( .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), resourcePolicyId: integer("resourcePolicyId") - // .notNull() + .notNull() .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), extendedCompatibilityIsActivated: boolean( "extendedCompatibilityIsActivated" @@ -591,7 +591,7 @@ export const resourceRules = pgTable("resourceRules", { .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), resourcePolicyId: integer("resourcePolicyId") - // .notNull() + .notNull() .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), enabled: boolean("enabled").notNull().default(true), priority: integer("priority").notNull(), From f6590aedbd56f88d5635731d1950c04f7219c1ef Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 12 Feb 2026 03:22:24 +0100 Subject: [PATCH 003/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20add=20default=20`s?= =?UTF-8?q?so:=20true`=20to=20resource=20policy=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 864aa7eb2..3d3751931 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -602,6 +602,7 @@ export const resourceRules = pgTable("resourceRules", { export const resourcePolicies = pgTable("resourcePolicies", { resourcePolicyId: serial('resourcePolicyId').primaryKey(), + sso: boolean("sso").notNull().default(true), idpId: integer("idpId").references(() => idp.idpId, { onDelete: "set null" }), From e6fd4c32c4d8345c6017e49dde3f79e82f38285d Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 12 Feb 2026 03:50:09 +0100 Subject: [PATCH 004/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20update=20DB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 3d3751931..59d6252a8 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -96,6 +96,8 @@ export const sites = pgTable("sites", { export const resources = pgTable("resources", { resourceId: serial("resourceId").primaryKey(), + resourcePolicyId: integer("resourcePolicyId") + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), resourceGuid: varchar("resourceGuid", { length: 36 }) .unique() .notNull() @@ -567,7 +569,10 @@ export const resourceWhitelist = pgTable("resourceWhitelist", { email: varchar("email").notNull(), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) + .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); export const resourceOtp = pgTable("resourceOtp", { @@ -575,6 +580,9 @@ export const resourceOtp = pgTable("resourceOtp", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), email: varchar("email").notNull(), otpHash: varchar("otpHash").notNull(), expiresAt: bigint("expiresAt", { mode: "number" }).notNull() From e7df24841eb45b78cf30fbff8afa1e0efaa557c1 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 12 Feb 2026 03:50:30 +0100 Subject: [PATCH 005/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20update=20sqlite=20?= =?UTF-8?q?DB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/sqlite/schema/schema.ts | 50 +++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 4137db3cb..6a2949dfb 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -106,6 +106,8 @@ export const sites = sqliteTable("sites", { export const resources = sqliteTable("resources", { resourceId: integer("resourceId").primaryKey({ autoIncrement: true }), + resourcePolicyId: integer("resourcePolicyId") + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), resourceGuid: text("resourceGuid", { length: 36 }) .unique() .notNull() @@ -747,7 +749,10 @@ export const roleResources = sqliteTable("roleResources", { .references(() => roles.roleId, { onDelete: "cascade" }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) + .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); export const userResources = sqliteTable("userResources", { @@ -756,7 +761,10 @@ export const userResources = sqliteTable("userResources", { .references(() => users.userId, { onDelete: "cascade" }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) + .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); export const userInvites = sqliteTable("userInvites", { @@ -779,6 +787,9 @@ export const resourcePincode = sqliteTable("resourcePincode", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), pincodeHash: text("pincodeHash").notNull(), digitLength: integer("digitLength").notNull() }); @@ -790,6 +801,9 @@ export const resourcePassword = sqliteTable("resourcePassword", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), passwordHash: text("passwordHash").notNull() }); @@ -800,6 +814,9 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), headerAuthHash: text("headerAuthHash").notNull() }); @@ -814,6 +831,9 @@ export const resourceHeaderAuthExtendedCompatibility = sqliteTable( resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), extendedCompatibilityIsActivated: integer( "extendedCompatibilityIsActivated", { mode: "boolean" } @@ -885,7 +905,10 @@ export const resourceWhitelist = sqliteTable("resourceWhitelist", { email: text("email").notNull(), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) + .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), }); export const resourceOtp = sqliteTable("resourceOtp", { @@ -895,6 +918,9 @@ export const resourceOtp = sqliteTable("resourceOtp", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), email: text("email").notNull(), otpHash: text("otpHash").notNull(), expiresAt: integer("expiresAt").notNull() @@ -910,6 +936,9 @@ export const resourceRules = sqliteTable("resourceRules", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), priority: integer("priority").notNull(), action: text("action").notNull(), // ACCEPT, DROP, PASS @@ -917,6 +946,21 @@ export const resourceRules = sqliteTable("resourceRules", { value: text("value").notNull() }); +export const resourcePolicies = sqliteTable("resourcePolicies", { + resourcePolicyId: integer('resourcePolicyId').primaryKey(), + sso: integer("sso", { mode: 'boolean' }).notNull().default(true), + idpId: integer("idpId").references(() => idp.idpId, { + onDelete: "set null" + }), + name: text("name").notNull(), + orgId: text("orgId") + .references(() => orgs.orgId, { + onDelete: "cascade" + }) + .notNull(), +}); + + export const supporterKey = sqliteTable("supporterKey", { keyId: integer("keyId").primaryKey({ autoIncrement: true }), key: text("key").notNull(), From 51aa55f963c0f1fb2a666d5a3c478b30fc83e2d3 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 13 Feb 2026 00:25:00 +0100 Subject: [PATCH 006/771] =?UTF-8?q?=E2=8F=AA=20revert=20changes=20already?= =?UTF-8?q?=20included=20in=20another=20PR?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 59d6252a8..2cc59f737 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -189,9 +189,7 @@ export const targetHealthCheck = pgTable("targetHealthCheck", { hcFollowRedirects: boolean("hcFollowRedirects").default(true), hcMethod: varchar("hcMethod").default("GET"), hcStatus: integer("hcStatus"), // http code - hcHealth: text("hcHealth") - .$type<"unknown" | "healthy" | "unhealthy">() - .default("unknown"), // "unknown", "healthy", "unhealthy" + hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy" hcTlsServerName: text("hcTlsServerName") }); @@ -221,7 +219,7 @@ export const siteResources = pgTable("siteResources", { .references(() => orgs.orgId, { onDelete: "cascade" }), niceId: varchar("niceId").notNull(), name: varchar("name").notNull(), - mode: varchar("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port" + mode: varchar("mode").notNull(), // "host" | "cidr" | "port" protocol: varchar("protocol"), // only for port mode proxyPort: integer("proxyPort"), // only for port mode destinationPort: integer("destinationPort"), // only for port mode From c3db8b972f45dfa3442b1d6a59442e499a632b0c Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 13 Feb 2026 05:36:42 +0100 Subject: [PATCH 007/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20schema=20updates?= =?UTF-8?q?=20for=20policies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 3 +++ server/db/sqlite/schema/schema.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 1c12bdfdc..b3b6534e7 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -610,9 +610,12 @@ export const resourceRules = pgTable("resourceRules", { export const resourcePolicies = pgTable("resourcePolicies", { resourcePolicyId: serial('resourcePolicyId').primaryKey(), sso: boolean("sso").notNull().default(true), + emailWhitelistEnabled: boolean("emailWhitelistEnabled").notNull().default(false), idpId: integer("idpId").references(() => idp.idpId, { onDelete: "set null" }), + niceId: text("niceId").notNull(), + isDefault: boolean("isDefault").notNull().default(true), name: varchar("name").notNull(), orgId: varchar("orgId") .references(() => orgs.orgId, { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 0ff784ca4..df188213c 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -950,6 +950,9 @@ export const resourceRules = sqliteTable("resourceRules", { export const resourcePolicies = sqliteTable("resourcePolicies", { resourcePolicyId: integer('resourcePolicyId').primaryKey(), sso: integer("sso", { mode: 'boolean' }).notNull().default(true), + emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: 'boolean' }).notNull().default(false), + niceId: text("niceId").notNull(), + isDefault: integer("isDefault", { mode: 'boolean' }).notNull().default(true), idpId: integer("idpId").references(() => idp.idpId, { onDelete: "set null" }), From 4d5f36466340bd1687b175650040d439547aea6e Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 13 Feb 2026 05:38:57 +0100 Subject: [PATCH 008/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20use=20the=20correc?= =?UTF-8?q?t=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/queries.ts | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/lib/queries.ts b/src/lib/queries.ts index f0dfa811a..0aa04dcf1 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -4,7 +4,9 @@ import type { ListClientsResponse } from "@server/routers/client"; import type { ListDomainsResponse } from "@server/routers/domain"; import type { GetResourceWhitelistResponse, - ListResourceNamesResponse + ListResourceNamesResponse, + ListResourceRolesResponse, + ListResourceUsersResponse } from "@server/routers/resource"; import type { ListRolesResponse } from "@server/routers/role"; import type { ListSitesResponse } from "@server/routers/site"; @@ -113,7 +115,7 @@ export const orgQueries = { return res.data.data.clients; } }), - users: ({ orgId }: { orgId: string }) => + users: ({ orgId }: { orgId: string; }) => queryOptions({ queryKey: ["ORG", orgId, "USERS"] as const, queryFn: async ({ signal, meta }) => { @@ -124,7 +126,7 @@ export const orgQueries = { return res.data.data.users; } }), - roles: ({ orgId }: { orgId: string }) => + roles: ({ orgId }: { orgId: string; }) => queryOptions({ queryKey: ["ORG", orgId, "ROLES"] as const, queryFn: async ({ signal, meta }) => { @@ -136,7 +138,7 @@ export const orgQueries = { } }), - sites: ({ orgId }: { orgId: string }) => + sites: ({ orgId }: { orgId: string; }) => queryOptions({ queryKey: ["ORG", orgId, "SITES"] as const, queryFn: async ({ signal, meta }) => { @@ -147,7 +149,7 @@ export const orgQueries = { } }), - domains: ({ orgId }: { orgId: string }) => + domains: ({ orgId }: { orgId: string; }) => queryOptions({ queryKey: ["ORG", orgId, "DOMAINS"] as const, queryFn: async ({ signal, meta }) => { @@ -169,7 +171,7 @@ export const orgQueries = { queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< AxiosResponse<{ - idps: { idpId: number; name: string }[]; + idps: { idpId: number; name: string; }[]; }> >( build === "saas" || useOrgOnlyIdp @@ -234,28 +236,28 @@ export const logQueries = { }; export const resourceQueries = { - resourceUsers: ({ resourceId }: { resourceId: number }) => + resourceUsers: ({ resourceId }: { resourceId: number; }) => queryOptions({ queryKey: ["RESOURCES", resourceId, "USERS"] as const, queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< - AxiosResponse + AxiosResponse >(`/resource/${resourceId}/users`, { signal }); return res.data.data.users; } }), - resourceRoles: ({ resourceId }: { resourceId: number }) => + resourceRoles: ({ resourceId }: { resourceId: number; }) => queryOptions({ queryKey: ["RESOURCES", resourceId, "ROLES"] as const, queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< - AxiosResponse + AxiosResponse >(`/resource/${resourceId}/roles`, { signal }); return res.data.data.roles; } }), - siteResourceUsers: ({ siteResourceId }: { siteResourceId: number }) => + siteResourceUsers: ({ siteResourceId }: { siteResourceId: number; }) => queryOptions({ queryKey: ["SITE_RESOURCES", siteResourceId, "USERS"] as const, queryFn: async ({ signal, meta }) => { @@ -265,7 +267,7 @@ export const resourceQueries = { return res.data.data.users; } }), - siteResourceRoles: ({ siteResourceId }: { siteResourceId: number }) => + siteResourceRoles: ({ siteResourceId }: { siteResourceId: number; }) => queryOptions({ queryKey: ["SITE_RESOURCES", siteResourceId, "ROLES"] as const, queryFn: async ({ signal, meta }) => { @@ -276,7 +278,7 @@ export const resourceQueries = { return res.data.data.roles; } }), - siteResourceClients: ({ siteResourceId }: { siteResourceId: number }) => + siteResourceClients: ({ siteResourceId }: { siteResourceId: number; }) => queryOptions({ queryKey: ["SITE_RESOURCES", siteResourceId, "CLIENTS"] as const, queryFn: async ({ signal, meta }) => { @@ -287,7 +289,7 @@ export const resourceQueries = { return res.data.data.clients; } }), - resourceTargets: ({ resourceId }: { resourceId: number }) => + resourceTargets: ({ resourceId }: { resourceId: number; }) => queryOptions({ queryKey: ["RESOURCES", resourceId, "TARGETS"] as const, queryFn: async ({ signal, meta }) => { @@ -298,7 +300,7 @@ export const resourceQueries = { return res.data.data.targets; } }), - resourceWhitelist: ({ resourceId }: { resourceId: number }) => + resourceWhitelist: ({ resourceId }: { resourceId: number; }) => queryOptions({ queryKey: ["RESOURCES", resourceId, "WHITELISTS"] as const, queryFn: async ({ signal, meta }) => { @@ -371,7 +373,7 @@ export const approvalQueries = { } const res = await meta!.api.get< - AxiosResponse<{ approvals: ApprovalItem[] }> + AxiosResponse<{ approvals: ApprovalItem[]; }> >(`/org/${orgId}/approvals?${sp.toString()}`, { signal }); @@ -383,7 +385,7 @@ export const approvalQueries = { queryKey: ["APPROVALS", orgId, "COUNT", "pending"] as const, queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< - AxiosResponse<{ count: number }> + AxiosResponse<{ count: number; }> >(`/org/${orgId}/approvals/count?approvalState=pending`, { signal }); From 47fe497ca1d0ff64e743ac074cf9ef639b32d987 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 13 Feb 2026 05:39:16 +0100 Subject: [PATCH 009/771] =?UTF-8?q?=F0=9F=9A=A7=20add=20sidebar=20item=20f?= =?UTF-8?q?or=20policies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + src/app/navigation.tsx | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 68f9640b2..d311976a6 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1231,6 +1231,7 @@ "sidebarResources": "Resources", "sidebarProxyResources": "Public", "sidebarClientResources": "Private", + "sidebarResourcePolicies": "Policies", "sidebarAccessControl": "Access Control", "sidebarLogsAndAnalytics": "Logs & Analytics", "sidebarUsers": "Users", diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 7df4364a5..85d7d010a 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -17,6 +17,7 @@ import { ScanEye, // Added from 'dev' branch Server, Settings, + ShieldIcon, SquareMousePointer, TicketCheck, User, @@ -62,7 +63,18 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [ title: "sidebarClientResources", href: "/{orgId}/settings/resources/client", icon: - } + }, + ...(build !== "oss" + ? [ + { + title: "sidebarResourcePolicies", + href: "/{orgId}/settings/resources/policies", + icon: ( + + ) + } + ] + : []) ] }, { @@ -86,7 +98,7 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [ href: "/{orgId}/settings/domains", icon: }, - ...(build == "saas" + ...(build === "saas" ? [ { title: "sidebarRemoteExitNodes", From 8d682ed9ad78fdaa098839957db77f3b2c6ff9d3 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 13 Feb 2026 05:39:35 +0100 Subject: [PATCH 010/771] =?UTF-8?q?=F0=9F=9A=A7=20list=20policies=20endpoi?= =?UTF-8?q?nt=20+=20list=20policies=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routers/resource/listResourcePolicies.ts | 175 ++++++++++++++++++ .../settings/resources/policies/page.tsx | 14 ++ 2 files changed, 189 insertions(+) create mode 100644 server/private/routers/resource/listResourcePolicies.ts create mode 100644 src/app/[orgId]/settings/resources/policies/page.tsx diff --git a/server/private/routers/resource/listResourcePolicies.ts b/server/private/routers/resource/listResourcePolicies.ts new file mode 100644 index 000000000..57ab0d4af --- /dev/null +++ b/server/private/routers/resource/listResourcePolicies.ts @@ -0,0 +1,175 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + + +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { + db, + resourceHeaderAuth, + resourceHeaderAuthExtendedCompatibility, + resourcePolicies +} from "@server/db"; +import { + resources, + userResources, + roleResources, + resourcePassword, + resourcePincode, + targets, + targetHealthCheck +} from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { sql, eq, or, inArray, and, count } from "drizzle-orm"; +import logger from "@server/logger"; +import { fromZodError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; + +const listResourcePoliciesParamsSchema = z.strictObject({ + orgId: z.string() +}); + +const listResourcePoliciesSchema = z.object({ + pageSize: z.coerce + .number() // for prettier formatting + .int() + .positive() + .optional() + .catch(20) + .default(20), + page: z.coerce + .number() // for prettier formatting + .int() + .min(0) + .optional() + .catch(1) + .default(1), + query: z.string().optional(), +}); + +function queryResourcePoliciesBase() { + return db + .select({ + resourcePolicyId: resourcePolicies.resourcePolicyId, + name: resourcePolicies.name, + niceId: resourcePolicies.niceId, + passwordId: resourcePassword.passwordId, + sso: resourcePolicies.sso, + pincodeId: resourcePincode.pincodeId, + whitelist: resourcePolicies.emailWhitelistEnabled, + headerAuthId: resourceHeaderAuth.headerAuthId, + headerAuthExtendedCompatibilityId: + resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId + }) + .from(resourcePolicies) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourcePolicyId, resourcePolicies.resourcePolicyId) + ) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourcePolicyId, resourcePolicies.resourcePolicyId) + ) + .leftJoin( + resourceHeaderAuth, + eq(resourceHeaderAuth.resourcePolicyId, resourcePolicies.resourcePolicyId) + ) + .leftJoin( + resourceHeaderAuthExtendedCompatibility, + eq( + resourceHeaderAuthExtendedCompatibility.resourcePolicyId, + resourcePolicies.resourcePolicyId + ) + ); + +} + +// TODO: replaced with `PaginatedResponse` when paginated table PR is merged +export type ListResourcesResponse = { + policies: Awaited>; + total: number; pageSize: number; page: number; +}; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/resource-policies", + description: "List resource policies for an organization.", + tags: [OpenAPITags.Org, OpenAPITags.Resource], + request: { + params: z.object({ + orgId: z.string() + }), + query: listResourcePoliciesSchema + }, + responses: {} +}); + + + +export async function listResourcePolicies( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = listResourcePoliciesSchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromZodError(parsedQuery.error) + ) + ); + } + const { page, pageSize, query, } = + parsedQuery.data; + + const parsedParams = listResourcePoliciesParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromZodError(parsedParams.error) + ) + ); + } + + const orgId = + parsedParams.data.orgId || + req.userOrg?.orgId || + req.apiKeyOrg?.orgId; + + if (!orgId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") + ); + } + + if (req.user && orgId && orgId !== req.userOrgId) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "User does not have access to this organization" + ) + ); + } + + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} \ No newline at end of file diff --git a/src/app/[orgId]/settings/resources/policies/page.tsx b/src/app/[orgId]/settings/resources/policies/page.tsx new file mode 100644 index 000000000..59e7120f9 --- /dev/null +++ b/src/app/[orgId]/settings/resources/policies/page.tsx @@ -0,0 +1,14 @@ +import { getTranslations } from "next-intl/server"; + +export interface ResourcePoliciesPageProps { + params: Promise<{ orgId: string }>; + searchParams: Promise<{ view?: string }>; +} + +export default async function ResourcePoliciesPage( + props: ResourcePoliciesPageProps +) { + const params = await props.params; + const t = await getTranslations(); + return <>; +} From 2c3e768867542ea2e2bc40a4b3ec262dfa363486 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 13 Feb 2026 05:54:45 +0100 Subject: [PATCH 011/771] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20list=20resource?= =?UTF-8?q?=20endpoints=20finished?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/auth/actions.ts | 6 +- server/private/routers/external.ts | 13 +++ server/private/routers/resource/index.ts | 1 + .../routers/resource/listResourcePolicies.ts | 82 ++++++++++++++++++- 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 094437f43..01748b6b9 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -131,7 +131,11 @@ export enum ActionsEnum { viewLogs = "viewLogs", exportLogs = "exportLogs", listApprovals = "listApprovals", - updateApprovals = "updateApprovals" + updateApprovals = "updateApprovals", + listResourcePolicies = "listResourcePolicies", + createResourcePolicies = "createResourcePolicies", + updateResourcePolicies = "updateResourcePolicies", + deleteResourcePolicies = "deleteResourcePolicies", } export async function checkUserActionPermission( diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index dae10a954..31724e8cd 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -25,6 +25,7 @@ import * as logs from "#private/routers/auditLogs"; import * as misc from "#private/routers/misc"; import * as reKey from "#private/routers/re-key"; import * as approval from "#private/routers/approvals"; +import * as resource from "#private/routers/resource"; import { verifyOrgAccess, @@ -340,6 +341,18 @@ authenticated.get( approval.countApprovals ); +authenticated.get( + "/org/:orgId/resource-policies", + verifyValidLicense, + // verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ? + verifyOrgAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.listResourcePolicies), + logActionAudit(ActionsEnum.listResourcePolicies), + resource.listResourcePolicies +); + + authenticated.put( "/org/:orgId/approvals/:approvalId", verifyValidLicense, diff --git a/server/private/routers/resource/index.ts b/server/private/routers/resource/index.ts index f82b55524..4bae8e982 100644 --- a/server/private/routers/resource/index.ts +++ b/server/private/routers/resource/index.ts @@ -12,3 +12,4 @@ */ export * from "./getMaintenanceInfo"; +export * from "./listResourcePolicies"; diff --git a/server/private/routers/resource/listResourcePolicies.ts b/server/private/routers/resource/listResourcePolicies.ts index 57ab0d4af..cc280f235 100644 --- a/server/private/routers/resource/listResourcePolicies.ts +++ b/server/private/routers/resource/listResourcePolicies.ts @@ -32,7 +32,7 @@ import { import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { sql, eq, or, inArray, and, count } from "drizzle-orm"; +import { sql, eq, or, inArray, and, count, ilike, asc } from "drizzle-orm"; import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; @@ -97,9 +97,9 @@ function queryResourcePoliciesBase() { } // TODO: replaced with `PaginatedResponse` when paginated table PR is merged -export type ListResourcesResponse = { +export type ListResourcePoliciesResponse = { policies: Awaited>; - total: number; pageSize: number; page: number; + pagination: { total: number; pageSize: number; page: number; }; }; registry.registerPath({ @@ -166,6 +166,82 @@ export async function listResourcePolicies( ); } + let accessibleResourcePolicies: Array<{ resourcePolicyId: number; }>; + if (req.user) { + accessibleResourcePolicies = await db + .select({ + resourcePolicyId: sql`COALESCE(${userResources.resourcePolicyId}, ${roleResources.resourcePolicyId})` + }) + .from(userResources) + .fullJoin( + roleResources, + eq(userResources.resourcePolicyId, roleResources.resourcePolicyId) + ) + .where( + or( + eq(userResources.userId, req.user!.userId), + eq(roleResources.roleId, req.userOrgRoleId!) + ) + ); + } else { + accessibleResourcePolicies = await db + .select({ + resourcePolicyId: resourcePolicies.resourcePolicyId + }) + .from(resourcePolicies) + .where(eq(resourcePolicies.orgId, orgId)); + } + + const accessibleResourceIds = accessibleResourcePolicies.map( + (resource) => resource.resourcePolicyId + ); + + const conditions = [ + and( + inArray(resourcePolicies.resourcePolicyId, accessibleResourceIds), + eq(resourcePolicies.orgId, orgId) + ) + ]; + + if (query) { + conditions.push( + or( + ilike(resourcePolicies.name, "%" + query + "%"), + ilike(resourcePolicies.niceId, "%" + query + "%"), + ) + ); + } + + const baseQuery = queryResourcePoliciesBase() + .where(and(...conditions)); + + // we need to add `as` so that drizzle filters the result as a subquery + const countQuery = db.$count(baseQuery.as("filtered_policies")); + + const [rows, totalCount] = await Promise.all([ + baseQuery + .limit(pageSize) + .offset(pageSize * (page - 1)) + .orderBy(asc(resourcePolicies.resourcePolicyId)), + countQuery + ]); + + return response(res, { + data: { + policies: rows, + pagination: { + total: totalCount, + pageSize, + page + } + }, + success: true, + error: false, + message: "Resources retrieved successfully", + status: HttpCode.OK + }); + + } catch (error) { logger.error(error); return next( From 230516347460c5902c52dcaef409d09481f649b6 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 14 Feb 2026 03:24:01 +0100 Subject: [PATCH 012/771] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 4 + .../routers/resource/listResourcePolicies.ts | 71 ++----- server/routers/resource/types.ts | 9 + .../settings/general/auth-page/page.tsx | 1 - src/app/[orgId]/settings/layout.tsx | 9 +- .../settings/resources/policies/page.tsx | 61 +++++- src/components/AuthPageBrandingForm.tsx | 28 ++- src/components/ResourcePoliciesTable.tsx | 182 ++++++++++++++++++ 8 files changed, 290 insertions(+), 75 deletions(-) create mode 100644 src/components/ResourcePoliciesTable.tsx diff --git a/messages/en-US.json b/messages/en-US.json index 4fec9cf6c..84dad51a2 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -166,6 +166,10 @@ "resourcesSearch": "Search resources...", "resourceAdd": "Add Resource", "resourceErrorDelte": "Error deleting resource", + "resourcePoliciesTitle": "Manage Resource Policies", + "resourcePoliciesDescription": "Create and manage authentication policies to control access to your resources", + "resourcePoliciesSearch": "Search policies...", + "resourcePoliciesAdd": "Add Policy", "authentication": "Authentication", "protected": "Protected", "notProtected": "Not Protected", diff --git a/server/private/routers/resource/listResourcePolicies.ts b/server/private/routers/resource/listResourcePolicies.ts index cc280f235..940a4b781 100644 --- a/server/private/routers/resource/listResourcePolicies.ts +++ b/server/private/routers/resource/listResourcePolicies.ts @@ -11,7 +11,6 @@ * This file is not licensed under the AGPLv3. */ - import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { @@ -36,6 +35,8 @@ import { sql, eq, or, inArray, and, count, ilike, asc } from "drizzle-orm"; import logger from "@server/logger"; import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; +import type { PaginatedResponse } from "@server/types/Pagination"; +import type { ListResourcePoliciesResponse } from "@server/routers/resource/types"; const listResourcePoliciesParamsSchema = z.strictObject({ orgId: z.string() @@ -56,7 +57,7 @@ const listResourcePoliciesSchema = z.object({ .optional() .catch(1) .default(1), - query: z.string().optional(), + query: z.string().optional() }); function queryResourcePoliciesBase() { @@ -65,43 +66,11 @@ function queryResourcePoliciesBase() { resourcePolicyId: resourcePolicies.resourcePolicyId, name: resourcePolicies.name, niceId: resourcePolicies.niceId, - passwordId: resourcePassword.passwordId, - sso: resourcePolicies.sso, - pincodeId: resourcePincode.pincodeId, - whitelist: resourcePolicies.emailWhitelistEnabled, - headerAuthId: resourceHeaderAuth.headerAuthId, - headerAuthExtendedCompatibilityId: - resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId + orgId: resourcePolicies.orgId }) - .from(resourcePolicies) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourcePolicyId, resourcePolicies.resourcePolicyId) - ) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourcePolicyId, resourcePolicies.resourcePolicyId) - ) - .leftJoin( - resourceHeaderAuth, - eq(resourceHeaderAuth.resourcePolicyId, resourcePolicies.resourcePolicyId) - ) - .leftJoin( - resourceHeaderAuthExtendedCompatibility, - eq( - resourceHeaderAuthExtendedCompatibility.resourcePolicyId, - resourcePolicies.resourcePolicyId - ) - ); - + .from(resourcePolicies); } -// TODO: replaced with `PaginatedResponse` when paginated table PR is merged -export type ListResourcePoliciesResponse = { - policies: Awaited>; - pagination: { total: number; pageSize: number; page: number; }; -}; - registry.registerPath({ method: "get", path: "/org/{orgId}/resource-policies", @@ -116,8 +85,6 @@ registry.registerPath({ responses: {} }); - - export async function listResourcePolicies( req: Request, res: Response, @@ -133,10 +100,11 @@ export async function listResourcePolicies( ) ); } - const { page, pageSize, query, } = - parsedQuery.data; + const { page, pageSize, query } = parsedQuery.data; - const parsedParams = listResourcePoliciesParamsSchema.safeParse(req.params); + const parsedParams = listResourcePoliciesParamsSchema.safeParse( + req.params + ); if (!parsedParams.success) { return next( createHttpError( @@ -166,7 +134,7 @@ export async function listResourcePolicies( ); } - let accessibleResourcePolicies: Array<{ resourcePolicyId: number; }>; + let accessibleResourcePolicies: Array<{ resourcePolicyId: number }>; if (req.user) { accessibleResourcePolicies = await db .select({ @@ -175,7 +143,10 @@ export async function listResourcePolicies( .from(userResources) .fullJoin( roleResources, - eq(userResources.resourcePolicyId, roleResources.resourcePolicyId) + eq( + userResources.resourcePolicyId, + roleResources.resourcePolicyId + ) ) .where( or( @@ -198,7 +169,10 @@ export async function listResourcePolicies( const conditions = [ and( - inArray(resourcePolicies.resourcePolicyId, accessibleResourceIds), + inArray( + resourcePolicies.resourcePolicyId, + accessibleResourceIds + ), eq(resourcePolicies.orgId, orgId) ) ]; @@ -207,13 +181,12 @@ export async function listResourcePolicies( conditions.push( or( ilike(resourcePolicies.name, "%" + query + "%"), - ilike(resourcePolicies.niceId, "%" + query + "%"), + ilike(resourcePolicies.niceId, "%" + query + "%") ) ); } - const baseQuery = queryResourcePoliciesBase() - .where(and(...conditions)); + const baseQuery = queryResourcePoliciesBase().where(and(...conditions)); // we need to add `as` so that drizzle filters the result as a subquery const countQuery = db.$count(baseQuery.as("filtered_policies")); @@ -240,12 +213,10 @@ export async function listResourcePolicies( message: "Resources retrieved successfully", status: HttpCode.OK }); - - } catch (error) { logger.error(error); return next( createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") ); } -} \ No newline at end of file +} diff --git a/server/routers/resource/types.ts b/server/routers/resource/types.ts index 9dcdcd086..223154a01 100644 --- a/server/routers/resource/types.ts +++ b/server/routers/resource/types.ts @@ -1,3 +1,6 @@ +import type { ResourcePolicy } from "@server/db"; +import type { PaginatedResponse } from "@server/types/Pagination"; + export type GetMaintenanceInfoResponse = { resourceId: number; name: string; @@ -8,3 +11,9 @@ export type GetMaintenanceInfoResponse = { maintenanceMessage: string | null; maintenanceEstimatedTime: string | null; }; + +export type ListResourcePoliciesResponse = PaginatedResponse<{ + policies: Array< + Pick + >; +}>; diff --git a/src/app/[orgId]/settings/general/auth-page/page.tsx b/src/app/[orgId]/settings/general/auth-page/page.tsx index 0bd482864..f245c8f86 100644 --- a/src/app/[orgId]/settings/general/auth-page/page.tsx +++ b/src/app/[orgId]/settings/general/auth-page/page.tsx @@ -11,7 +11,6 @@ import { GetLoginPageResponse } from "@server/routers/loginPage/types"; import { AxiosResponse } from "axios"; -import { redirect } from "next/navigation"; export interface AuthPageProps { params: Promise<{ orgId: string }>; diff --git a/src/app/[orgId]/settings/layout.tsx b/src/app/[orgId]/settings/layout.tsx index 34ed3ac2f..310d36ca0 100644 --- a/src/app/[orgId]/settings/layout.tsx +++ b/src/app/[orgId]/settings/layout.tsx @@ -13,6 +13,7 @@ import { Layout } from "@app/components/Layout"; import { getTranslations } from "next-intl/server"; import { pullEnv } from "@app/lib/pullEnv"; import { orgNavSections } from "@app/app/navigation"; +import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; export const dynamic = "force-dynamic"; @@ -48,13 +49,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { const t = await getTranslations(); try { - const getOrgUser = cache(() => - internal.get>( - `/org/${params.orgId}/user/${user.userId}`, - cookie - ) - ); - const orgUser = await getOrgUser(); + const orgUser = await getCachedOrgUser(params.orgId, user.userId); if (!orgUser.data.data.isAdmin && !orgUser.data.data.isOwner) { throw new Error(t("userErrorNotAdminOrOwner")); diff --git a/src/app/[orgId]/settings/resources/policies/page.tsx b/src/app/[orgId]/settings/resources/policies/page.tsx index 59e7120f9..e641696ef 100644 --- a/src/app/[orgId]/settings/resources/policies/page.tsx +++ b/src/app/[orgId]/settings/resources/policies/page.tsx @@ -1,8 +1,18 @@ +import { ResourcePoliciesTable } from "@app/components/ResourcePoliciesTable"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { internal } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { getCachedOrg } from "@app/lib/api/getCachedOrg"; +import OrgProvider from "@app/providers/OrgProvider"; +import type { GetOrgResponse } from "@server/routers/org"; +import type { ListResourcePoliciesResponse } from "@server/routers/resource/types"; +import type { AxiosResponse } from "axios"; import { getTranslations } from "next-intl/server"; +import { redirect } from "next/navigation"; export interface ResourcePoliciesPageProps { params: Promise<{ orgId: string }>; - searchParams: Promise<{ view?: string }>; + searchParams: Promise>; } export default async function ResourcePoliciesPage( @@ -10,5 +20,52 @@ export default async function ResourcePoliciesPage( ) { const params = await props.params; const t = await getTranslations(); - return <>; + const searchParams = new URLSearchParams(await props.searchParams); + + let org: GetOrgResponse | null = null; + try { + const res = await getCachedOrg(params.orgId); + org = res.data.data; + } catch { + redirect(`/${params.orgId}/settings/resources`); + } + + let policies: ListResourcePoliciesResponse["policies"] = []; + let pagination: ListResourcePoliciesResponse["pagination"] = { + total: 0, + page: 1, + pageSize: 20 + }; + try { + const res = await internal.get< + AxiosResponse + >( + `/org/${params.orgId}/resource-policies?${searchParams.toString()}`, + await authCookieHeader() + ); + const responseData = res.data.data; + policies = responseData.policies; + pagination = responseData.pagination; + } catch (e) {} + + return ( + <> + + + + + + + ); } diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx index a19980627..f3c1da524 100644 --- a/src/components/AuthPageBrandingForm.tsx +++ b/src/components/AuthPageBrandingForm.tsx @@ -1,18 +1,18 @@ "use client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { startTransition, useActionState, useState } from "react"; -import { useForm } from "react-hook-form"; -import z from "zod"; import { Form, FormControl, - FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslations } from "next-intl"; +import { useActionState } from "react"; +import { useForm } from "react-hook-form"; +import z from "zod"; import { SettingsSection, SettingsSectionBody, @@ -21,21 +21,19 @@ import { SettingsSectionHeader, SettingsSectionTitle } from "./Settings"; -import { useTranslations } from "next-intl"; -import type { GetLoginPageBrandingResponse } from "@server/routers/loginPage/types"; -import { Input } from "./ui/input"; -import { ExternalLink, InfoIcon, XIcon } from "lucide-react"; -import { Button } from "./ui/button"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useRouter } from "next/navigation"; -import { toast } from "@app/hooks/useToast"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import { build } from "@server/build"; -import { PaidFeaturesAlert } from "./PaidFeaturesAlert"; -import { Alert, AlertDescription, AlertTitle } from "./ui/alert"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import type { GetLoginPageBrandingResponse } from "@server/routers/loginPage/types"; +import { XIcon } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { PaidFeaturesAlert } from "./PaidFeaturesAlert"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; export type AuthPageCustomizationProps = { orgId: string; diff --git a/src/components/ResourcePoliciesTable.tsx b/src/components/ResourcePoliciesTable.tsx new file mode 100644 index 000000000..0d348b2a3 --- /dev/null +++ b/src/components/ResourcePoliciesTable.tsx @@ -0,0 +1,182 @@ +"use client"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useNavigationContext } from "@app/hooks/useNavigationContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient } from "@app/lib/api"; +import type { ListResourcePoliciesResponse } from "@server/routers/resource/types"; +import type { PaginationState } from "@tanstack/react-table"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useTransition } from "react"; +import type { ExtendedColumnDef } from "./ui/data-table"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger +} from "./ui/dropdown-menu"; +import { Button } from "./ui/button"; +import { MoreHorizontal, ArrowRight } from "lucide-react"; +import Link from "next/link"; +import { ControlledDataTable } from "./ui/controlled-data-table"; +import { useDebouncedCallback } from "use-debounce"; + +type ResourcePolicyRow = ListResourcePoliciesResponse["policies"][number]; + +export type ResourcePoliciesTableProps = { + policies: Array; + orgId: string; + pagination: PaginationState; + rowCount: number; +}; + +export function ResourcePoliciesTable({ + policies, + orgId, + pagination, + rowCount +}: ResourcePoliciesTableProps) { + const router = useRouter(); + const { + navigate: filter, + isNavigating: isFiltering, + searchParams + } = useNavigationContext(); + const t = useTranslations(); + + const { env } = useEnvContext(); + + const api = createApiClient({ env }); + + const [isRefreshing, startTransition] = useTransition(); + const [isNavigatingToAddPage, startNavigation] = useTransition(); + + const refreshData = () => { + startTransition(() => { + try { + router.refresh(); + } catch (error) { + toast({ + title: t("error"), + description: t("refreshError"), + variant: "destructive" + }); + } + }); + }; + + const proxyColumns: ExtendedColumnDef[] = [ + { + accessorKey: "name", + enableHiding: false, + friendlyName: t("name"), + header: () => {t("name")} + }, + { + id: "niceId", + accessorKey: "nice", + friendlyName: t("identifier"), + enableHiding: true, + header: () => {t("identifier")}, + cell: ({ row }) => { + return {row.original.niceId || "-"}; + } + }, + { + id: "actions", + enableHiding: false, + header: () => , + cell: ({ row }) => { + const policyRow = row.original; + return ( +
+ + + + + + + + {t("viewSettings")} + + + { + // setSelectedResource(resourceRow); + // setIsDeleteModalOpen(true); + }} + > + + {t("delete")} + + + + + + + +
+ ); + } + } + ]; + + const handlePaginationChange = (newPage: PaginationState) => { + searchParams.set("page", (newPage.pageIndex + 1).toString()); + searchParams.set("pageSize", newPage.pageSize.toString()); + filter({ + searchParams + }); + }; + + const handleSearchChange = useDebouncedCallback((query: string) => { + searchParams.set("query", query); + searchParams.delete("page"); + filter({ + searchParams + }); + }, 300); + + return ( + <> + + startNavigation(() => + router.push( + `/${orgId}/settings/resources/policies/create` + ) + ) + } + addButtonText={t("resourcePoliciesAdd")} + onRefresh={refreshData} + isRefreshing={isRefreshing || isFiltering} + isNavigatingToAddPage={isNavigatingToAddPage} + enableColumnVisibility + columnVisibility={{ niceId: false }} + stickyLeftColumn="name" + stickyRightColumn="actions" + /> + + ); +} From 805d82b8d94493f6c4ffcb394e001e631c641411 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 14 Feb 2026 04:59:35 +0100 Subject: [PATCH 013/771] =?UTF-8?q?=E2=9C=A8=20policies=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + .../routers/resource/listResourcePolicies.ts | 42 ++++++++----------- server/routers/resource/types.ts | 5 ++- src/components/ResourcePoliciesTable.tsx | 21 +++++++++- 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 84dad51a2..d50073f1f 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -170,6 +170,7 @@ "resourcePoliciesDescription": "Create and manage authentication policies to control access to your resources", "resourcePoliciesSearch": "Search policies...", "resourcePoliciesAdd": "Add Policy", + "resourcePoliciesDefaultBadgeText": "Default policy", "authentication": "Authentication", "protected": "Protected", "notProtected": "Not Protected", diff --git a/server/private/routers/resource/listResourcePolicies.ts b/server/private/routers/resource/listResourcePolicies.ts index 940a4b781..0f2089bbc 100644 --- a/server/private/routers/resource/listResourcePolicies.ts +++ b/server/private/routers/resource/listResourcePolicies.ts @@ -11,32 +11,17 @@ * This file is not licensed under the AGPLv3. */ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { - db, - resourceHeaderAuth, - resourceHeaderAuthExtendedCompatibility, - resourcePolicies -} from "@server/db"; -import { - resources, - userResources, - roleResources, - resourcePassword, - resourcePincode, - targets, - targetHealthCheck -} from "@server/db"; +import { db, resourcePolicies, roleResources, userResources } from "@server/db"; import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import { sql, eq, or, inArray, and, count, ilike, asc } from "drizzle-orm"; import logger from "@server/logger"; -import { fromZodError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import type { PaginatedResponse } from "@server/types/Pagination"; import type { ListResourcePoliciesResponse } from "@server/routers/resource/types"; +import HttpCode from "@server/types/HttpCode"; +import { and, asc, eq, inArray, like, or, sql } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromZodError } from "zod-validation-error"; const listResourcePoliciesParamsSchema = z.strictObject({ orgId: z.string() @@ -66,7 +51,8 @@ function queryResourcePoliciesBase() { resourcePolicyId: resourcePolicies.resourcePolicyId, name: resourcePolicies.name, niceId: resourcePolicies.niceId, - orgId: resourcePolicies.orgId + orgId: resourcePolicies.orgId, + isDefault: resourcePolicies.isDefault }) .from(resourcePolicies); } @@ -180,8 +166,14 @@ export async function listResourcePolicies( if (query) { conditions.push( or( - ilike(resourcePolicies.name, "%" + query + "%"), - ilike(resourcePolicies.niceId, "%" + query + "%") + like( + sql`LOWER(${resourcePolicies.name})`, + "%" + query.toLowerCase() + "%" + ), + like( + sql`LOWER(${resourcePolicies.niceId})`, + "%" + query.toLowerCase() + "%" + ) ) ); } diff --git a/server/routers/resource/types.ts b/server/routers/resource/types.ts index 223154a01..6e0ea3d50 100644 --- a/server/routers/resource/types.ts +++ b/server/routers/resource/types.ts @@ -14,6 +14,9 @@ export type GetMaintenanceInfoResponse = { export type ListResourcePoliciesResponse = PaginatedResponse<{ policies: Array< - Pick + Pick< + ResourcePolicy, + "resourcePolicyId" | "niceId" | "name" | "orgId" | "isDefault" + > >; }>; diff --git a/src/components/ResourcePoliciesTable.tsx b/src/components/ResourcePoliciesTable.tsx index 0d348b2a3..1473f72d4 100644 --- a/src/components/ResourcePoliciesTable.tsx +++ b/src/components/ResourcePoliciesTable.tsx @@ -20,6 +20,7 @@ import { MoreHorizontal, ArrowRight } from "lucide-react"; import Link from "next/link"; import { ControlledDataTable } from "./ui/controlled-data-table"; import { useDebouncedCallback } from "use-debounce"; +import { Badge } from "./ui/badge"; type ResourcePolicyRow = ListResourcePoliciesResponse["policies"][number]; @@ -70,7 +71,25 @@ export function ResourcePoliciesTable({ accessorKey: "name", enableHiding: false, friendlyName: t("name"), - header: () => {t("name")} + header: () => {t("name")}, + cell({ row }) { + const r = row.original; + return ( +
+ {r.name} + {r.isDefault && ( + <> + + {t("resourcePoliciesDefaultBadgeText")} + + + )} +
+ ); + } }, { id: "niceId", From 801f6fb6612e2860ecbe172deb822cd59e462013 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 14 Feb 2026 05:03:40 +0100 Subject: [PATCH 014/771] =?UTF-8?q?=F0=9F=9A=9A=20move=20policies=20page?= =?UTF-8?q?=20to=20`(private)`=20folder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[orgId]/settings/{ => (private)}/resources/policies/page.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/app/[orgId]/settings/{ => (private)}/resources/policies/page.tsx (100%) diff --git a/src/app/[orgId]/settings/resources/policies/page.tsx b/src/app/[orgId]/settings/(private)/resources/policies/page.tsx similarity index 100% rename from src/app/[orgId]/settings/resources/policies/page.tsx rename to src/app/[orgId]/settings/(private)/resources/policies/page.tsx From 7177ab7f770b1992b23a79736f7a5666e9edfb64 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 14 Feb 2026 05:08:41 +0100 Subject: [PATCH 015/771] =?UTF-8?q?=F0=9F=9A=A7=20create=20resource=20poli?= =?UTF-8?q?cy=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 2 ++ .../resources/policies/create/page.tsx | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx diff --git a/messages/en-US.json b/messages/en-US.json index d50073f1f..ffd28a518 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -171,6 +171,8 @@ "resourcePoliciesSearch": "Search policies...", "resourcePoliciesAdd": "Add Policy", "resourcePoliciesDefaultBadgeText": "Default policy", + "resourcePoliciesCreate": "Create Resource Policy", + "resourcePoliciesCreateDescription": "Follow the steps below to create a new policy", "authentication": "Authentication", "protected": "Protected", "notProtected": "Not Protected", diff --git a/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx b/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx new file mode 100644 index 000000000..02afa06cd --- /dev/null +++ b/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx @@ -0,0 +1,32 @@ +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { getCachedOrg } from "@app/lib/api/getCachedOrg"; +import type { GetOrgResponse } from "@server/routers/org"; +import { getTranslations } from "next-intl/server"; +import { redirect } from "next/navigation"; + +export interface CreateResourcePolicyPageProps { + params: Promise<{ orgId: string }>; +} + +export default async function CreateResourcePolicyPage( + props: CreateResourcePolicyPageProps +) { + const params = await props.params; + const t = await getTranslations(); + + let org: GetOrgResponse | null = null; + try { + const res = await getCachedOrg(params.orgId); + org = res.data.data; + } catch { + redirect(`/${params.orgId}/settings/resources`); + } + return ( + <> + + + ); +} From e409a34a09f3bd81674323f64f5b52c684c4b000 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 18 Feb 2026 05:08:27 +0100 Subject: [PATCH 016/771] =?UTF-8?q?=F0=9F=9A=A7=20create=20policy=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 4 + .../resources/policies/create/page.tsx | 24 +- src/components/CreatePolicyForm.tsx | 620 ++++++++++++++++++ 3 files changed, 644 insertions(+), 4 deletions(-) create mode 100644 src/components/CreatePolicyForm.tsx diff --git a/messages/en-US.json b/messages/en-US.json index ffd28a518..58a772967 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -173,6 +173,10 @@ "resourcePoliciesDefaultBadgeText": "Default policy", "resourcePoliciesCreate": "Create Resource Policy", "resourcePoliciesCreateDescription": "Follow the steps below to create a new policy", + "resourcePolicyName": "Policy Name", + "resourcePolicyNameDescription": "Give this policy a name to identify it across your resources", + "resourcePolicyNamePlaceholder": "e.g. Internal Access Policy", + "policiesSeeAll": "See All Policies", "authentication": "Authentication", "protected": "Protected", "notProtected": "Not Protected", diff --git a/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx b/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx index 02afa06cd..e43ef39ee 100644 --- a/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx @@ -1,7 +1,11 @@ +import { CreatePolicyForm } from "@app/components/CreatePolicyForm"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { Button } from "@app/components/ui/button"; import { getCachedOrg } from "@app/lib/api/getCachedOrg"; +import OrgProvider from "@app/providers/OrgProvider"; import type { GetOrgResponse } from "@server/routers/org"; import { getTranslations } from "next-intl/server"; +import Link from "next/link"; import { redirect } from "next/navigation"; export interface CreateResourcePolicyPageProps { @@ -23,10 +27,22 @@ export default async function CreateResourcePolicyPage( } return ( <> - +
+ + + +
+ + + + ); } diff --git a/src/components/CreatePolicyForm.tsx b/src/components/CreatePolicyForm.tsx new file mode 100644 index 000000000..b1cb49a01 --- /dev/null +++ b/src/components/CreatePolicyForm.tsx @@ -0,0 +1,620 @@ +"use client"; + +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionFooter, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { SwitchInput } from "@app/components/SwitchInput"; +import { Tag, TagInput } from "@app/components/tags/tag-input"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { InfoPopup } from "@app/components/ui/info-popup"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { createApiClient } from "@app/lib/api"; +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; +import { orgQueries } from "@app/lib/queries"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { build } from "@server/build"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { UserType } from "@server/types/UserTypes"; +import { useQuery } from "@tanstack/react-query"; +import { Binary, Bot, InfoIcon, Key } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useActionState, useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; +import z from "zod"; + +const createPolicySchema = z.object({ + name: z.string().min(1).max(255), + sso: z.boolean().default(true), + skipToIdpId: z.number().nullable().optional(), + emailWhitelistEnabled: z.boolean().default(false), + roles: z.array( + z.object({ + id: z.string(), + text: z.string() + }) + ), + users: z.array( + z.object({ + id: z.string(), + text: z.string() + }) + ), + emails: z.array( + z.object({ + id: z.string(), + text: z.string() + }) + ) +}); + +export type CreatePolicyFormProps = {}; + +export function CreatePolicyForm({}: CreatePolicyFormProps) { + const { org } = useOrgContext(); + const t = useTranslations(); + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const [, formAction, isSubmitting] = useActionState(onSubmit, null); + const router = useRouter(); + const { isPaidUser } = usePaidStatus(); + + const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery( + orgQueries.roles({ + orgId: org.org.orgId + }) + ); + const { data: orgUsers = [], isLoading: isLoadingOrgUsers } = useQuery( + orgQueries.users({ + orgId: org.org.orgId + }) + ); + const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery( + orgQueries.identityProviders({ + orgId: org.org.orgId, + useOrgOnlyIdp: env.app.identityProviderMode === "org" + }) + ); + + const form = useForm({ + resolver: zodResolver(createPolicySchema), + defaultValues: { + name: "", + sso: true, + skipToIdpId: null, + emailWhitelistEnabled: false, + roles: [], + users: [], + emails: [] + } + }); + + const [ssoEnabled, setSsoEnabled] = useState(true); + const [whitelistEnabled, setWhitelistEnabled] = useState(false); + const [selectedIdpId, setSelectedIdpId] = useState(null); + const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< + number | null + >(null); + const [activeUsersTagIndex, setActiveUsersTagIndex] = useState< + number | null + >(null); + const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< + number | null + >(null); + + async function onSubmit() { + // ... + } + + const allRoles = useMemo(() => { + return orgRoles + .map((role) => ({ + id: role.roleId.toString(), + text: role.name + })) + .filter((role) => role.text !== "Admin"); + }, [orgRoles]); + + const allUsers = useMemo(() => { + return orgUsers.map((user) => ({ + id: user.id.toString(), + text: `${getUserDisplayName({ + email: user.email, + username: user.username + })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` + })); + }, [orgUsers]); + + const allIdps = useMemo(() => { + if (build === "saas") { + if (isPaidUser(tierMatrix.orgOidc)) { + return orgIdps.map((idp) => ({ + id: idp.idpId, + text: idp.name + })); + } + } else { + return orgIdps.map((idp) => ({ + id: idp.idpId, + text: idp.name + })); + } + return []; + }, [orgIdps]); + + const pageLoading = + isLoadingOrgRoles || isLoadingOrgUsers || isLoadingOrgIdps; + + if (pageLoading) { + return <>; + } + + return ( +
+ + + {/* Name */} + + + + {t("resourcePolicyName")} + + + {t("resourcePolicyNameDescription")} + + + + + ( + + {t("name")} + + + + + + )} + /> + + + + + {/* Users & Roles */} + + + + {t("resourceUsersRoles")} + + + {t("resourceUsersRolesDescription")} + + + + + { + setSsoEnabled(val); + form.setValue("sso", val); + }} + /> + + {ssoEnabled && ( + <> + ( + + + {t("roles")} + + + { + form.setValue( + "roles", + newRoles as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allRoles + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + {t( + "resourceRoleDescription" + )} + + + )} + /> + ( + + + {t("users")} + + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allUsers + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + )} + /> + + )} + + {ssoEnabled && allIdps.length > 0 && ( +
+ + +

+ {t( + "defaultIdentityProviderDescription" + )} +

+
+ )} +
+
+
+ + {/* Auth Methods */} + + + + {t("resourceAuthMethods")} + + + {t("resourceAuthMethodsDescriptions")} + + + + +
+
+ + + {t("resourcePasswordProtection", { + status: t("disabled") + })} + +
+ +
+ +
+
+ + + {t("resourcePincodeProtection", { + status: t("disabled") + })} + +
+ +
+ +
+
+ + + {t( + "resourceHeaderAuthProtectionDisabled" + )} + +
+ +
+
+
+
+ + {/* OTP Email */} + + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + {!env.email.emailEnabled && ( + + + + {t("otpEmailSmtpRequired")} + + + {t( + "otpEmailSmtpRequiredDescription" + )} + + + )} + { + setWhitelistEnabled(val); + form.setValue( + "emailWhitelistEnabled", + val + ); + }} + disabled={!env.email.emailEnabled} + /> + + {whitelistEnabled && env.email.emailEnabled && ( + ( + + + + + + {/* @ts-ignore */} + { + return z + .email() + .or( + z + .string() + .regex( + /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, + { + message: + t( + "otpEmailErrorInvalid" + ) + } + ) + ) + .safeParse(tag) + .success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder={t( + "otpEmailEnter" + )} + tags={ + form.getValues() + .emails + } + setTags={( + newEmails + ) => { + form.setValue( + "emails", + newEmails as [ + Tag, + ...Tag[] + ] + ); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t( + "otpEmailEnterDescription" + )} + + + )} + /> + )} + + + + + + +
+
+ + ); +} From ee21e1faa7d372e998ccec39a0df77ac163d16ee Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 18 Feb 2026 05:08:42 +0100 Subject: [PATCH 017/771] =?UTF-8?q?=F0=9F=9A=A7=20list=20authentication=20?= =?UTF-8?q?items=20from=20policy=20APIs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/auth/actions.ts | 4 + server/middlewares/index.ts | 1 + .../middlewares/verifyResourcePolicyAccess.ts | 125 +++++++++++++ server/routers/external.ts | 36 +++- server/routers/resource/index.ts | 4 + .../resource/listResourcePolicyRoles.ts | 80 +++++++++ .../resource/listResourcePolicyUsers.ts | 85 +++++++++ .../resource/setResourcePolicyRoles.ts | 165 ++++++++++++++++++ .../resource/setResourcePolicyUsers.ts | 124 +++++++++++++ 9 files changed, 623 insertions(+), 1 deletion(-) create mode 100644 server/middlewares/verifyResourcePolicyAccess.ts create mode 100644 server/routers/resource/listResourcePolicyRoles.ts create mode 100644 server/routers/resource/listResourcePolicyUsers.ts create mode 100644 server/routers/resource/setResourcePolicyRoles.ts create mode 100644 server/routers/resource/setResourcePolicyUsers.ts diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 01748b6b9..9c94c6a6a 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -136,6 +136,10 @@ export enum ActionsEnum { createResourcePolicies = "createResourcePolicies", updateResourcePolicies = "updateResourcePolicies", deleteResourcePolicies = "deleteResourcePolicies", + listResourcePolicyRoles = "listResourcePolicyRoles", + setResourcePolicyRoles = "setResourcePolicyRoles", + listResourcePolicyUsers = "listResourcePolicyUsers", + setResourcePolicyUsers = "setResourcePolicyUsers", } export async function checkUserActionPermission( diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index 6437c90e2..9ea190113 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -30,3 +30,4 @@ export * from "./verifySiteResourceAccess"; export * from "./logActionAudit"; export * from "./verifyOlmAccess"; export * from "./verifyLimits"; +export * from "./verifyResourcePolicyAccess"; diff --git a/server/middlewares/verifyResourcePolicyAccess.ts b/server/middlewares/verifyResourcePolicyAccess.ts new file mode 100644 index 000000000..83eb69d7f --- /dev/null +++ b/server/middlewares/verifyResourcePolicyAccess.ts @@ -0,0 +1,125 @@ +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { resourcePolicies, userOrgs } from "@server/db"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; + +export async function verifyResourcePolicyAccess( + req: Request, + res: Response, + next: NextFunction +) { + const userId = req.user!.userId; + const resourcePolicyIdStr = + req.params?.resourcePolicyId || + req.body?.resourcePolicyId || + req.query?.resourcePolicyId; + const niceId = + req.params?.niceId || req.body?.niceId || req.query?.niceId; + const orgId = + req.params?.orgId || req.body?.orgId || req.query?.orgId; + + try { + if (!userId) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated") + ); + } + + let policy: typeof resourcePolicies.$inferSelect | null = null; + + if (orgId && niceId) { + const [policyRes] = await db + .select() + .from(resourcePolicies) + .where( + and( + eq(resourcePolicies.niceId, niceId), + eq(resourcePolicies.orgId, orgId) + ) + ) + .limit(1); + policy = policyRes ?? null; + } else { + const resourcePolicyId = parseInt(resourcePolicyIdStr); + if (isNaN(resourcePolicyId)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid resource policy ID" + ) + ); + } + const [policyRes] = await db + .select() + .from(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)) + .limit(1); + policy = policyRes ?? null; + } + + if (!policy) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource policy with ID ${resourcePolicyIdStr ?? niceId} not found` + ) + ); + } + + if (!req.userOrg) { + const userOrgRes = await db + .select() + .from(userOrgs) + .where( + and( + eq(userOrgs.userId, userId), + eq(userOrgs.orgId, policy.orgId) + ) + ) + .limit(1); + req.userOrg = userOrgRes[0]; + } + + if (!req.userOrg || req.userOrg.orgId !== policy.orgId) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "User does not have access to this organization" + ) + ); + } + + if (req.orgPolicyAllowed === undefined && req.userOrg.orgId) { + const policyCheck = await checkOrgAccessPolicy({ + orgId: req.userOrg.orgId, + userId, + session: req.session + }); + req.orgPolicyAllowed = policyCheck.allowed; + if (!policyCheck.allowed || policyCheck.error) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Failed organization access policy check: " + + (policyCheck.error || "Unknown error") + ) + ); + } + } + + req.userOrgRoleId = req.userOrg.roleId; + req.userOrgId = policy.orgId; + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying resource policy access" + ) + ); + } +} diff --git a/server/routers/external.ts b/server/routers/external.ts index 52aaa81e9..c69fdacc5 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -42,7 +42,8 @@ import { verifyUserIsOrgOwner, verifySiteResourceAccess, verifyOlmAccess, - verifyLimits + verifyLimits, + verifyResourcePolicyAccess } from "@server/middlewares"; import { ActionsEnum } from "@server/auth/actions"; import rateLimit, { ipKeyGenerator } from "express-rate-limit"; @@ -676,6 +677,39 @@ authenticated.post( resource.setResourceUsers ); +authenticated.get( + "/resource-policy/:resourcePolicyId/roles", + verifyResourcePolicyAccess, + verifyUserHasAction(ActionsEnum.listResourcePolicyRoles), + resource.listResourcePolicyRoles +); + +authenticated.get( + "/resource-policy/:resourcePolicyId/users", + verifyResourcePolicyAccess, + verifyUserHasAction(ActionsEnum.listResourcePolicyUsers), + resource.listResourcePolicyUsers +); + +authenticated.post( + "/resource-policy/:resourcePolicyId/roles", + verifyResourcePolicyAccess, + verifyRoleAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.setResourcePolicyRoles), + logActionAudit(ActionsEnum.setResourcePolicyRoles), + resource.setResourcePolicyRoles +); + +authenticated.post( + "/resource-policy/:resourcePolicyId/users", + verifyResourcePolicyAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.setResourcePolicyUsers), + logActionAudit(ActionsEnum.setResourcePolicyUsers), + resource.setResourcePolicyUsers +); + authenticated.post( `/resource/:resourceId/password`, verifyResourceAccess, diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 3ada13d85..3a6ff49f6 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -31,3 +31,7 @@ export * from "./addUserToResource"; export * from "./removeUserFromResource"; export * from "./listAllResourceNames"; export * from "./removeEmailFromResourceWhitelist"; +export * from "./listResourcePolicyRoles"; +export * from "./listResourcePolicyUsers"; +export * from "./setResourcePolicyRoles"; +export * from "./setResourcePolicyUsers"; diff --git a/server/routers/resource/listResourcePolicyRoles.ts b/server/routers/resource/listResourcePolicyRoles.ts new file mode 100644 index 000000000..187e46d6b --- /dev/null +++ b/server/routers/resource/listResourcePolicyRoles.ts @@ -0,0 +1,80 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { roleResources, roles } from "@server/db"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; + +const listResourcePolicyRolesSchema = z.strictObject({ + resourcePolicyId: z.string().transform(Number).pipe(z.int().positive()) +}); + +async function query(resourcePolicyId: number) { + return await db + .selectDistinct({ + roleId: roles.roleId, + name: roles.name, + description: roles.description, + isAdmin: roles.isAdmin + }) + .from(roleResources) + .innerJoin(roles, eq(roleResources.roleId, roles.roleId)) + .where(eq(roleResources.resourcePolicyId, resourcePolicyId)); +} + +export type ListResourcePolicyRolesResponse = { + roles: NonNullable>>; +}; + +registry.registerPath({ + method: "get", + path: "/resource-policy/{resourcePolicyId}/roles", + description: "List all roles for a resource policy.", + tags: [OpenAPITags.Resource, OpenAPITags.Role], + request: { + params: listResourcePolicyRolesSchema + }, + responses: {} +}); + +export async function listResourcePolicyRoles( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = listResourcePolicyRolesSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { resourcePolicyId } = parsedParams.data; + + const policyRolesList = await query(resourcePolicyId); + + return response(res, { + data: { + roles: policyRolesList + }, + success: true, + error: false, + message: "Resource policy roles retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/resource/listResourcePolicyUsers.ts b/server/routers/resource/listResourcePolicyUsers.ts new file mode 100644 index 000000000..a67366bb8 --- /dev/null +++ b/server/routers/resource/listResourcePolicyUsers.ts @@ -0,0 +1,85 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { idp, userResources, users } from "@server/db"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; + +const listResourcePolicyUsersSchema = z.strictObject({ + resourcePolicyId: z.string().transform(Number).pipe(z.int().positive()) +}); + +async function queryUsers(resourcePolicyId: number) { + return await db + .selectDistinct({ + userId: userResources.userId, + username: users.username, + type: users.type, + idpName: idp.name, + idpId: users.idpId, + email: users.email + }) + .from(userResources) + .innerJoin(users, eq(userResources.userId, users.userId)) + .leftJoin(idp, eq(users.idpId, idp.idpId)) + .where(eq(userResources.resourcePolicyId, resourcePolicyId)); +} + +export type ListResourcePolicyUsersResponse = { + users: NonNullable>>; +}; + +registry.registerPath({ + method: "get", + path: "/resource-policy/{resourcePolicyId}/users", + description: "List all users for a resource policy.", + tags: [OpenAPITags.Resource, OpenAPITags.User], + request: { + params: listResourcePolicyUsersSchema + }, + responses: {} +}); + +export async function listResourcePolicyUsers( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = listResourcePolicyUsersSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { resourcePolicyId } = parsedParams.data; + + const policyUsersList = await queryUsers(resourcePolicyId); + + return response(res, { + data: { + users: policyUsersList + }, + success: true, + error: false, + message: "Resource policy users retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/resource/setResourcePolicyRoles.ts b/server/routers/resource/setResourcePolicyRoles.ts new file mode 100644 index 000000000..2a5134f4e --- /dev/null +++ b/server/routers/resource/setResourcePolicyRoles.ts @@ -0,0 +1,165 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { resourcePolicies, resources, roleResources, roles } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { eq, and, ne, inArray } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const setResourcePolicyRolesBodySchema = z.strictObject({ + roleIds: z.array(z.int().positive()) +}); + +const setResourcePolicyRolesParamsSchema = z.strictObject({ + resourcePolicyId: z.string().transform(Number).pipe(z.int().positive()) +}); + +registry.registerPath({ + method: "post", + path: "/resource-policy/{resourcePolicyId}/roles", + description: + "Set roles for a resource policy. This will replace all existing roles across all resources under this policy.", + tags: [OpenAPITags.Resource, OpenAPITags.Role], + request: { + params: setResourcePolicyRolesParamsSchema, + body: { + content: { + "application/json": { + schema: setResourcePolicyRolesBodySchema + } + } + } + }, + responses: {} +}); + +export async function setResourcePolicyRoles( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = setResourcePolicyRolesBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { roleIds } = parsedBody.data; + + const parsedParams = setResourcePolicyRolesParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { resourcePolicyId } = parsedParams.data; + + const [policy] = await db + .select() + .from(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)) + .limit(1); + + if (!policy) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Resource policy not found") + ); + } + + // Check if any of the roleIds are admin roles + const rolesToCheck = await db + .select() + .from(roles) + .where( + and( + inArray(roles.roleId, roleIds), + eq(roles.orgId, policy.orgId) + ) + ); + + const hasAdminRole = rolesToCheck.some((role) => role.isAdmin); + if (hasAdminRole) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Admin role cannot be assigned to resource policies" + ) + ); + } + + // Get admin role IDs for this org to exclude from deletion + const adminRoles = await db + .select() + .from(roles) + .where( + and(eq(roles.isAdmin, true), eq(roles.orgId, policy.orgId)) + ); + const adminRoleIds = adminRoles.map((role) => role.roleId); + + // Get all resources under this policy + const policyResources = await db + .select({ resourceId: resources.resourceId }) + .from(resources) + .where(eq(resources.resourcePolicyId, resourcePolicyId)); + + await db.transaction(async (trx) => { + // Delete existing role associations for this policy (excluding admin roles) + if (adminRoleIds.length > 0) { + await trx.delete(roleResources).where( + and( + eq(roleResources.resourcePolicyId, resourcePolicyId), + ne(roleResources.roleId, adminRoleIds[0]) + ) + ); + } else { + await trx + .delete(roleResources) + .where( + eq(roleResources.resourcePolicyId, resourcePolicyId) + ); + } + + // Insert new role associations for each resource under the policy + if (roleIds.length > 0 && policyResources.length > 0) { + await Promise.all( + policyResources.flatMap(({ resourceId }) => + roleIds.map((roleId) => + trx + .insert(roleResources) + .values({ roleId, resourceId, resourcePolicyId }) + .returning() + ) + ) + ); + } + + return response(res, { + data: {}, + success: true, + error: false, + message: "Roles set for resource policy successfully", + status: HttpCode.CREATED + }); + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/resource/setResourcePolicyUsers.ts b/server/routers/resource/setResourcePolicyUsers.ts new file mode 100644 index 000000000..8f18d93c8 --- /dev/null +++ b/server/routers/resource/setResourcePolicyUsers.ts @@ -0,0 +1,124 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { resourcePolicies, resources, userResources } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { eq } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const setResourcePolicyUsersBodySchema = z.strictObject({ + userIds: z.array(z.string()) +}); + +const setResourcePolicyUsersParamsSchema = z.strictObject({ + resourcePolicyId: z.string().transform(Number).pipe(z.int().positive()) +}); + +registry.registerPath({ + method: "post", + path: "/resource-policy/{resourcePolicyId}/users", + description: + "Set users for a resource policy. This will replace all existing users across all resources under this policy.", + tags: [OpenAPITags.Resource, OpenAPITags.User], + request: { + params: setResourcePolicyUsersParamsSchema, + body: { + content: { + "application/json": { + schema: setResourcePolicyUsersBodySchema + } + } + } + }, + responses: {} +}); + +export async function setResourcePolicyUsers( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = setResourcePolicyUsersBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { userIds } = parsedBody.data; + + const parsedParams = setResourcePolicyUsersParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { resourcePolicyId } = parsedParams.data; + + const [policy] = await db + .select() + .from(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)) + .limit(1); + + if (!policy) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Resource policy not found") + ); + } + + // Get all resources under this policy + const policyResources = await db + .select({ resourceId: resources.resourceId }) + .from(resources) + .where(eq(resources.resourcePolicyId, resourcePolicyId)); + + await db.transaction(async (trx) => { + // Delete existing user associations for this policy + await trx + .delete(userResources) + .where(eq(userResources.resourcePolicyId, resourcePolicyId)); + + // Insert new user associations for each resource under the policy + if (userIds.length > 0 && policyResources.length > 0) { + await Promise.all( + policyResources.flatMap(({ resourceId }) => + userIds.map((userId) => + trx + .insert(userResources) + .values({ userId, resourceId, resourcePolicyId }) + .returning() + ) + ) + ); + } + + return response(res, { + data: {}, + success: true, + error: false, + message: "Users set for resource policy successfully", + status: HttpCode.CREATED + }); + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} From a53363d064a717d9e3ff2c80993efb2dca922bc1 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 19 Feb 2026 03:23:54 +0100 Subject: [PATCH 018/771] =?UTF-8?q?=F0=9F=92=84=20include=20rules=20in=20c?= =?UTF-8?q?reate=20policy=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CreatePolicyForm.tsx | 1119 ++++++++++++++++++++++++++- 1 file changed, 1104 insertions(+), 15 deletions(-) diff --git a/src/components/CreatePolicyForm.tsx b/src/components/CreatePolicyForm.tsx index b1cb49a01..666c3b7ea 100644 --- a/src/components/CreatePolicyForm.tsx +++ b/src/components/CreatePolicyForm.tsx @@ -5,7 +5,6 @@ import { SettingsSection, SettingsSectionBody, SettingsSectionDescription, - SettingsSectionFooter, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle @@ -14,6 +13,14 @@ import { SwitchInput } from "@app/components/SwitchInput"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Button } from "@app/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; import { Form, FormControl, @@ -23,8 +30,13 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; -import { Input } from "@app/components/ui/input"; import { InfoPopup } from "@app/components/ui/info-popup"; +import { Input } from "@app/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; import { Select, SelectContent, @@ -32,24 +44,76 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; +import { Switch } from "@app/components/ui/switch"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@app/components/ui/table"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { toast } from "@app/hooks/useToast"; import { createApiClient } from "@app/lib/api"; import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { orgQueries } from "@app/lib/queries"; import { zodResolver } from "@hookform/resolvers/zod"; import { build } from "@server/build"; +import { MAJOR_ASNS } from "@server/db/asns"; +import { COUNTRIES } from "@server/db/countries"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { + isValidCIDR, + isValidIP, + isValidUrlGlobPattern +} from "@server/lib/validators"; import { UserType } from "@server/types/UserTypes"; import { useQuery } from "@tanstack/react-query"; -import { Binary, Bot, InfoIcon, Key } from "lucide-react"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable +} from "@tanstack/react-table"; +import { + ArrowUpDown, + Binary, + Bot, + Check, + ChevronsUpDown, + InfoIcon, + Key +} from "lucide-react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; -import { useActionState, useMemo, useState } from "react"; +import { useActionState, useCallback, useMemo, useState } from "react"; import { useForm } from "react-hook-form"; import z from "zod"; +const addRuleSchema = z.object({ + action: z.enum(["ACCEPT", "DROP", "PASS"]), + match: z.string(), + value: z.string(), + priority: z.coerce.number().int().optional() +}); + +type LocalRule = { + ruleId: number; + action: "ACCEPT" | "DROP" | "PASS"; + match: string; + value: string; + priority: number; + enabled: boolean; + new?: boolean; + updated?: boolean; +}; + const createPolicySchema = z.object({ name: z.string().min(1).max(255), sso: z.boolean().default(true), @@ -72,7 +136,19 @@ const createPolicySchema = z.object({ id: z.string(), text: z.string() }) - ) + ), + applyRules: z.boolean().default(false), + rules: z + .array( + z.object({ + action: z.enum(["ACCEPT", "DROP", "PASS"]), + match: z.string(), + value: z.string(), + priority: z.number().int(), + enabled: z.boolean() + }) + ) + .default([]) }); export type CreatePolicyFormProps = {}; @@ -86,6 +162,11 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { const router = useRouter(); const { isPaidUser } = usePaidStatus(); + const isMaxmindAvailable = + env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0; + const isMaxmindAsnAvailable = + env.server.maxmind_asn_path && env.server.maxmind_asn_path.length > 0; + const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery( orgQueries.roles({ orgId: org.org.orgId @@ -112,7 +193,9 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { emailWhitelistEnabled: false, roles: [], users: [], - emails: [] + emails: [], + applyRules: false, + rules: [] } }); @@ -129,10 +212,176 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { number | null >(null); + // Rules state + const [rules, setRules] = useState([]); + const [rulesEnabled, setRulesEnabled] = useState(false); + const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = + useState(false); + const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false); + + const addRuleForm = useForm({ + resolver: zodResolver(addRuleSchema), + defaultValues: { + action: "ACCEPT" as const, + match: "IP", + value: "" + } + }); + + const RuleAction = useMemo(() => { + return { + ACCEPT: t("alwaysAllow"), + DROP: t("alwaysDeny"), + PASS: t("passToAuth") + } as const; + }, [t]); + + const RuleMatch = useMemo(() => { + return { + PATH: t("path"), + IP: "IP", + CIDR: t("ipAddressRange"), + COUNTRY: t("country"), + ASN: "ASN" + } as const; + }, [t]); + async function onSubmit() { // ... } + const addRule = useCallback(function addRule(data: z.infer) { + const isDuplicate = rules.some( + (rule) => + rule.action === data.action && + rule.match === data.match && + rule.value === data.value + ); + + if (isDuplicate) { + toast({ + variant: "destructive", + title: t("rulesErrorDuplicate"), + description: t("rulesErrorDuplicateDescription") + }); + return; + } + + if (data.match === "CIDR" && !isValidCIDR(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidIpAddressRange"), + description: t("rulesErrorInvalidIpAddressRangeDescription") + }); + return; + } + if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidUrl"), + description: t("rulesErrorInvalidUrlDescription") + }); + return; + } + if (data.match === "IP" && !isValidIP(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidIpAddress"), + description: t("rulesErrorInvalidIpAddressDescription") + }); + return; + } + if ( + data.match === "COUNTRY" && + !COUNTRIES.some((c) => c.code === data.value) + ) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidCountry"), + description: t("rulesErrorInvalidCountryDescription") || "" + }); + return; + } + + let priority = data.priority; + if (priority === undefined) { + priority = rules.reduce( + (acc, rule) => (rule.priority > acc ? rule.priority : acc), + 0 + ); + priority++; + } + + const newRule: LocalRule = { + ...data, + ruleId: new Date().getTime(), + new: true, + priority, + enabled: true + }; + + const updatedRules = [...rules, newRule]; + setRules(updatedRules); + form.setValue( + "rules", + updatedRules.map(({ action, match, value, priority, enabled }) => ({ + action, + match, + value, + priority, + enabled + })) + ); + addRuleForm.reset(); + }, [rules, t, form, addRuleForm]); + + const removeRule = useCallback(function removeRule(ruleId: number) { + const updatedRules = rules.filter((rule) => rule.ruleId !== ruleId); + setRules(updatedRules); + form.setValue( + "rules", + updatedRules.map(({ action, match, value, priority, enabled }) => ({ + action, + match, + value, + priority, + enabled + })) + ); + }, [rules, form]); + + const updateRule = useCallback(function updateRule(ruleId: number, data: Partial) { + const updatedRules = rules.map((rule) => + rule.ruleId === ruleId ? { ...rule, ...data, updated: true } : rule + ); + setRules(updatedRules); + form.setValue( + "rules", + updatedRules.map(({ action, match, value, priority, enabled }) => ({ + action, + match, + value, + priority, + enabled + })) + ); + }, [rules, form]); + + const getValueHelpText = useCallback(function getValueHelpText(type: string) { + switch (type) { + case "CIDR": + return t("rulesMatchIpAddressRangeDescription"); + case "IP": + return t("rulesMatchIpAddress"); + case "PATH": + return t("rulesMatchUrl"); + case "COUNTRY": + return t("rulesMatchCountry"); + case "ASN": + return "Enter an Autonomous System Number (e.g., AS15169 or 15169)"; + } + }, [t]); + const allRoles = useMemo(() => { return orgRoles .map((role) => ({ @@ -169,6 +418,348 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { return []; }, [orgIdps]); + const columns: ColumnDef[] = useMemo( + () => [ + { + accessorKey: "priority", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + e.currentTarget.focus()} + onBlur={(e) => { + const parsed = z.coerce + .number() + .int() + .optional() + .safeParse(e.target.value); + if (!parsed.success) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidPriority"), + description: t( + "rulesErrorInvalidPriorityDescription" + ) + }); + return; + } + updateRule(row.original.ruleId, { + priority: parsed.data + }); + }} + /> + ) + }, + { + accessorKey: "action", + header: () => {t("rulesAction")}, + cell: ({ row }) => ( + + ) + }, + { + accessorKey: "match", + header: () => ( + {t("rulesMatchType")} + ), + cell: ({ row }) => ( + + ) + }, + { + accessorKey: "value", + header: () => {t("value")}, + cell: ({ row }) => + row.original.match === "COUNTRY" ? ( + + + + + + + + + + {t("noCountryFound")} + + + {COUNTRIES.map((country) => ( + { + updateRule( + row.original.ruleId, + { + value: country.code + } + ); + }} + > + + {country.name} ( + {country.code}) + + ))} + + + + + + ) : row.original.match === "ASN" ? ( + + + + + + + + + + No ASN found. Enter a custom ASN + below. + + + {MAJOR_ASNS.map((asn) => ( + { + updateRule( + row.original.ruleId, + { value: asn.code } + ); + }} + > + + {asn.name} ({asn.code}) + + ))} + + + +
+ + asn.code === + row.original.value + ) + ? row.original.value + : "" + } + onKeyDown={(e) => { + if (e.key === "Enter") { + const value = + e.currentTarget.value + .toUpperCase() + .replace(/^AS/, ""); + if (/^\d+$/.test(value)) { + updateRule( + row.original.ruleId, + { value: "AS" + value } + ); + } + } + }} + className="text-sm" + /> +
+
+
+ ) : ( + + updateRule(row.original.ruleId, { + value: e.target.value + }) + } + /> + ) + }, + { + accessorKey: "enabled", + header: () => {t("enabled")}, + cell: ({ row }) => ( + + updateRule(row.original.ruleId, { enabled: val }) + } + /> + ) + }, + { + id: "actions", + header: () => {t("actions")}, + cell: ({ row }) => ( +
+ +
+ ) + } + ], + [ + t, + RuleAction, + RuleMatch, + isMaxmindAvailable, + isMaxmindAsnAvailable, + updateRule, + removeRule + ] + ); + + const table = useReactTable({ + data: rules, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { + pagination: { + pageIndex: 0, + pageSize: 1000 + } + } + }); + const pageLoading = isLoadingOrgRoles || isLoadingOrgUsers || isLoadingOrgIdps; @@ -603,17 +1194,515 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { )} - - - + + + {/* Rules */} + + + + {t("rulesResource")} + + + {t("rulesResourceDescription")} + + + +
+
+ { + setRulesEnabled(val); + form.setValue("applyRules", val); + }} + /> +
+ +
+ +
+ ( + + + {t("rulesAction")} + + + + + + + )} + /> + ( + + + {t( + "rulesMatchType" + )} + + + + + + + )} + /> + ( + + + + {addRuleForm.watch( + "match" + ) === "COUNTRY" ? ( + + + + + + + + + + {t( + "noCountryFound" + )} + + + {COUNTRIES.map( + ( + country + ) => ( + { + field.onChange( + country.code + ); + setOpenAddRuleCountrySelect( + false + ); + }} + > + + { + country.name + }{" "} + ( + { + country.code + } + + ) + + ) + )} + + + + + + ) : addRuleForm.watch( + "match" + ) === "ASN" ? ( + + + + + + + + + + No + ASN + found. + Use + the + custom + input + below. + + + {MAJOR_ASNS.map( + ( + asn + ) => ( + { + field.onChange( + asn.code + ); + setOpenAddRuleAsnSelect( + false + ); + }} + > + + { + asn.name + }{" "} + ( + { + asn.code + } + + ) + + ) + )} + + + +
+ { + if ( + e.key === + "Enter" + ) { + const value = + e.currentTarget.value + .toUpperCase() + .replace( + /^AS/, + "" + ); + if ( + /^\d+$/.test( + value + ) + ) { + field.onChange( + "AS" + + value + ); + setOpenAddRuleAsnSelect( + false + ); + } + } + }} + className="text-sm" + /> +
+
+
+ ) : ( + + )} +
+ +
+ )} + /> + +
+
+ + + + + {table + .getHeaderGroups() + .map((headerGroup) => ( + + {headerGroup.headers.map( + (header) => { + const isActionsColumn = + header.column + .id === + "actions"; + return ( + + {header.isPlaceholder + ? null + : flexRender( + header + .column + .columnDef + .header, + header.getContext() + )} + + ); + } + )} + + ))} + + + {table.getRowModel().rows?.length ? ( + table + .getRowModel() + .rows.map((row) => ( + + {row + .getVisibleCells() + .map((cell) => { + const isActionsColumn = + cell.column + .id === + "actions"; + return ( + + {flexRender( + cell + .column + .columnDef + .cell, + cell.getContext() + )} + + ); + })} + + )) + ) : ( + + + {t("rulesNoOne")} + + + )} + +
+
+
+ +
+ +
); From c3fdda026b99de4be55ae1bea4faaba02af9bf1d Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 19 Feb 2026 04:36:42 +0100 Subject: [PATCH 019/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20separate=20into=20?= =?UTF-8?q?diff=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CreatePolicyForm.tsx | 2250 +++++++++++++-------------- 1 file changed, 1076 insertions(+), 1174 deletions(-) diff --git a/src/components/CreatePolicyForm.tsx b/src/components/CreatePolicyForm.tsx index 666c3b7ea..96154edfe 100644 --- a/src/components/CreatePolicyForm.tsx +++ b/src/components/CreatePolicyForm.tsx @@ -57,7 +57,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { toast } from "@app/hooks/useToast"; -import { createApiClient } from "@app/lib/api"; + import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { orgQueries } from "@app/lib/queries"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -91,11 +91,13 @@ import { Key } from "lucide-react"; import { useTranslations } from "next-intl"; -import { useRouter } from "next/navigation"; + import { useActionState, useCallback, useMemo, useState } from "react"; -import { useForm } from "react-hook-form"; +import { UseFormReturn, useForm } from "react-hook-form"; import z from "zod"; +// ─── Schemas & types ────────────────────────────────────────────────────────── + const addRuleSchema = z.object({ action: z.enum(["ACCEPT", "DROP", "PASS"]), match: z.string(), @@ -119,24 +121,9 @@ const createPolicySchema = z.object({ sso: z.boolean().default(true), skipToIdpId: z.number().nullable().optional(), emailWhitelistEnabled: z.boolean().default(false), - roles: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ), - users: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ), - emails: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ), + roles: z.array(z.object({ id: z.string(), text: z.string() })), + users: z.array(z.object({ id: z.string(), text: z.string() })), + emails: z.array(z.object({ id: z.string(), text: z.string() })), applyRules: z.boolean().default(false), rules: z .array( @@ -151,31 +138,31 @@ const createPolicySchema = z.object({ .default([]) }); +type PolicyFormValues = z.infer; + +// ─── CreatePolicyForm ───────────────────────────────────────────────────────── + export type CreatePolicyFormProps = {}; export function CreatePolicyForm({}: CreatePolicyFormProps) { const { org } = useOrgContext(); const t = useTranslations(); const { env } = useEnvContext(); - const api = createApiClient({ env }); const [, formAction, isSubmitting] = useActionState(onSubmit, null); - const router = useRouter(); const { isPaidUser } = usePaidStatus(); - const isMaxmindAvailable = - env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0; - const isMaxmindAsnAvailable = - env.server.maxmind_asn_path && env.server.maxmind_asn_path.length > 0; + const isMaxmindAvailable = !!( + env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0 + ); + const isMaxmindAsnAvailable = !!( + env.server.maxmind_asn_path && env.server.maxmind_asn_path.length > 0 + ); const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery( - orgQueries.roles({ - orgId: org.org.orgId - }) + orgQueries.roles({ orgId: org.org.orgId }) ); const { data: orgUsers = [], isLoading: isLoadingOrgUsers } = useQuery( - orgQueries.users({ - orgId: org.org.orgId - }) + orgQueries.users({ orgId: org.org.orgId }) ); const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery( orgQueries.identityProviders({ @@ -184,8 +171,8 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { }) ); - const form = useForm({ - resolver: zodResolver(createPolicySchema), + const form = useForm({ + resolver: zodResolver(createPolicySchema) as any, defaultValues: { name: "", sso: true, @@ -199,8 +186,135 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { } }); + async function onSubmit() { + // ... + } + + const allRoles = useMemo( + () => + orgRoles + .map((role) => ({ + id: role.roleId.toString(), + text: role.name + })) + .filter((role) => role.text !== "Admin"), + [orgRoles] + ); + + const allUsers = useMemo( + () => + orgUsers.map((user) => ({ + id: user.id.toString(), + text: `${getUserDisplayName({ email: user.email, username: user.username })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` + })), + [orgUsers] + ); + + const allIdps = useMemo(() => { + if (build === "saas") { + if (isPaidUser(tierMatrix.orgOidc)) { + return orgIdps.map((idp) => ({ + id: idp.idpId, + text: idp.name + })); + } + } else { + return orgIdps.map((idp) => ({ id: idp.idpId, text: idp.name })); + } + return []; + }, [orgIdps, isPaidUser]); + + if (isLoadingOrgRoles || isLoadingOrgUsers || isLoadingOrgIdps) { + return <>; + } + + return ( +
+ + + {/* Name */} + + + + {t("resourcePolicyName")} + + + {t("resourcePolicyNameDescription")} + + + + + ( + + {t("name")} + + + + + + )} + /> + + + + + + + + + + +
+ +
+
+ + ); +} + +// ─── PolicyUsersRolesSection ────────────────────────────────────────────────── + +type PolicyUsersRolesSectionProps = { + form: UseFormReturn; + allRoles: { id: string; text: string }[]; + allUsers: { id: string; text: string }[]; + allIdps: { id: number; text: string }[]; +}; + +function PolicyUsersRolesSection({ + form, + allRoles, + allUsers, + allIdps +}: PolicyUsersRolesSectionProps) { + const t = useTranslations(); const [ssoEnabled, setSsoEnabled] = useState(true); - const [whitelistEnabled, setWhitelistEnabled] = useState(false); const [selectedIdpId, setSelectedIdpId] = useState(null); const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< number | null @@ -208,11 +322,364 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { const [activeUsersTagIndex, setActiveUsersTagIndex] = useState< number | null >(null); + + return ( + + + + {t("resourceUsersRoles")} + + + {t("resourceUsersRolesDescription")} + + + + + { + setSsoEnabled(val); + form.setValue("sso", val); + }} + /> + + {ssoEnabled && ( + <> + ( + + {t("roles")} + + { + form.setValue( + "roles", + newRoles as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={true} + autocompleteOptions={allRoles} + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + {t("resourceRoleDescription")} + + + )} + /> + ( + + {t("users")} + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={true} + autocompleteOptions={allUsers} + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + )} + /> + + )} + + {ssoEnabled && allIdps.length > 0 && ( +
+ + +

+ {t("defaultIdentityProviderDescription")} +

+
+ )} +
+
+
+ ); +} + +// ─── PolicyAuthMethodsSection ───────────────────────────────────────────────── + +function PolicyAuthMethodsSection() { + const t = useTranslations(); + return ( + + + + {t("resourceAuthMethods")} + + + {t("resourceAuthMethodsDescriptions")} + + + + +
+
+ + + {t("resourcePasswordProtection", { + status: t("disabled") + })} + +
+ +
+ +
+
+ + + {t("resourcePincodeProtection", { + status: t("disabled") + })} + +
+ +
+ +
+
+ + + {t("resourceHeaderAuthProtectionDisabled")} + +
+ +
+
+
+
+ ); +} + +// ─── PolicyOtpEmailSection ──────────────────────────────────────────────────── + +type PolicyOtpEmailSectionProps = { + form: UseFormReturn; + emailEnabled: boolean; +}; + +function PolicyOtpEmailSection({ + form, + emailEnabled +}: PolicyOtpEmailSectionProps) { + const t = useTranslations(); + const [whitelistEnabled, setWhitelistEnabled] = useState(false); const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< number | null >(null); - // Rules state + return ( + + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + {!emailEnabled && ( + + + + {t("otpEmailSmtpRequired")} + + + {t("otpEmailSmtpRequiredDescription")} + + + )} + { + setWhitelistEnabled(val); + form.setValue("emailWhitelistEnabled", val); + }} + disabled={!emailEnabled} + /> + + {whitelistEnabled && emailEnabled && ( + ( + + + + + + {/* @ts-ignore */} + { + return z + .email() + .or( + z + .string() + .regex( + /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, + { + message: t( + "otpEmailErrorInvalid" + ) + } + ) + ) + .safeParse(tag).success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder={t("otpEmailEnter")} + tags={form.getValues().emails} + setTags={(newEmails) => { + form.setValue( + "emails", + newEmails as [Tag, ...Tag[]] + ); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t("otpEmailEnterDescription")} + + + )} + /> + )} + + + + ); +} + +// ─── PolicyRulesSection ─────────────────────────────────────────────────────── + +type PolicyRulesSectionProps = { + form: UseFormReturn; + isMaxmindAvailable: boolean; + isMaxmindAsnAvailable: boolean; +}; + +function PolicyRulesSection({ + form, + isMaxmindAvailable, + isMaxmindAsnAvailable +}: PolicyRulesSectionProps) { + const t = useTranslations(); const [rules, setRules] = useState([]); const [rulesEnabled, setRulesEnabled] = useState(false); const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = @@ -228,195 +695,162 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { } }); - const RuleAction = useMemo(() => { - return { + const RuleAction = useMemo( + () => ({ ACCEPT: t("alwaysAllow"), DROP: t("alwaysDeny"), PASS: t("passToAuth") - } as const; - }, [t]); + }), + [t] + ); - const RuleMatch = useMemo(() => { - return { + const RuleMatch = useMemo( + () => ({ PATH: t("path"), IP: "IP", CIDR: t("ipAddressRange"), COUNTRY: t("country"), ASN: "ASN" - } as const; - }, [t]); + }), + [t] + ); - async function onSubmit() { - // ... - } - - const addRule = useCallback(function addRule(data: z.infer) { - const isDuplicate = rules.some( - (rule) => - rule.action === data.action && - rule.match === data.match && - rule.value === data.value - ); - - if (isDuplicate) { - toast({ - variant: "destructive", - title: t("rulesErrorDuplicate"), - description: t("rulesErrorDuplicateDescription") - }); - return; - } - - if (data.match === "CIDR" && !isValidCIDR(data.value)) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidIpAddressRange"), - description: t("rulesErrorInvalidIpAddressRangeDescription") - }); - return; - } - if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidUrl"), - description: t("rulesErrorInvalidUrlDescription") - }); - return; - } - if (data.match === "IP" && !isValidIP(data.value)) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidIpAddress"), - description: t("rulesErrorInvalidIpAddressDescription") - }); - return; - } - if ( - data.match === "COUNTRY" && - !COUNTRIES.some((c) => c.code === data.value) - ) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidCountry"), - description: t("rulesErrorInvalidCountryDescription") || "" - }); - return; - } - - let priority = data.priority; - if (priority === undefined) { - priority = rules.reduce( - (acc, rule) => (rule.priority > acc ? rule.priority : acc), - 0 + const syncFormRules = useCallback( + (updatedRules: LocalRule[]) => { + form.setValue( + "rules", + updatedRules.map( + ({ action, match, value, priority, enabled }) => ({ + action, + match, + value, + priority, + enabled + }) + ) ); - priority++; - } + }, + [form] + ); - const newRule: LocalRule = { - ...data, - ruleId: new Date().getTime(), - new: true, - priority, - enabled: true - }; - - const updatedRules = [...rules, newRule]; - setRules(updatedRules); - form.setValue( - "rules", - updatedRules.map(({ action, match, value, priority, enabled }) => ({ - action, - match, - value, - priority, - enabled - })) - ); - addRuleForm.reset(); - }, [rules, t, form, addRuleForm]); - - const removeRule = useCallback(function removeRule(ruleId: number) { - const updatedRules = rules.filter((rule) => rule.ruleId !== ruleId); - setRules(updatedRules); - form.setValue( - "rules", - updatedRules.map(({ action, match, value, priority, enabled }) => ({ - action, - match, - value, - priority, - enabled - })) - ); - }, [rules, form]); - - const updateRule = useCallback(function updateRule(ruleId: number, data: Partial) { - const updatedRules = rules.map((rule) => - rule.ruleId === ruleId ? { ...rule, ...data, updated: true } : rule - ); - setRules(updatedRules); - form.setValue( - "rules", - updatedRules.map(({ action, match, value, priority, enabled }) => ({ - action, - match, - value, - priority, - enabled - })) - ); - }, [rules, form]); - - const getValueHelpText = useCallback(function getValueHelpText(type: string) { - switch (type) { - case "CIDR": - return t("rulesMatchIpAddressRangeDescription"); - case "IP": - return t("rulesMatchIpAddress"); - case "PATH": - return t("rulesMatchUrl"); - case "COUNTRY": - return t("rulesMatchCountry"); - case "ASN": - return "Enter an Autonomous System Number (e.g., AS15169 or 15169)"; - } - }, [t]); - - const allRoles = useMemo(() => { - return orgRoles - .map((role) => ({ - id: role.roleId.toString(), - text: role.name - })) - .filter((role) => role.text !== "Admin"); - }, [orgRoles]); - - const allUsers = useMemo(() => { - return orgUsers.map((user) => ({ - id: user.id.toString(), - text: `${getUserDisplayName({ - email: user.email, - username: user.username - })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` - })); - }, [orgUsers]); - - const allIdps = useMemo(() => { - if (build === "saas") { - if (isPaidUser(tierMatrix.orgOidc)) { - return orgIdps.map((idp) => ({ - id: idp.idpId, - text: idp.name - })); + const addRule = useCallback( + function addRule(data: z.infer) { + const isDuplicate = rules.some( + (rule) => + rule.action === data.action && + rule.match === data.match && + rule.value === data.value + ); + if (isDuplicate) { + toast({ + variant: "destructive", + title: t("rulesErrorDuplicate"), + description: t("rulesErrorDuplicateDescription") + }); + return; } - } else { - return orgIdps.map((idp) => ({ - id: idp.idpId, - text: idp.name - })); - } - return []; - }, [orgIdps]); + if (data.match === "CIDR" && !isValidCIDR(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidIpAddressRange"), + description: t("rulesErrorInvalidIpAddressRangeDescription") + }); + return; + } + if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidUrl"), + description: t("rulesErrorInvalidUrlDescription") + }); + return; + } + if (data.match === "IP" && !isValidIP(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidIpAddress"), + description: t("rulesErrorInvalidIpAddressDescription") + }); + return; + } + if ( + data.match === "COUNTRY" && + !COUNTRIES.some((c) => c.code === data.value) + ) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidCountry"), + description: t("rulesErrorInvalidCountryDescription") || "" + }); + return; + } + + let priority = data.priority; + if (priority === undefined) { + priority = + rules.reduce( + (acc, rule) => + rule.priority > acc ? rule.priority : acc, + 0 + ) + 1; + } + + const updatedRules = [ + ...rules, + { + ...data, + ruleId: new Date().getTime(), + new: true, + priority, + enabled: true + } + ]; + setRules(updatedRules); + syncFormRules(updatedRules); + addRuleForm.reset(); + }, + [rules, t, addRuleForm, syncFormRules] + ); + + const removeRule = useCallback( + function removeRule(ruleId: number) { + const updatedRules = rules.filter((rule) => rule.ruleId !== ruleId); + setRules(updatedRules); + syncFormRules(updatedRules); + }, + [rules, syncFormRules] + ); + + const updateRule = useCallback( + function updateRule(ruleId: number, data: Partial) { + const updatedRules = rules.map((rule) => + rule.ruleId === ruleId + ? { ...rule, ...data, updated: true } + : rule + ); + setRules(updatedRules); + syncFormRules(updatedRules); + }, + [rules, syncFormRules] + ); + + const getValueHelpText = useCallback( + function getValueHelpText(type: string) { + switch (type) { + case "CIDR": + return t("rulesMatchIpAddressRangeDescription"); + case "IP": + return t("rulesMatchIpAddress"); + case "PATH": + return t("rulesMatchUrl"); + case "COUNTRY": + return t("rulesMatchCountry"); + case "ASN": + return "Enter an Autonomous System Number (e.g., AS15169 or 15169)"; + } + }, + [t] + ); const columns: ColumnDef[] = useMemo( () => [ @@ -550,9 +984,8 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { > {row.original.value ? COUNTRIES.find( - (country) => - country.code === - row.original.value + (c) => + c.code === row.original.value )?.name + " (" + row.original.value + @@ -575,23 +1008,17 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { { + onSelect={() => updateRule( row.original.ruleId, { value: country.code } - ); - }} + ) + } > {country.name} ( {country.code}) @@ -642,21 +1069,15 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { " " + asn.code } - onSelect={() => { + onSelect={() => updateRule( row.original.ruleId, { value: asn.code } - ); - }} + ) + } > {asn.name} ({asn.code}) @@ -752,958 +1173,439 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), - state: { - pagination: { - pageIndex: 0, - pageSize: 1000 - } - } + state: { pagination: { pageIndex: 0, pageSize: 1000 } } }); - const pageLoading = - isLoadingOrgRoles || isLoadingOrgUsers || isLoadingOrgIdps; - - if (pageLoading) { - return <>; - } - return ( -
- - - {/* Name */} - - - - {t("resourcePolicyName")} - - - {t("resourcePolicyNameDescription")} - - - - + + + + {t("rulesResource")} + + + {t("rulesResourceDescription")} + + + +
+
+ { + setRulesEnabled(val); + form.setValue("applyRules", val); + }} + /> +
+ + + +
( - {t("name")} + + {t("rulesAction")} + - + )} /> - - - - - {/* Users & Roles */} - - - - {t("resourceUsersRoles")} - - - {t("resourceUsersRolesDescription")} - - - - - { - setSsoEnabled(val); - form.setValue("sso", val); - }} - /> - - {ssoEnabled && ( - <> - ( - - - {t("roles")} - - - { - form.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allRoles - } - allowDuplicates={ - false - } - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - {t( - "resourceRoleDescription" - )} - - - )} - /> - ( - - - {t("users")} - - - { - form.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allUsers - } - allowDuplicates={ - false - } - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - )} - /> - - )} - - {ssoEnabled && allIdps.length > 0 && ( -
- - -

- {t( - "defaultIdentityProviderDescription" - )} -

-
- )} -
-
-
- - {/* Auth Methods */} - - - - {t("resourceAuthMethods")} - - - {t("resourceAuthMethodsDescriptions")} - - - - -
-
- - - {t("resourcePasswordProtection", { - status: t("disabled") - })} - -
- -
- -
-
- - - {t("resourcePincodeProtection", { - status: t("disabled") - })} - -
- -
- -
-
- - - {t( - "resourceHeaderAuthProtectionDisabled" - )} - -
- -
-
-
-
- - {/* OTP Email */} - - - - {t("otpEmailTitle")} - - - {t("otpEmailTitleDescription")} - - - - - {!env.email.emailEnabled && ( - - - - {t("otpEmailSmtpRequired")} - - - {t( - "otpEmailSmtpRequiredDescription" - )} - - - )} - { - setWhitelistEnabled(val); - form.setValue( - "emailWhitelistEnabled", - val - ); - }} - disabled={!env.email.emailEnabled} - /> - - {whitelistEnabled && env.email.emailEnabled && ( - ( - - - - - - {/* @ts-ignore */} - { - return z - .email() - .or( - z - .string() - .regex( - /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, - { - message: - t( - "otpEmailErrorInvalid" - ) - } - ) - ) - .safeParse(tag) - .success; - }} - setActiveTagIndex={ - setActiveEmailTagIndex - } - placeholder={t( - "otpEmailEnter" - )} - tags={ - form.getValues() - .emails - } - setTags={( - newEmails - ) => { - form.setValue( - "emails", - newEmails as [ - Tag, - ...Tag[] - ] - ); - }} - allowDuplicates={false} - sortTags={true} - /> - - - {t( - "otpEmailEnterDescription" - )} - - - )} - /> - )} - - - - - {/* Rules */} - - - - {t("rulesResource")} - - - {t("rulesResourceDescription")} - - - -
-
- { - setRulesEnabled(val); - form.setValue("applyRules", val); - }} - /> -
- - - -
- ( - - - {t("rulesAction")} - - - - - - - )} - /> - ( - - - {t( - "rulesMatchType" - )} - - - - - - - )} - /> - ( - - - - {addRuleForm.watch( - "match" - ) === "COUNTRY" ? ( - - - - - - - - - - {t( - "noCountryFound" - )} - - - {COUNTRIES.map( - ( - country - ) => ( - { - field.onChange( - country.code - ); - setOpenAddRuleCountrySelect( - false - ); - }} - > - - { - country.name - }{" "} - ( - { - country.code - } - - ) - - ) - )} - - - - - - ) : addRuleForm.watch( - "match" - ) === "ASN" ? ( - - - - - - - - - - No - ASN - found. - Use - the - custom - input - below. - - - {MAJOR_ASNS.map( - ( - asn - ) => ( - { - field.onChange( - asn.code - ); - setOpenAddRuleAsnSelect( - false - ); - }} - > - - { - asn.name - }{" "} - ( - { - asn.code - } - - ) - - ) - )} - - - -
- { - if ( - e.key === - "Enter" - ) { - const value = - e.currentTarget.value - .toUpperCase() - .replace( - /^AS/, - "" - ); - if ( - /^\d+$/.test( - value - ) - ) { - field.onChange( - "AS" + - value - ); - setOpenAddRuleAsnSelect( - false - ); - } - } - }} - className="text-sm" - /> -
-
-
- ) : ( - - )} -
- -
- )} - /> - -
- - - - - - {table - .getHeaderGroups() - .map((headerGroup) => ( - - {headerGroup.headers.map( - (header) => { - const isActionsColumn = - header.column - .id === - "actions"; - return ( - - {header.isPlaceholder - ? null - : flexRender( - header - .column - .columnDef - .header, - header.getContext() - )} - - ); - } - )} - - ))} - - - {table.getRowModel().rows?.length ? ( - table - .getRowModel() - .rows.map((row) => ( - - {row - .getVisibleCells() - .map((cell) => { - const isActionsColumn = - cell.column - .id === - "actions"; - return ( - - {flexRender( - cell - .column - .columnDef - .cell, - cell.getContext() - )} - - ); - })} - - )) - ) : ( - - ( + + + {t("rulesMatchType")} + + +
-
-
-
- + + + + + + {RuleMatch.PATH} + + + {RuleMatch.IP} + + + {RuleMatch.CIDR} + + {isMaxmindAvailable && ( + + { + RuleMatch.COUNTRY + } + + )} + {isMaxmindAsnAvailable && ( + + {RuleMatch.ASN} + + )} + + + + + + )} + /> + ( + + + + {addRuleForm.watch("match") === + "COUNTRY" ? ( + + + + + + + + + + {t( + "noCountryFound" + )} + + + {COUNTRIES.map( + ( + country + ) => ( + { + field.onChange( + country.code + ); + setOpenAddRuleCountrySelect( + false + ); + }} + > + + { + country.name + }{" "} + ( + { + country.code + } -
- + ) + + ) + )} + + + + + + ) : addRuleForm.watch( + "match" + ) === "ASN" ? ( + + + + + + + + + + No ASN + found. + Use the + custom + input + below. + + + {MAJOR_ASNS.map( + ( + asn + ) => ( + { + field.onChange( + asn.code + ); + setOpenAddRuleAsnSelect( + false + ); + }} + > + + { + asn.name + }{" "} + ( + { + asn.code + } + + ) + + ) + )} + + + +
+ { + if ( + e.key === + "Enter" + ) { + const value = + e.currentTarget.value + .toUpperCase() + .replace( + /^AS/, + "" + ); + if ( + /^\d+$/.test( + value + ) + ) { + field.onChange( + "AS" + + value + ); + setOpenAddRuleAsnSelect( + false + ); + } + } + }} + className="text-sm" + /> +
+
+
+ ) : ( + + )} + + + + )} + /> + +
+ + + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const isActionsColumn = + header.column.id === "actions"; + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column + .columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const isActionsColumn = + cell.column.id === "actions"; + return ( + + {flexRender( + cell.column.columnDef + .cell, + cell.getContext() + )} + + ); + })} + + )) + ) : ( + + + {t("rulesNoOne")} + + + )} + +
- - + + ); } From 003bf7fdf32a4ec52de5ff4f6d8222f141961f0c Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 19 Feb 2026 04:59:51 +0100 Subject: [PATCH 020/771] =?UTF-8?q?=F0=9F=9A=B8=20hide=20otp,=20rules=20an?= =?UTF-8?q?d=20resource=20rules=20config=20by=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 6 +- .../resources/policies/create/page.tsx | 2 +- src/components/CreatePolicyForm.tsx | 91 +++++++++++++++++-- src/components/tags/tag-popover.tsx | 10 +- 4 files changed, 98 insertions(+), 11 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 58a772967..91cf42c53 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -176,7 +176,11 @@ "resourcePolicyName": "Policy Name", "resourcePolicyNameDescription": "Give this policy a name to identify it across your resources", "resourcePolicyNamePlaceholder": "e.g. Internal Access Policy", - "policiesSeeAll": "See All Policies", + "resourcePoliciesSeeAll": "See All Policies", + "resourcePolicyAuthMethodAdd": "Add Authentication Method", + "resourcePolicyOtpEmailAdd": "Add OTP emails", + "resourcePolicyRulesAdd": "Add Rules", + "rulesResourcePolicyDescription": "Configure rules to control access resources associated to this policy", "authentication": "Authentication", "protected": "Protected", "notProtected": "Not Protected", diff --git a/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx b/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx index e43ef39ee..19fd2ca37 100644 --- a/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/resources/policies/create/page.tsx @@ -35,7 +35,7 @@ export default async function CreateResourcePolicyPage(
diff --git a/src/components/CreatePolicyForm.tsx b/src/components/CreatePolicyForm.tsx index 96154edfe..01d56fa46 100644 --- a/src/components/CreatePolicyForm.tsx +++ b/src/components/CreatePolicyForm.tsx @@ -88,7 +88,8 @@ import { Check, ChevronsUpDown, InfoIcon, - Key + Key, + Plus } from "lucide-react"; import { useTranslations } from "next-intl"; @@ -314,8 +315,8 @@ function PolicyUsersRolesSection({ allIdps }: PolicyUsersRolesSectionProps) { const t = useTranslations(); - const [ssoEnabled, setSsoEnabled] = useState(true); - const [selectedIdpId, setSelectedIdpId] = useState(null); + const ssoEnabled = form.watch("sso"); + const selectedIdpId = form.watch("skipToIdpId"); const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< number | null >(null); @@ -338,9 +339,8 @@ function PolicyUsersRolesSection({ { - setSsoEnabled(val); form.setValue("sso", val); }} /> @@ -445,11 +445,9 @@ function PolicyUsersRolesSection({ + + + + )} + /> +
+
+
+ + + + + +
+ +
+ +
+ + + ); +} diff --git a/src/components/CreatePolicyForm.tsx b/src/components/resource-policy/ResourcePolicySubForms.tsx similarity index 79% rename from src/components/CreatePolicyForm.tsx rename to src/components/resource-policy/ResourcePolicySubForms.tsx index 01d56fa46..bcb0f0470 100644 --- a/src/components/CreatePolicyForm.tsx +++ b/src/components/resource-policy/ResourcePolicySubForms.tsx @@ -1,7 +1,6 @@ "use client"; import { - SettingsContainer, SettingsSection, SettingsSectionBody, SettingsSectionDescription, @@ -53,25 +52,31 @@ import { TableHeader, TableRow } from "@app/components/ui/table"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useOrgContext } from "@app/hooks/useOrgContext"; -import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; import { toast } from "@app/hooks/useToast"; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot +} from "@app/components/ui/input-otp"; -import { getUserDisplayName } from "@app/lib/getUserDisplayName"; -import { orgQueries } from "@app/lib/queries"; import { zodResolver } from "@hookform/resolvers/zod"; -import { build } from "@server/build"; import { MAJOR_ASNS } from "@server/db/asns"; import { COUNTRIES } from "@server/db/countries"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "@server/lib/validators"; -import { UserType } from "@server/types/UserTypes"; -import { useQuery } from "@tanstack/react-query"; import { ColumnDef, flexRender, @@ -93,11 +98,10 @@ import { } from "lucide-react"; import { useTranslations } from "next-intl"; -import { useActionState, useCallback, useMemo, useState } from "react"; +import { useCallback, useMemo, useState } from "react"; import { UseFormReturn, useForm } from "react-hook-form"; import z from "zod"; - -// ─── Schemas & types ────────────────────────────────────────────────────────── +import type { PolicyFormValues } from "."; const addRuleSchema = z.object({ action: z.enum(["ACCEPT", "DROP", "PASS"]), @@ -117,190 +121,6 @@ type LocalRule = { updated?: boolean; }; -const createPolicySchema = z.object({ - name: z.string().min(1).max(255), - sso: z.boolean().default(true), - skipToIdpId: z.number().nullable().optional(), - emailWhitelistEnabled: z.boolean().default(false), - roles: z.array(z.object({ id: z.string(), text: z.string() })), - users: z.array(z.object({ id: z.string(), text: z.string() })), - emails: z.array(z.object({ id: z.string(), text: z.string() })), - applyRules: z.boolean().default(false), - rules: z - .array( - z.object({ - action: z.enum(["ACCEPT", "DROP", "PASS"]), - match: z.string(), - value: z.string(), - priority: z.number().int(), - enabled: z.boolean() - }) - ) - .default([]) -}); - -type PolicyFormValues = z.infer; - -// ─── CreatePolicyForm ───────────────────────────────────────────────────────── - -export type CreatePolicyFormProps = {}; - -export function CreatePolicyForm({}: CreatePolicyFormProps) { - const { org } = useOrgContext(); - const t = useTranslations(); - const { env } = useEnvContext(); - const [, formAction, isSubmitting] = useActionState(onSubmit, null); - const { isPaidUser } = usePaidStatus(); - - const isMaxmindAvailable = !!( - env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0 - ); - const isMaxmindAsnAvailable = !!( - env.server.maxmind_asn_path && env.server.maxmind_asn_path.length > 0 - ); - - const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery( - orgQueries.roles({ orgId: org.org.orgId }) - ); - const { data: orgUsers = [], isLoading: isLoadingOrgUsers } = useQuery( - orgQueries.users({ orgId: org.org.orgId }) - ); - const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery( - orgQueries.identityProviders({ - orgId: org.org.orgId, - useOrgOnlyIdp: env.app.identityProviderMode === "org" - }) - ); - - const form = useForm({ - resolver: zodResolver(createPolicySchema) as any, - defaultValues: { - name: "", - sso: true, - skipToIdpId: null, - emailWhitelistEnabled: false, - roles: [], - users: [], - emails: [], - applyRules: false, - rules: [] - } - }); - - async function onSubmit() { - // ... - } - - const allRoles = useMemo( - () => - orgRoles - .map((role) => ({ - id: role.roleId.toString(), - text: role.name - })) - .filter((role) => role.text !== "Admin"), - [orgRoles] - ); - - const allUsers = useMemo( - () => - orgUsers.map((user) => ({ - id: user.id.toString(), - text: `${getUserDisplayName({ email: user.email, username: user.username })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` - })), - [orgUsers] - ); - - const allIdps = useMemo(() => { - if (build === "saas") { - if (isPaidUser(tierMatrix.orgOidc)) { - return orgIdps.map((idp) => ({ - id: idp.idpId, - text: idp.name - })); - } - } else { - return orgIdps.map((idp) => ({ id: idp.idpId, text: idp.name })); - } - return []; - }, [orgIdps, isPaidUser]); - - if (isLoadingOrgRoles || isLoadingOrgUsers || isLoadingOrgIdps) { - return <>; - } - - return ( -
- - - {/* Name */} - - - - {t("resourcePolicyName")} - - - {t("resourcePolicyNameDescription")} - - - - - ( - - {t("name")} - - - - - - )} - /> - - - - - - - - - - -
- -
-
- - ); -} - -// ─── PolicyUsersRolesSection ────────────────────────────────────────────────── - type PolicyUsersRolesSectionProps = { form: UseFormReturn; allRoles: { id: string; text: string }[]; @@ -308,7 +128,9 @@ type PolicyUsersRolesSectionProps = { allIdps: { id: number; text: string }[]; }; -function PolicyUsersRolesSection({ +// ─── PolicyUsersRolesSection ────────────────────────────────────────────────── + +export function PolicyUsersRolesSection({ form, allRoles, allUsers, @@ -331,7 +153,7 @@ function PolicyUsersRolesSection({ {t("resourceUsersRoles")}
- {t("resourceUsersRolesDescription")} + {t("resourcePolicyUsersRolesDescription")} @@ -489,9 +311,51 @@ function PolicyUsersRolesSection({ // ─── PolicyAuthMethodsSection ───────────────────────────────────────────────── -function PolicyAuthMethodsSection() { +const setPasswordSchema = z.object({ + password: z.string().min(4).max(100) +}); + +const setPincodeSchema = z.object({ + pincode: z.string().length(6) +}); + +const setHeaderAuthSchema = z.object({ + user: z.string().min(4).max(100), + password: z.string().min(4).max(100), + extendedCompatibility: z.boolean() +}); + +type PolicyAuthMethodsSectionProps = { + form: UseFormReturn; +}; + +export function PolicyAuthMethodsSection({ + form +}: PolicyAuthMethodsSectionProps) { const t = useTranslations(); const [isOpen, setIsOpen] = useState(false); + const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); + const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); + const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); + + const password = form.watch("password"); + const pincode = form.watch("pincode"); + const headerAuth = form.watch("headerAuth"); + + const passwordForm = useForm({ + resolver: zodResolver(setPasswordSchema), + defaultValues: { password: "" } + }); + + const pincodeForm = useForm({ + resolver: zodResolver(setPincodeSchema), + defaultValues: { pincode: "" } + }); + + const headerAuthForm = useForm({ + resolver: zodResolver(setHeaderAuthSchema), + defaultValues: { user: "", password: "", extendedCompatibility: true } + }); if (!isOpen) { return ( @@ -501,7 +365,7 @@ function PolicyAuthMethodsSection() { {t("resourceAuthMethods")}
- {t("resourceAuthMethodsDescriptions")} + {t("resourcePolicyAuthMethodsDescription")} @@ -519,59 +383,359 @@ function PolicyAuthMethodsSection() { } return ( - - - - {t("resourceAuthMethods")} - - - {t("resourceAuthMethodsDescriptions")} - - - - -
-
- - - {t("resourcePasswordProtection", { - status: t("disabled") + <> + {/* Password Credenza */} + { + setIsSetPasswordOpen(val); + if (!val) passwordForm.reset(); + }} + > + + + + {t("resourcePasswordSetupTitle")} + + + {t("resourcePasswordSetupTitleDescription")} + + + +
+ { + form.setValue("password", data); + setIsSetPasswordOpen(false); + passwordForm.reset(); })} - -
- + + -
+ + + -
-
- - - {t("resourcePincodeProtection", { - status: t("disabled") + {/* Pincode Credenza */} + { + setIsSetPincodeOpen(val); + if (!val) pincodeForm.reset(); + }} + > + + + + {t("resourcePincodeSetupTitle")} + + + {t("resourcePincodeSetupTitleDescription")} + + + +
+ { + form.setValue("pincode", data); + setIsSetPincodeOpen(false); + pincodeForm.reset(); })} - -
- + + -
+ + + -
-
- - - {t("resourceHeaderAuthProtectionDisabled")} - -
- + + -
-
-
-
+ + + + + + + + {t("resourceAuthMethods")} + + + {t("resourcePolicyAuthMethodsDescription")} + + + + + {/* Password row */} +
+
+ + + {t("resourcePasswordProtection", { + status: password + ? t("enabled") + : t("disabled") + })} + +
+ +
+ + {/* Pincode row */} +
+
+ + + {t("resourcePincodeProtection", { + status: pincode + ? t("enabled") + : t("disabled") + })} + +
+ +
+ + {/* Header auth row */} +
+
+ + + {headerAuth + ? t("resourceHeaderAuthProtectionEnabled") + : t("resourceHeaderAuthProtectionDisabled")} + +
+ +
+
+
+
+ ); } @@ -582,7 +746,7 @@ type PolicyOtpEmailSectionProps = { emailEnabled: boolean; }; -function PolicyOtpEmailSection({ +export function PolicyOtpEmailSection({ form, emailEnabled }: PolicyOtpEmailSectionProps) { @@ -725,7 +889,7 @@ type PolicyRulesSectionProps = { isMaxmindAsnAvailable: boolean; }; -function PolicyRulesSection({ +export function PolicyRulesSection({ form, isMaxmindAvailable, isMaxmindAsnAvailable diff --git a/src/components/resource-policy/index.ts b/src/components/resource-policy/index.ts new file mode 100644 index 000000000..7b77faddb --- /dev/null +++ b/src/components/resource-policy/index.ts @@ -0,0 +1,47 @@ +// ─── Schemas & types ────────────────────────────────────────────────────────── + +import z from "zod"; + +export const createPolicySchema = z.object({ + name: z.string().min(1).max(255), + sso: z.boolean().default(true), + skipToIdpId: z.number().nullable().optional(), + emailWhitelistEnabled: z.boolean().default(false), + roles: z.array(z.object({ id: z.string(), text: z.string() })), + users: z.array(z.object({ id: z.string(), text: z.string() })), + emails: z.array(z.object({ id: z.string(), text: z.string() })), + password: z + .object({ + password: z.string().min(4).max(100) + }) + .nullable() + .default(null), + pincode: z + .object({ + pincode: z.string().regex(/^\d{6}$/) + }) + .nullable() + .default(null), + headerAuth: z + .object({ + user: z.string().min(4).max(100), + password: z.string().min(4).max(100), + extendedCompatibility: z.boolean().default(true) + }) + .nullable() + .default(null), + applyRules: z.boolean().default(false), + rules: z + .array( + z.object({ + action: z.enum(["ACCEPT", "DROP", "PASS"]), + match: z.string(), + value: z.string(), + priority: z.number().int(), + enabled: z.boolean() + }) + ) + .default([]) +}); + +export type PolicyFormValues = z.infer; From ba9a0c5e3cc3b9f72ecaada46200734b1fb7b071 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 19 Feb 2026 05:23:20 +0100 Subject: [PATCH 022/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/resource-policy/CreatePolicyForm.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/resource-policy/CreatePolicyForm.tsx b/src/components/resource-policy/CreatePolicyForm.tsx index a2288268f..05e7aea99 100644 --- a/src/components/resource-policy/CreatePolicyForm.tsx +++ b/src/components/resource-policy/CreatePolicyForm.tsx @@ -85,7 +85,10 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { users: [], emails: [], applyRules: false, - rules: [] + rules: [], + password: null, + headerAuth: null, + pincode: null } }); From 267b40b73c02e9d1eb559b20a2f5dbe371f9324b Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 19 Feb 2026 05:27:05 +0100 Subject: [PATCH 023/771] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../private/routers/resource/createResourcePolicy.ts | 12 ++++++++++++ server/private/routers/resource/index.ts | 1 + 2 files changed, 13 insertions(+) create mode 100644 server/private/routers/resource/createResourcePolicy.ts diff --git a/server/private/routers/resource/createResourcePolicy.ts b/server/private/routers/resource/createResourcePolicy.ts new file mode 100644 index 000000000..76d0133ef --- /dev/null +++ b/server/private/routers/resource/createResourcePolicy.ts @@ -0,0 +1,12 @@ +import { Request, Response, NextFunction } from "express"; +import z from "zod"; + +const createResourcePolicyParamsSchema = z.strictObject({ + orgId: z.string() +}); + +export async function createResourcePolicy( + req: Request, + res: Response, + next: NextFunction +) {} diff --git a/server/private/routers/resource/index.ts b/server/private/routers/resource/index.ts index 4bae8e982..0d217b671 100644 --- a/server/private/routers/resource/index.ts +++ b/server/private/routers/resource/index.ts @@ -13,3 +13,4 @@ export * from "./getMaintenanceInfo"; export * from "./listResourcePolicies"; +export * from "./createResourcePolicy"; From 0e4abdf4b6deced00e6c456d0155b4de550d35d7 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 20 Feb 2026 02:06:23 +0100 Subject: [PATCH 024/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20usewatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ResourcePolicySubForms.tsx | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/components/resource-policy/ResourcePolicySubForms.tsx b/src/components/resource-policy/ResourcePolicySubForms.tsx index bcb0f0470..3b0056ba4 100644 --- a/src/components/resource-policy/ResourcePolicySubForms.tsx +++ b/src/components/resource-policy/ResourcePolicySubForms.tsx @@ -99,7 +99,7 @@ import { import { useTranslations } from "next-intl"; import { useCallback, useMemo, useState } from "react"; -import { UseFormReturn, useForm } from "react-hook-form"; +import { UseFormReturn, useForm, useWatch } from "react-hook-form"; import z from "zod"; import type { PolicyFormValues } from "."; @@ -137,8 +137,11 @@ export function PolicyUsersRolesSection({ allIdps }: PolicyUsersRolesSectionProps) { const t = useTranslations(); - const ssoEnabled = form.watch("sso"); - const selectedIdpId = form.watch("skipToIdpId"); + const ssoEnabled = useWatch({ control: form.control, name: "sso" }); + const selectedIdpId = useWatch({ + control: form.control, + name: "skipToIdpId" + }); const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< number | null >(null); @@ -163,6 +166,7 @@ export function PolicyUsersRolesSection({ label={t("ssoUse")} defaultChecked={ssoEnabled} onCheckedChange={(val) => { + console.log(`form.setValue("sso", ${val})`); form.setValue("sso", val); }} /> @@ -555,11 +559,13 @@ export function PolicyAuthMethodsSection({
{ - form.setValue("headerAuth", data); - setIsSetHeaderAuthOpen(false); - headerAuthForm.reset(); - })} + onSubmit={headerAuthForm.handleSubmit( + (data) => { + form.setValue("headerAuth", data); + setIsSetHeaderAuthOpen(false); + headerAuthForm.reset(); + } + )} className="space-y-4" id="set-header-auth-form" > @@ -672,7 +678,9 @@ export function PolicyAuthMethodsSection({ : () => setIsSetPasswordOpen(true) } > - {password ? t("passwordRemove") : t("passwordAdd")} + {password + ? t("passwordRemove") + : t("passwordAdd")} @@ -712,8 +720,12 @@ export function PolicyAuthMethodsSection({ {headerAuth - ? t("resourceHeaderAuthProtectionEnabled") - : t("resourceHeaderAuthProtectionDisabled")} + ? t( + "resourceHeaderAuthProtectionEnabled" + ) + : t( + "resourceHeaderAuthProtectionDisabled" + )} + + + + ); +} From c5231d37f69274309e8634fff996a170cd3191ae Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 26 Feb 2026 19:20:15 +0100 Subject: [PATCH 028/771] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 2 + server/auth/actions.ts | 1 + server/middlewares/integration/index.ts | 1 + .../verifyApiKeyResourcePolicyAccess.ts | 92 + .../routers/policy/updateResourcePolicy.ts | 1 - server/routers/external.ts | 11 + server/routers/integration.ts | 11 +- server/routers/policy/getResourcePolicy.ts | 123 + server/routers/policy/index.ts | 1 + .../settings/(private)/policies/layout.tsx | 23 + .../policies/resource/[niceId]/page.tsx | 58 + .../{resources => resource}/create/page.tsx | 17 +- .../policies/{resources => resource}/page.tsx | 20 +- src/app/navigation.tsx | 2 +- src/components/ResourcePoliciesTable.tsx | 6 +- .../resource-policy/CreatePolicyForm.tsx | 1870 +++++++++++++++- .../resource-policy/EditPolicyForm.tsx | 1978 ++++++++++++++++- .../ResourcePolicySubForms.tsx | 58 +- src/components/resource-policy/index.ts | 18 + 19 files changed, 4177 insertions(+), 116 deletions(-) create mode 100644 server/middlewares/integration/verifyApiKeyResourcePolicyAccess.ts create mode 100644 server/routers/policy/getResourcePolicy.ts create mode 100644 server/routers/policy/index.ts create mode 100644 src/app/[orgId]/settings/(private)/policies/layout.tsx create mode 100644 src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx rename src/app/[orgId]/settings/(private)/policies/{resources => resource}/create/page.tsx (62%) rename src/app/[orgId]/settings/(private)/policies/{resources => resource}/page.tsx (83%) diff --git a/messages/en-US.json b/messages/en-US.json index fb827ffe3..78b4ad0ba 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -238,6 +238,8 @@ "rules": "Rules", "resourceSettingDescription": "Configure the settings on the resource", "resourceSetting": "{resourceName} Settings", + "resourcePolicySettingDescription": "Configure the settings on the resource policy", + "resourcePolicySetting": "{policyName} Settings", "alwaysAllow": "Bypass Auth", "alwaysDeny": "Block Access", "passToAuth": "Pass to Auth", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 6e863829e..bcb5df50c 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -133,6 +133,7 @@ export enum ActionsEnum { listApprovals = "listApprovals", updateApprovals = "updateApprovals", listResourcePolicies = "listResourcePolicies", + getResourcePolicy = "getResourcePolicy", createResourcePolicy = "createResourcePolicy", updateResourcePolicy = "updateResourcePolicy", deleteResourcePolicy = "deleteResourcePolicy", diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts index 565751913..fa004bd8c 100644 --- a/server/middlewares/integration/index.ts +++ b/server/middlewares/integration/index.ts @@ -14,3 +14,4 @@ export * from "./verifyApiKeyApiKeyAccess"; export * from "./verifyApiKeyClientAccess"; export * from "./verifyApiKeySiteResourceAccess"; export * from "./verifyApiKeyIdpAccess"; +export * from "./verifyApiKeyResourcePolicyAccess"; diff --git a/server/middlewares/integration/verifyApiKeyResourcePolicyAccess.ts b/server/middlewares/integration/verifyApiKeyResourcePolicyAccess.ts new file mode 100644 index 000000000..2d997de53 --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyResourcePolicyAccess.ts @@ -0,0 +1,92 @@ +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { resourcePolicies, apiKeyOrg } from "@server/db"; +import { eq, and } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeyResourcePolicyAccess( + req: Request, + res: Response, + next: NextFunction +) { + const apiKey = req.apiKey; + const resourcePolicyId = + req.params.resourcePolicyId || + req.body.resourcePolicyId || + req.query.resourcePolicyId; + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + try { + // Retrieve the resource policy + const [policy] = await db + .select() + .from(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)) + .limit(1); + + if (!policy) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource policy with ID ${resourcePolicyId} not found` + ) + ); + } + + if (apiKey.isRoot) { + // Root keys can access any resource policy in any org + return next(); + } + + if (!policy.orgId) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + `Resource policy with ID ${resourcePolicyId} does not have an organization ID` + ) + ); + } + + // Verify that the API key is linked to the resource policy's organization + if (!req.apiKeyOrg) { + const apiKeyOrgResult = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, policy.orgId) + ) + ) + .limit(1); + + if (apiKeyOrgResult.length > 0) { + req.apiKeyOrg = apiKeyOrgResult[0]; + } + } + + if (!req.apiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying resource policy access" + ) + ); + } +} diff --git a/server/private/routers/policy/updateResourcePolicy.ts b/server/private/routers/policy/updateResourcePolicy.ts index 58ea688cc..1f4ff5971 100644 --- a/server/private/routers/policy/updateResourcePolicy.ts +++ b/server/private/routers/policy/updateResourcePolicy.ts @@ -10,7 +10,6 @@ import { resourcePolicies, rolePolicies, userPolicies, - type ResourcePolicy, type ResourcePolicy } from "@server/db"; import { and, eq } from "drizzle-orm"; diff --git a/server/routers/external.ts b/server/routers/external.ts index c69fdacc5..67494c643 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -3,6 +3,7 @@ import config from "@server/lib/config"; import * as site from "./site"; import * as org from "./org"; import * as resource from "./resource"; +import * as policy from "./policy"; import * as domain from "./domain"; import * as target from "./target"; import * as user from "./user"; @@ -521,6 +522,7 @@ authenticated.get( verifyUserHasAction(ActionsEnum.getResource), resource.getResource ); + authenticated.post( "/resource/:resourceId", verifyResourceAccess, @@ -627,6 +629,15 @@ authenticated.post( logActionAudit(ActionsEnum.updateRole), role.updateRole ); + +authenticated.get( + "/org/:orgId/resource-policy/:niceId", + verifyOrgAccess, + verifyResourcePolicyAccess, + verifyUserHasAction(ActionsEnum.getResourcePolicy), + policy.getResourcePolicy +); + // authenticated.get( // "/role/:roleId", // verifyRoleAccess, diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 6c39fe983..e52e710ee 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -2,6 +2,7 @@ import * as site from "./site"; import * as org from "./org"; import * as blueprints from "./blueprints"; import * as resource from "./resource"; +import * as policy from "./policy"; import * as domain from "./domain"; import * as target from "./target"; import * as user from "./user"; @@ -27,7 +28,8 @@ import { verifyApiKeyClientAccess, verifyApiKeySiteResourceAccess, verifyApiKeySetResourceClients, - verifyLimits + verifyLimits, + verifyApiKeyResourcePolicyAccess } from "@server/middlewares"; import HttpCode from "@server/types/HttpCode"; import { Router } from "express"; @@ -392,6 +394,13 @@ authenticated.get( resource.getResource ); +authenticated.get( + "/resource-policy/:resourcePolicyId", + verifyApiKeyResourcePolicyAccess, + verifyApiKeyHasAction(ActionsEnum.getResourcePolicy), + policy.getResourcePolicy +); + authenticated.post( "/resource/:resourceId", verifyApiKeyResourceAccess, diff --git a/server/routers/policy/getResourcePolicy.ts b/server/routers/policy/getResourcePolicy.ts new file mode 100644 index 000000000..0d33a222e --- /dev/null +++ b/server/routers/policy/getResourcePolicy.ts @@ -0,0 +1,123 @@ +import { db, resourcePolicies, rolePolicies, userPolicies } from "@server/db"; +import response from "@server/lib/response"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; +import HttpCode from "@server/types/HttpCode"; +import { and, eq, type SQL } from "drizzle-orm"; +import type { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import z from "zod"; +import { fromError } from "zod-validation-error"; + +const getResourcePolicySchema = z + .strictObject({ + niceId: z.string(), + orgId: z.string() + }) + .or( + z.strictObject({ + resourcePolicyId: z.coerce.number().int().positive() + }) + ); + +async function query(params: z.infer) { + const conditions: SQL[] = []; + if ("resourcePolicyId" in params) { + conditions.push( + eq(resourcePolicies.resourcePolicyId, params.resourcePolicyId) + ); + } else { + conditions.push( + eq(resourcePolicies.niceId, params.niceId), + eq(resourcePolicies.orgId, params.orgId) + ); + } + + const [res] = await db + .select({ + policy: resourcePolicies, + userPolicies, + rolePolicies + }) + .from(resourcePolicies) + .leftJoin( + userPolicies, + eq(userPolicies.resourcePolicyId, resourcePolicies.resourcePolicyId) + ) + .leftJoin( + rolePolicies, + eq(rolePolicies.resourcePolicyId, resourcePolicies.resourcePolicyId) + ) + .where(and(...conditions)) + .limit(1); + return res; +} + +export type GetResourcePolicyResponse = Awaited>; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/resource-policy/{niceId}", + description: + "Get a resource policy by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.", + tags: [OpenAPITags.Org, OpenAPITags.Policy], + request: { + params: z.object({ + orgId: z.string(), + niceId: z.string() + }) + }, + responses: {} +}); + +registry.registerPath({ + method: "get", + path: "/resource-policy/{resourcePolicyId}", + description: "Get a resource policy by its resourcePolicyId.", + tags: [OpenAPITags.Policy], + request: { + params: z.object({ + resourcePolicyId: z.number() + }) + }, + responses: {} +}); + +export async function getResourcePolicy( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = getResourcePolicySchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const policy = await query(parsedParams.data); + + if (!policy) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Resource policy not found") + ); + } + + return response(res, { + data: policy, + success: true, + error: false, + message: "Resource Policy retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/policy/index.ts b/server/routers/policy/index.ts new file mode 100644 index 000000000..a05c292ee --- /dev/null +++ b/server/routers/policy/index.ts @@ -0,0 +1 @@ +export * from "./getResourcePolicy"; diff --git a/src/app/[orgId]/settings/(private)/policies/layout.tsx b/src/app/[orgId]/settings/(private)/policies/layout.tsx new file mode 100644 index 000000000..ef5803e1a --- /dev/null +++ b/src/app/[orgId]/settings/(private)/policies/layout.tsx @@ -0,0 +1,23 @@ +import { getCachedOrg } from "@app/lib/api/getCachedOrg"; +import OrgProvider from "@app/providers/OrgProvider"; +import type { GetOrgResponse } from "@server/routers/org"; +import { redirect } from "next/navigation"; + +export interface PolicyLayoutPageProps { + params: Promise<{ orgId: string }>; + children: React.ReactNode; +} + +export default async function PolicyLayoutPage(props: PolicyLayoutPageProps) { + const params = await props.params; + + let org: GetOrgResponse | null = null; + try { + const res = await getCachedOrg(params.orgId); + org = res.data.data; + } catch { + redirect(`/${params.orgId}/settings`); + } + + return {props.children}; +} diff --git a/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx b/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx new file mode 100644 index 000000000..61833a1f1 --- /dev/null +++ b/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx @@ -0,0 +1,58 @@ +import { EditPolicyForm } from "@app/components/resource-policy/EditPolicyForm"; +import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import { Button } from "@app/components/ui/button"; +import { internal } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import type { ResourcePolicy } from "@server/db"; +import type { GetResourcePolicyResponse } from "@server/routers/policy"; +import type { AxiosResponse } from "axios"; +import { getTranslations } from "next-intl/server"; +import Link from "next/link"; +import { redirect } from "next/navigation"; + +export interface EditPolicyPageProps { + params: Promise<{ niceId: string; orgId: string }>; +} + +export default async function EditPolicyPage(props: EditPolicyPageProps) { + const params = await props.params; + const t = await getTranslations(); + + let policy: ResourcePolicy | null = null; + try { + const res = await internal.get< + AxiosResponse + >( + `/org/${params.orgId}/resource-policy/${params.niceId}`, + await authCookieHeader() + ); + policy = res.data.data.policy; + } catch { + redirect(`/${params.orgId}/settings/policies/resource`); + } + + if (!policy) { + redirect(`/${params.orgId}/settings/policies/resource`); + } + + return ( + <> +
+ + + +
+ + + + ); +} diff --git a/src/app/[orgId]/settings/(private)/policies/resources/create/page.tsx b/src/app/[orgId]/settings/(private)/policies/resource/create/page.tsx similarity index 62% rename from src/app/[orgId]/settings/(private)/policies/resources/create/page.tsx rename to src/app/[orgId]/settings/(private)/policies/resource/create/page.tsx index 6efdc5597..edf67fbef 100644 --- a/src/app/[orgId]/settings/(private)/policies/resources/create/page.tsx +++ b/src/app/[orgId]/settings/(private)/policies/resource/create/page.tsx @@ -1,12 +1,8 @@ import { CreatePolicyForm } from "@app/components/resource-policy/CreatePolicyForm"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { Button } from "@app/components/ui/button"; -import { getCachedOrg } from "@app/lib/api/getCachedOrg"; -import OrgProvider from "@app/providers/OrgProvider"; -import type { GetOrgResponse } from "@server/routers/org"; import { getTranslations } from "next-intl/server"; import Link from "next/link"; -import { redirect } from "next/navigation"; export interface CreateResourcePolicyPageProps { params: Promise<{ orgId: string }>; @@ -18,13 +14,6 @@ export default async function CreateResourcePolicyPage( const params = await props.params; const t = await getTranslations(); - let org: GetOrgResponse | null = null; - try { - const res = await getCachedOrg(params.orgId); - org = res.data.data; - } catch { - redirect(`/${params.orgId}/settings/resources`); - } return ( <>
@@ -34,15 +23,13 @@ export default async function CreateResourcePolicyPage( />
- - - + ); } diff --git a/src/app/[orgId]/settings/(private)/policies/resources/page.tsx b/src/app/[orgId]/settings/(private)/policies/resource/page.tsx similarity index 83% rename from src/app/[orgId]/settings/(private)/policies/resources/page.tsx rename to src/app/[orgId]/settings/(private)/policies/resource/page.tsx index e641696ef..3f2ec53b0 100644 --- a/src/app/[orgId]/settings/(private)/policies/resources/page.tsx +++ b/src/app/[orgId]/settings/(private)/policies/resource/page.tsx @@ -55,17 +55,15 @@ export default async function ResourcePoliciesPage( description={t("resourcePoliciesDescription")} /> - - - + ); } diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index bf68837f5..324f051c3 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -132,7 +132,7 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [ items: [ { title: "sidebarResourcePolicies", - href: "/{orgId}/settings/policies/resources", + href: "/{orgId}/settings/policies/resource", icon: ( ) diff --git a/src/components/ResourcePoliciesTable.tsx b/src/components/ResourcePoliciesTable.tsx index 32860a464..69dee6963 100644 --- a/src/components/ResourcePoliciesTable.tsx +++ b/src/components/ResourcePoliciesTable.tsx @@ -103,7 +103,7 @@ export function ResourcePoliciesTable({ {t("viewSettings")} @@ -122,7 +122,7 @@ export function ResourcePoliciesTable({ +
+ + ); + } + + return ( + <> + {/* Password Credenza */} + { + setIsSetPasswordOpen(val); + if (!val) passwordForm.reset(); + }} + > + + + + {t("resourcePasswordSetupTitle")} + + + {t("resourcePasswordSetupTitleDescription")} + + + +
+ { + form.setValue("password", data); + setIsSetPasswordOpen(false); + passwordForm.reset(); + })} + className="space-y-4" + id="set-password-form" + > + ( + + + {t("password")} + + + + + + + )} + /> + + +
+ + + + + + +
+
+ + {/* Pincode Credenza */} + { + setIsSetPincodeOpen(val); + if (!val) pincodeForm.reset(); + }} + > + + + + {t("resourcePincodeSetupTitle")} + + + {t("resourcePincodeSetupTitleDescription")} + + + +
+ { + form.setValue("pincode", data); + setIsSetPincodeOpen(false); + pincodeForm.reset(); + })} + className="space-y-4" + id="set-pincode-form" + > + ( + + + {t("resourcePincode")} + + +
+ + + + + + + + + + +
+
+ +
+ )} + /> + + +
+ + + + + + +
+
+ + {/* Header Auth Credenza */} + { + setIsSetHeaderAuthOpen(val); + if (!val) headerAuthForm.reset(); + }} + > + + + + {t("resourceHeaderAuthSetupTitle")} + + + {t("resourceHeaderAuthSetupTitleDescription")} + + + +
+ { + form.setValue("headerAuth", data); + setIsSetHeaderAuthOpen(false); + headerAuthForm.reset(); + } + )} + className="space-y-4" + id="set-header-auth-form" + > + ( + + {t("user")} + + + + + + )} + /> + ( + + + {t("password")} + + + + + + + )} + /> + ( + + + + + + + )} + /> + + +
+ + + + + + +
+
+ + + + + {t("resourceAuthMethods")} + + + {t("resourcePolicyAuthMethodsDescription")} + + + + + {/* Password row */} +
+
+ + + {t("resourcePasswordProtection", { + status: password + ? t("enabled") + : t("disabled") + })} + +
+ +
+ + {/* Pincode row */} +
+
+ + + {t("resourcePincodeProtection", { + status: pincode + ? t("enabled") + : t("disabled") + })} + +
+ +
+ + {/* Header auth row */} +
+
+ + + {headerAuth + ? t( + "resourceHeaderAuthProtectionEnabled" + ) + : t( + "resourceHeaderAuthProtectionDisabled" + )} + +
+ +
+
+
+
+ + ); +} + +// ─── PolicyOtpEmailSection ──────────────────────────────────────────────────── + +type PolicyOtpEmailSectionProps = { + form: UseFormReturn; + emailEnabled: boolean; +}; + +export function PolicyOtpEmailSection({ + form, + emailEnabled +}: PolicyOtpEmailSectionProps) { + const t = useTranslations(); + const [isOpen, setIsOpen] = useState(false); + const [whitelistEnabled, setWhitelistEnabled] = useState(false); + const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< + number | null + >(null); + + if (!isOpen) { + return ( + + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + + + ); + } + + return ( + + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + {!emailEnabled && ( + + + + {t("otpEmailSmtpRequired")} + + + {t("otpEmailSmtpRequiredDescription")} + + + )} + { + setWhitelistEnabled(val); + form.setValue("emailWhitelistEnabled", val); + }} + disabled={!emailEnabled} + /> + + {whitelistEnabled && emailEnabled && ( + ( + + + + + + {/* @ts-ignore */} + { + return z + .email() + .or( + z + .string() + .regex( + /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, + { + message: t( + "otpEmailErrorInvalid" + ) + } + ) + ) + .safeParse(tag).success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder={t("otpEmailEnter")} + tags={form.getValues().emails} + setTags={(newEmails) => { + form.setValue( + "emails", + newEmails as [Tag, ...Tag[]] + ); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t("otpEmailEnterDescription")} + + + )} + /> + )} + + + + ); +} + +// ─── PolicyRulesSection ─────────────────────────────────────────────────────── + +const addRuleSchema = z.object({ + action: z.enum(["ACCEPT", "DROP", "PASS"]), + match: z.string(), + value: z.string(), + priority: z.coerce.number().int().optional() +}); + +type LocalRule = { + ruleId: number; + action: "ACCEPT" | "DROP" | "PASS"; + match: string; + value: string; + priority: number; + enabled: boolean; + new?: boolean; + updated?: boolean; +}; + +type PolicyRulesSectionProps = { + form: UseFormReturn; + isMaxmindAvailable: boolean; + isMaxmindAsnAvailable: boolean; +}; + +export function PolicyRulesSection({ + form, + isMaxmindAvailable, + isMaxmindAsnAvailable +}: PolicyRulesSectionProps) { + const t = useTranslations(); + const [isOpen, setIsOpen] = useState(false); + const [rules, setRules] = useState([]); + const [rulesEnabled, setRulesEnabled] = useState(false); + const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = + useState(false); + const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false); + + const addRuleForm = useForm({ + resolver: zodResolver(addRuleSchema), + defaultValues: { + action: "ACCEPT" as const, + match: "IP", + value: "" + } + }); + + const RuleAction = useMemo( + () => ({ + ACCEPT: t("alwaysAllow"), + DROP: t("alwaysDeny"), + PASS: t("passToAuth") + }), + [t] + ); + + const RuleMatch = useMemo( + () => ({ + PATH: t("path"), + IP: "IP", + CIDR: t("ipAddressRange"), + COUNTRY: t("country"), + ASN: "ASN" + }), + [t] + ); + + const syncFormRules = useCallback( + (updatedRules: LocalRule[]) => { + form.setValue( + "rules", + updatedRules.map( + ({ action, match, value, priority, enabled }) => ({ + action, + match, + value, + priority, + enabled + }) + ) + ); + }, + [form] + ); + + const addRule = useCallback( + function addRule(data: z.infer) { + const isDuplicate = rules.some( + (rule) => + rule.action === data.action && + rule.match === data.match && + rule.value === data.value + ); + if (isDuplicate) { + toast({ + variant: "destructive", + title: t("rulesErrorDuplicate"), + description: t("rulesErrorDuplicateDescription") + }); + return; + } + if (data.match === "CIDR" && !isValidCIDR(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidIpAddressRange"), + description: t("rulesErrorInvalidIpAddressRangeDescription") + }); + return; + } + if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidUrl"), + description: t("rulesErrorInvalidUrlDescription") + }); + return; + } + if (data.match === "IP" && !isValidIP(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidIpAddress"), + description: t("rulesErrorInvalidIpAddressDescription") + }); + return; + } + if ( + data.match === "COUNTRY" && + !COUNTRIES.some((c) => c.code === data.value) + ) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidCountry"), + description: t("rulesErrorInvalidCountryDescription") || "" + }); + return; + } + + let priority = data.priority; + if (priority === undefined) { + priority = + rules.reduce( + (acc, rule) => + rule.priority > acc ? rule.priority : acc, + 0 + ) + 1; + } + + const updatedRules = [ + ...rules, + { + ...data, + ruleId: new Date().getTime(), + new: true, + priority, + enabled: true + } + ]; + setRules(updatedRules); + syncFormRules(updatedRules); + addRuleForm.reset(); + }, + [rules, t, addRuleForm, syncFormRules] + ); + + const removeRule = useCallback( + function removeRule(ruleId: number) { + const updatedRules = rules.filter((rule) => rule.ruleId !== ruleId); + setRules(updatedRules); + syncFormRules(updatedRules); + }, + [rules, syncFormRules] + ); + + const updateRule = useCallback( + function updateRule(ruleId: number, data: Partial) { + const updatedRules = rules.map((rule) => + rule.ruleId === ruleId + ? { ...rule, ...data, updated: true } + : rule + ); + setRules(updatedRules); + syncFormRules(updatedRules); + }, + [rules, syncFormRules] + ); + + const getValueHelpText = useCallback( + function getValueHelpText(type: string) { + switch (type) { + case "CIDR": + return t("rulesMatchIpAddressRangeDescription"); + case "IP": + return t("rulesMatchIpAddress"); + case "PATH": + return t("rulesMatchUrl"); + case "COUNTRY": + return t("rulesMatchCountry"); + case "ASN": + return "Enter an Autonomous System Number (e.g., AS15169 or 15169)"; + } + }, + [t] + ); + + const columns: ColumnDef[] = useMemo( + () => [ + { + accessorKey: "priority", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + e.currentTarget.focus()} + onBlur={(e) => { + const parsed = z.coerce + .number() + .int() + .optional() + .safeParse(e.target.value); + if (!parsed.success) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidPriority"), + description: t( + "rulesErrorInvalidPriorityDescription" + ) + }); + return; + } + updateRule(row.original.ruleId, { + priority: parsed.data + }); + }} + /> + ) + }, + { + accessorKey: "action", + header: () => {t("rulesAction")}, + cell: ({ row }) => ( + + ) + }, + { + accessorKey: "match", + header: () => ( + {t("rulesMatchType")} + ), + cell: ({ row }) => ( + + ) + }, + { + accessorKey: "value", + header: () => {t("value")}, + cell: ({ row }) => + row.original.match === "COUNTRY" ? ( + + + + + + + + + + {t("noCountryFound")} + + + {COUNTRIES.map((country) => ( + + updateRule( + row.original.ruleId, + { + value: country.code + } + ) + } + > + + {country.name} ( + {country.code}) + + ))} + + + + + + ) : row.original.match === "ASN" ? ( + + + + + + + + + + No ASN found. Enter a custom ASN + below. + + + {MAJOR_ASNS.map((asn) => ( + + updateRule( + row.original.ruleId, + { value: asn.code } + ) + } + > + + {asn.name} ({asn.code}) + + ))} + + + +
+ + asn.code === + row.original.value + ) + ? row.original.value + : "" + } + onKeyDown={(e) => { + if (e.key === "Enter") { + const value = + e.currentTarget.value + .toUpperCase() + .replace(/^AS/, ""); + if (/^\d+$/.test(value)) { + updateRule( + row.original.ruleId, + { value: "AS" + value } + ); + } + } + }} + className="text-sm" + /> +
+
+
+ ) : ( + + updateRule(row.original.ruleId, { + value: e.target.value + }) + } + /> + ) + }, + { + accessorKey: "enabled", + header: () => {t("enabled")}, + cell: ({ row }) => ( + + updateRule(row.original.ruleId, { enabled: val }) + } + /> + ) + }, + { + id: "actions", + header: () => {t("actions")}, + cell: ({ row }) => ( +
+ +
+ ) + } + ], + [ + t, + RuleAction, + RuleMatch, + isMaxmindAvailable, + isMaxmindAsnAvailable, + updateRule, + removeRule + ] + ); + + const table = useReactTable({ + data: rules, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { pagination: { pageIndex: 0, pageSize: 1000 } } + }); + + if (!isOpen) { + return ( + + + + {t("rulesResource")} + + + {t("rulesResourcePolicyDescription")} + + + + + + + ); + } + + return ( + + + + {t("rulesResource")} + + + {t("rulesResourceDescription")} + + + +
+
+ { + setRulesEnabled(val); + form.setValue("applyRules", val); + }} + /> +
+ +
+ +
+ ( + + + {t("rulesAction")} + + + + + + + )} + /> + ( + + + {t("rulesMatchType")} + + + + + + + )} + /> + ( + + + + {addRuleForm.watch("match") === + "COUNTRY" ? ( + + + + + + + + + + {t( + "noCountryFound" + )} + + + {COUNTRIES.map( + ( + country + ) => ( + { + field.onChange( + country.code + ); + setOpenAddRuleCountrySelect( + false + ); + }} + > + + { + country.name + }{" "} + ( + { + country.code + } + + ) + + ) + )} + + + + + + ) : addRuleForm.watch( + "match" + ) === "ASN" ? ( + + + + + + + + + + No ASN + found. + Use the + custom + input + below. + + + {MAJOR_ASNS.map( + ( + asn + ) => ( + { + field.onChange( + asn.code + ); + setOpenAddRuleAsnSelect( + false + ); + }} + > + + { + asn.name + }{" "} + ( + { + asn.code + } + + ) + + ) + )} + + + +
+ { + if ( + e.key === + "Enter" + ) { + const value = + e.currentTarget.value + .toUpperCase() + .replace( + /^AS/, + "" + ); + if ( + /^\d+$/.test( + value + ) + ) { + field.onChange( + "AS" + + value + ); + setOpenAddRuleAsnSelect( + false + ); + } + } + }} + className="text-sm" + /> +
+
+
+ ) : ( + + )} +
+ +
+ )} + /> + +
+
+ + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const isActionsColumn = + header.column.id === "actions"; + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column + .columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const isActionsColumn = + cell.column.id === "actions"; + return ( + + {flexRender( + cell.column.columnDef + .cell, + cell.getContext() + )} + + ); + })} + + )) + ) : ( + + + {t("rulesNoOne")} + + + )} + +
+
+
+
+ ); +} diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index 3f09f7ef8..4331fb286 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -9,16 +9,7 @@ import { SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; -import { Button } from "@app/components/ui/button"; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { Input } from "@app/components/ui/input"; + import { useEnvContext } from "@app/hooks/useEnvContext"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; @@ -32,15 +23,8 @@ import { UserType } from "@server/types/UserTypes"; import { useQuery } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; -import { useActionState, useMemo } from "react"; -import { useForm } from "react-hook-form"; import z from "zod"; -import { - PolicyAuthMethodsSection, - PolicyOtpEmailSection, - PolicyRulesSection, - PolicyUsersRolesSection -} from "./ResourcePolicySubForms"; + import { type PolicyFormValues, createPolicySchema } from "."; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; @@ -48,13 +32,107 @@ import { orgs, type ResourcePolicy } from "@server/db"; import type { AxiosResponse } from "axios"; import { useRouter } from "next/navigation"; -// ─── CreatePolicyForm ───────────────────────────────────────────────────────── +import { SwitchInput } from "@app/components/SwitchInput"; +import { Tag, TagInput } from "@app/components/tags/tag-input"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { Button } from "@app/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { InfoPopup } from "@app/components/ui/info-popup"; +import { Input } from "@app/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { Switch } from "@app/components/ui/switch"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@app/components/ui/table"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot +} from "@app/components/ui/input-otp"; + +import { MAJOR_ASNS } from "@server/db/asns"; +import { COUNTRIES } from "@server/db/countries"; +import { + isValidCIDR, + isValidIP, + isValidUrlGlobPattern +} from "@server/lib/validators"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable +} from "@tanstack/react-table"; +import { + ArrowUpDown, + Binary, + Bot, + Check, + ChevronsUpDown, + InfoIcon, + Key, + Plus +} from "lucide-react"; + +import { useCallback, useMemo, useState, useActionState } from "react"; +import { UseFormReturn, useForm, useWatch } from "react-hook-form"; + +// ─── EditPolicyForm ───────────────────────────────────────────────────────── export type EditPolicyFormProps = { policy: ResourcePolicy; + hidePolicyNameForm?: boolean; }; -export function EditPolicyForm({}: EditPolicyFormProps) { +export function EditPolicyForm({ + hidePolicyNameForm, + policy +}: EditPolicyFormProps) { const { org } = useOrgContext(); const t = useTranslations(); const { env } = useEnvContext(); @@ -87,7 +165,7 @@ export function EditPolicyForm({}: EditPolicyFormProps) { const form = useForm({ resolver: zodResolver(createPolicySchema) as any, defaultValues: { - name: "", + name: policy.name, sso: true, skipToIdpId: null, emailWhitelistEnabled: false, @@ -197,39 +275,7 @@ export function EditPolicyForm({}: EditPolicyFormProps) {
{/* Name */} - - - - {t("resourcePolicyName")} - - - {t("resourcePolicyNameDescription")} - - - - - ( - - {t("name")} - - - - - - )} - /> - - - - + {!hidePolicyNameForm && } - -
- -
); } + +// ─── PolicyNameSection ────────────────────────────────────────────────── +type PolicyNameSectionProps = { + form: UseFormReturn; + isEditing?: boolean; +}; + +export function PolicyNameSection({ form }: PolicyNameSectionProps) { + const t = useTranslations(); + return ( + + + + {t("resourcePolicyName")} + + + {t("resourcePolicyNameDescription")} + + + + + ( + + {t("name")} + + + + + + )} + /> + + + +
+ +
+
+ ); +} + +// ─── PolicyUsersRolesSection ────────────────────────────────────────────────── + +type PolicyUsersRolesSectionProps = { + form: UseFormReturn; + allRoles: { id: string; text: string }[]; + allUsers: { id: string; text: string }[]; + allIdps: { id: number; text: string }[]; +}; + +export function PolicyUsersRolesSection({ + form, + allRoles, + allUsers, + allIdps +}: PolicyUsersRolesSectionProps) { + const t = useTranslations(); + const ssoEnabled = useWatch({ control: form.control, name: "sso" }); + const selectedIdpId = useWatch({ + control: form.control, + name: "skipToIdpId" + }); + const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< + number | null + >(null); + const [activeUsersTagIndex, setActiveUsersTagIndex] = useState< + number | null + >(null); + + return ( + + + + {t("resourceUsersRoles")} + + + {t("resourcePolicyUsersRolesDescription")} + + + + + { + console.log(`form.setValue("sso", ${val})`); + form.setValue("sso", val); + }} + /> + + {ssoEnabled && ( + <> + ( + + {t("roles")} + + { + form.setValue( + "roles", + newRoles as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={true} + autocompleteOptions={allRoles} + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + {t("resourceRoleDescription")} + + + )} + /> + ( + + {t("users")} + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={true} + autocompleteOptions={allUsers} + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + )} + /> + + )} + + {ssoEnabled && allIdps.length > 0 && ( +
+ + +

+ {t("defaultIdentityProviderDescription")} +

+
+ )} +
+
+
+ ); +} + +// ─── PolicyAuthMethodsSection ───────────────────────────────────────────────── + +const setPasswordSchema = z.object({ + password: z.string().min(4).max(100) +}); + +const setPincodeSchema = z.object({ + pincode: z.string().length(6) +}); + +const setHeaderAuthSchema = z.object({ + user: z.string().min(4).max(100), + password: z.string().min(4).max(100), + extendedCompatibility: z.boolean() +}); + +type PolicyAuthMethodsSectionProps = { + form: UseFormReturn; +}; + +export function PolicyAuthMethodsSection({ + form +}: PolicyAuthMethodsSectionProps) { + const t = useTranslations(); + const [isOpen, setIsOpen] = useState(false); + const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); + const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); + const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); + + const password = form.watch("password"); + const pincode = form.watch("pincode"); + const headerAuth = form.watch("headerAuth"); + + const passwordForm = useForm({ + resolver: zodResolver(setPasswordSchema), + defaultValues: { password: "" } + }); + + const pincodeForm = useForm({ + resolver: zodResolver(setPincodeSchema), + defaultValues: { pincode: "" } + }); + + const headerAuthForm = useForm({ + resolver: zodResolver(setHeaderAuthSchema), + defaultValues: { user: "", password: "", extendedCompatibility: true } + }); + + if (!isOpen) { + return ( + + + + {t("resourceAuthMethods")} + + + {t("resourcePolicyAuthMethodsDescription")} + + + + + + + ); + } + + return ( + <> + {/* Password Credenza */} + { + setIsSetPasswordOpen(val); + if (!val) passwordForm.reset(); + }} + > + + + + {t("resourcePasswordSetupTitle")} + + + {t("resourcePasswordSetupTitleDescription")} + + + +
+ { + form.setValue("password", data); + setIsSetPasswordOpen(false); + passwordForm.reset(); + })} + className="space-y-4" + id="set-password-form" + > + ( + + + {t("password")} + + + + + + + )} + /> + + +
+ + + + + + +
+
+ + {/* Pincode Credenza */} + { + setIsSetPincodeOpen(val); + if (!val) pincodeForm.reset(); + }} + > + + + + {t("resourcePincodeSetupTitle")} + + + {t("resourcePincodeSetupTitleDescription")} + + + +
+ { + form.setValue("pincode", data); + setIsSetPincodeOpen(false); + pincodeForm.reset(); + })} + className="space-y-4" + id="set-pincode-form" + > + ( + + + {t("resourcePincode")} + + +
+ + + + + + + + + + +
+
+ +
+ )} + /> + + +
+ + + + + + +
+
+ + {/* Header Auth Credenza */} + { + setIsSetHeaderAuthOpen(val); + if (!val) headerAuthForm.reset(); + }} + > + + + + {t("resourceHeaderAuthSetupTitle")} + + + {t("resourceHeaderAuthSetupTitleDescription")} + + + +
+ { + form.setValue("headerAuth", data); + setIsSetHeaderAuthOpen(false); + headerAuthForm.reset(); + } + )} + className="space-y-4" + id="set-header-auth-form" + > + ( + + {t("user")} + + + + + + )} + /> + ( + + + {t("password")} + + + + + + + )} + /> + ( + + + + + + + )} + /> + + +
+ + + + + + +
+
+ + + + + {t("resourceAuthMethods")} + + + {t("resourcePolicyAuthMethodsDescription")} + + + + + {/* Password row */} +
+
+ + + {t("resourcePasswordProtection", { + status: password + ? t("enabled") + : t("disabled") + })} + +
+ +
+ + {/* Pincode row */} +
+
+ + + {t("resourcePincodeProtection", { + status: pincode + ? t("enabled") + : t("disabled") + })} + +
+ +
+ + {/* Header auth row */} +
+
+ + + {headerAuth + ? t( + "resourceHeaderAuthProtectionEnabled" + ) + : t( + "resourceHeaderAuthProtectionDisabled" + )} + +
+ +
+
+
+
+ + ); +} + +// ─── PolicyOtpEmailSection ──────────────────────────────────────────────────── + +type PolicyOtpEmailSectionProps = { + form: UseFormReturn; + emailEnabled: boolean; +}; + +export function PolicyOtpEmailSection({ + form, + emailEnabled +}: PolicyOtpEmailSectionProps) { + const t = useTranslations(); + const [isOpen, setIsOpen] = useState(false); + const [whitelistEnabled, setWhitelistEnabled] = useState(false); + const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< + number | null + >(null); + + if (!isOpen) { + return ( + + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + + + ); + } + + return ( + + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + {!emailEnabled && ( + + + + {t("otpEmailSmtpRequired")} + + + {t("otpEmailSmtpRequiredDescription")} + + + )} + { + setWhitelistEnabled(val); + form.setValue("emailWhitelistEnabled", val); + }} + disabled={!emailEnabled} + /> + + {whitelistEnabled && emailEnabled && ( + ( + + + + + + {/* @ts-ignore */} + { + return z + .email() + .or( + z + .string() + .regex( + /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, + { + message: t( + "otpEmailErrorInvalid" + ) + } + ) + ) + .safeParse(tag).success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder={t("otpEmailEnter")} + tags={form.getValues().emails} + setTags={(newEmails) => { + form.setValue( + "emails", + newEmails as [Tag, ...Tag[]] + ); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t("otpEmailEnterDescription")} + + + )} + /> + )} + + + + ); +} + +// ─── PolicyRulesSection ─────────────────────────────────────────────────────── + +const addRuleSchema = z.object({ + action: z.enum(["ACCEPT", "DROP", "PASS"]), + match: z.string(), + value: z.string(), + priority: z.coerce.number().int().optional() +}); + +type LocalRule = { + ruleId: number; + action: "ACCEPT" | "DROP" | "PASS"; + match: string; + value: string; + priority: number; + enabled: boolean; + new?: boolean; + updated?: boolean; +}; + +type PolicyRulesSectionProps = { + form: UseFormReturn; + isMaxmindAvailable: boolean; + isMaxmindAsnAvailable: boolean; +}; + +export function PolicyRulesSection({ + form, + isMaxmindAvailable, + isMaxmindAsnAvailable +}: PolicyRulesSectionProps) { + const t = useTranslations(); + const [isOpen, setIsOpen] = useState(false); + const [rules, setRules] = useState([]); + const [rulesEnabled, setRulesEnabled] = useState(false); + const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = + useState(false); + const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false); + + const addRuleForm = useForm({ + resolver: zodResolver(addRuleSchema), + defaultValues: { + action: "ACCEPT" as const, + match: "IP", + value: "" + } + }); + + const RuleAction = useMemo( + () => ({ + ACCEPT: t("alwaysAllow"), + DROP: t("alwaysDeny"), + PASS: t("passToAuth") + }), + [t] + ); + + const RuleMatch = useMemo( + () => ({ + PATH: t("path"), + IP: "IP", + CIDR: t("ipAddressRange"), + COUNTRY: t("country"), + ASN: "ASN" + }), + [t] + ); + + const syncFormRules = useCallback( + (updatedRules: LocalRule[]) => { + form.setValue( + "rules", + updatedRules.map( + ({ action, match, value, priority, enabled }) => ({ + action, + match, + value, + priority, + enabled + }) + ) + ); + }, + [form] + ); + + const addRule = useCallback( + function addRule(data: z.infer) { + const isDuplicate = rules.some( + (rule) => + rule.action === data.action && + rule.match === data.match && + rule.value === data.value + ); + if (isDuplicate) { + toast({ + variant: "destructive", + title: t("rulesErrorDuplicate"), + description: t("rulesErrorDuplicateDescription") + }); + return; + } + if (data.match === "CIDR" && !isValidCIDR(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidIpAddressRange"), + description: t("rulesErrorInvalidIpAddressRangeDescription") + }); + return; + } + if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidUrl"), + description: t("rulesErrorInvalidUrlDescription") + }); + return; + } + if (data.match === "IP" && !isValidIP(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidIpAddress"), + description: t("rulesErrorInvalidIpAddressDescription") + }); + return; + } + if ( + data.match === "COUNTRY" && + !COUNTRIES.some((c) => c.code === data.value) + ) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidCountry"), + description: t("rulesErrorInvalidCountryDescription") || "" + }); + return; + } + + let priority = data.priority; + if (priority === undefined) { + priority = + rules.reduce( + (acc, rule) => + rule.priority > acc ? rule.priority : acc, + 0 + ) + 1; + } + + const updatedRules = [ + ...rules, + { + ...data, + ruleId: new Date().getTime(), + new: true, + priority, + enabled: true + } + ]; + setRules(updatedRules); + syncFormRules(updatedRules); + addRuleForm.reset(); + }, + [rules, t, addRuleForm, syncFormRules] + ); + + const removeRule = useCallback( + function removeRule(ruleId: number) { + const updatedRules = rules.filter((rule) => rule.ruleId !== ruleId); + setRules(updatedRules); + syncFormRules(updatedRules); + }, + [rules, syncFormRules] + ); + + const updateRule = useCallback( + function updateRule(ruleId: number, data: Partial) { + const updatedRules = rules.map((rule) => + rule.ruleId === ruleId + ? { ...rule, ...data, updated: true } + : rule + ); + setRules(updatedRules); + syncFormRules(updatedRules); + }, + [rules, syncFormRules] + ); + + const getValueHelpText = useCallback( + function getValueHelpText(type: string) { + switch (type) { + case "CIDR": + return t("rulesMatchIpAddressRangeDescription"); + case "IP": + return t("rulesMatchIpAddress"); + case "PATH": + return t("rulesMatchUrl"); + case "COUNTRY": + return t("rulesMatchCountry"); + case "ASN": + return "Enter an Autonomous System Number (e.g., AS15169 or 15169)"; + } + }, + [t] + ); + + const columns: ColumnDef[] = useMemo( + () => [ + { + accessorKey: "priority", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + e.currentTarget.focus()} + onBlur={(e) => { + const parsed = z.coerce + .number() + .int() + .optional() + .safeParse(e.target.value); + if (!parsed.success) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidPriority"), + description: t( + "rulesErrorInvalidPriorityDescription" + ) + }); + return; + } + updateRule(row.original.ruleId, { + priority: parsed.data + }); + }} + /> + ) + }, + { + accessorKey: "action", + header: () => {t("rulesAction")}, + cell: ({ row }) => ( + + ) + }, + { + accessorKey: "match", + header: () => ( + {t("rulesMatchType")} + ), + cell: ({ row }) => ( + + ) + }, + { + accessorKey: "value", + header: () => {t("value")}, + cell: ({ row }) => + row.original.match === "COUNTRY" ? ( + + + + + + + + + + {t("noCountryFound")} + + + {COUNTRIES.map((country) => ( + + updateRule( + row.original.ruleId, + { + value: country.code + } + ) + } + > + + {country.name} ( + {country.code}) + + ))} + + + + + + ) : row.original.match === "ASN" ? ( + + + + + + + + + + No ASN found. Enter a custom ASN + below. + + + {MAJOR_ASNS.map((asn) => ( + + updateRule( + row.original.ruleId, + { value: asn.code } + ) + } + > + + {asn.name} ({asn.code}) + + ))} + + + +
+ + asn.code === + row.original.value + ) + ? row.original.value + : "" + } + onKeyDown={(e) => { + if (e.key === "Enter") { + const value = + e.currentTarget.value + .toUpperCase() + .replace(/^AS/, ""); + if (/^\d+$/.test(value)) { + updateRule( + row.original.ruleId, + { value: "AS" + value } + ); + } + } + }} + className="text-sm" + /> +
+
+
+ ) : ( + + updateRule(row.original.ruleId, { + value: e.target.value + }) + } + /> + ) + }, + { + accessorKey: "enabled", + header: () => {t("enabled")}, + cell: ({ row }) => ( + + updateRule(row.original.ruleId, { enabled: val }) + } + /> + ) + }, + { + id: "actions", + header: () => {t("actions")}, + cell: ({ row }) => ( +
+ +
+ ) + } + ], + [ + t, + RuleAction, + RuleMatch, + isMaxmindAvailable, + isMaxmindAsnAvailable, + updateRule, + removeRule + ] + ); + + const table = useReactTable({ + data: rules, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { pagination: { pageIndex: 0, pageSize: 1000 } } + }); + + if (!isOpen) { + return ( + + + + {t("rulesResource")} + + + {t("rulesResourcePolicyDescription")} + + + + + + + ); + } + + return ( + + + + {t("rulesResource")} + + + {t("rulesResourceDescription")} + + + +
+
+ { + setRulesEnabled(val); + form.setValue("applyRules", val); + }} + /> +
+ +
+ +
+ ( + + + {t("rulesAction")} + + + + + + + )} + /> + ( + + + {t("rulesMatchType")} + + + + + + + )} + /> + ( + + + + {addRuleForm.watch("match") === + "COUNTRY" ? ( + + + + + + + + + + {t( + "noCountryFound" + )} + + + {COUNTRIES.map( + ( + country + ) => ( + { + field.onChange( + country.code + ); + setOpenAddRuleCountrySelect( + false + ); + }} + > + + { + country.name + }{" "} + ( + { + country.code + } + + ) + + ) + )} + + + + + + ) : addRuleForm.watch( + "match" + ) === "ASN" ? ( + + + + + + + + + + No ASN + found. + Use the + custom + input + below. + + + {MAJOR_ASNS.map( + ( + asn + ) => ( + { + field.onChange( + asn.code + ); + setOpenAddRuleAsnSelect( + false + ); + }} + > + + { + asn.name + }{" "} + ( + { + asn.code + } + + ) + + ) + )} + + + +
+ { + if ( + e.key === + "Enter" + ) { + const value = + e.currentTarget.value + .toUpperCase() + .replace( + /^AS/, + "" + ); + if ( + /^\d+$/.test( + value + ) + ) { + field.onChange( + "AS" + + value + ); + setOpenAddRuleAsnSelect( + false + ); + } + } + }} + className="text-sm" + /> +
+
+
+ ) : ( + + )} +
+ +
+ )} + /> + +
+
+ + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const isActionsColumn = + header.column.id === "actions"; + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column + .columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const isActionsColumn = + cell.column.id === "actions"; + return ( + + {flexRender( + cell.column.columnDef + .cell, + cell.getContext() + )} + + ); + })} + + )) + ) : ( + + + {t("rulesNoOne")} + + + )} + +
+
+
+
+ ); +} diff --git a/src/components/resource-policy/ResourcePolicySubForms.tsx b/src/components/resource-policy/ResourcePolicySubForms.tsx index 3b0056ba4..37a83b3fe 100644 --- a/src/components/resource-policy/ResourcePolicySubForms.tsx +++ b/src/components/resource-policy/ResourcePolicySubForms.tsx @@ -121,6 +121,62 @@ type LocalRule = { updated?: boolean; }; +// ─── PolicyNameSection ────────────────────────────────────────────────── +type PolicyNameSectionProps = { + form: UseFormReturn; + isEditing?: boolean; +}; + +export function PolicyNameSection({ form }: PolicyNameSectionProps) { + const t = useTranslations(); + return ( + + + + {t("resourcePolicyName")} + + + {t("resourcePolicyNameDescription")} + + + + + ( + + {t("name")} + + + + + + )} + /> + + + +
+ +
+
+ ); +} + +// ─── PolicyUsersRolesSection ────────────────────────────────────────────────── + type PolicyUsersRolesSectionProps = { form: UseFormReturn; allRoles: { id: string; text: string }[]; @@ -128,8 +184,6 @@ type PolicyUsersRolesSectionProps = { allIdps: { id: number; text: string }[]; }; -// ─── PolicyUsersRolesSection ────────────────────────────────────────────────── - export function PolicyUsersRolesSection({ form, allRoles, diff --git a/src/components/resource-policy/index.ts b/src/components/resource-policy/index.ts index 7b77faddb..8579a6de5 100644 --- a/src/components/resource-policy/index.ts +++ b/src/components/resource-policy/index.ts @@ -45,3 +45,21 @@ export const createPolicySchema = z.object({ }); export type PolicyFormValues = z.infer; + +export const addRuleSchema = z.object({ + action: z.enum(["ACCEPT", "DROP", "PASS"]), + match: z.string(), + value: z.string(), + priority: z.coerce.number().int().optional() +}); + +export type LocalRule = { + ruleId: number; + action: "ACCEPT" | "DROP" | "PASS"; + match: string; + value: string; + priority: number; + enabled: boolean; + new?: boolean; + updated?: boolean; +}; From d6a80216137ffab2546a21867aaef071cc82d7ad Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 27 Feb 2026 04:21:20 +0100 Subject: [PATCH 029/771] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20update=20resource?= =?UTF-8?q?=20policy=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 4 + server/routers/external.ts | 7 + server/routers/policy/index.ts | 1 + .../routers/policy/updateResourcePolicy.ts | 9 +- .../policies/resource/[niceId]/page.tsx | 14 +- .../resource-policy/CreatePolicyForm.tsx | 10 +- .../resource-policy/EditPolicyForm.tsx | 313 +++++++++++------- src/providers/ResourcePolicyProvider.tsx | 63 ++++ 8 files changed, 272 insertions(+), 149 deletions(-) rename server/{private => }/routers/policy/updateResourcePolicy.ts (97%) create mode 100644 src/providers/ResourcePolicyProvider.tsx diff --git a/messages/en-US.json b/messages/en-US.json index 78b4ad0ba..3a0c77462 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -642,7 +642,11 @@ "policyErrorCreate": "Error creating policy", "policyErrorCreateDescription": "An error occurred when creating the policy", "policyErrorCreateMessageDescription": "An unexpected error occurred", + "policyErrorUpdate": "Error updating policy", + "policyErrorUpdateDescription": "An error occurred when updating the policy", + "policyErrorUpdateMessageDescription": "An unexpected error occurred", "policyCreatedSuccess": "Resource policy succesfully created", + "policyUpdatedSuccess": "Resource policy succesfully updated", "resourceErrorCreate": "Error creating resource", "resourceErrorCreateDescription": "An error occurred when creating the resource", "resourceErrorCreateMessage": "Error creating resource:", diff --git a/server/routers/external.ts b/server/routers/external.ts index 67494c643..519d2b4a9 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -638,6 +638,13 @@ authenticated.get( policy.getResourcePolicy ); +authenticated.put( + "/resource-policy/:resourcePolicyId", + verifyResourcePolicyAccess, + verifyUserHasAction(ActionsEnum.updateResourcePolicy), + policy.updateResourcePolicy +); + // authenticated.get( // "/role/:roleId", // verifyRoleAccess, diff --git a/server/routers/policy/index.ts b/server/routers/policy/index.ts index a05c292ee..8cd264925 100644 --- a/server/routers/policy/index.ts +++ b/server/routers/policy/index.ts @@ -1 +1,2 @@ export * from "./getResourcePolicy"; +export * from "./updateResourcePolicy"; diff --git a/server/private/routers/policy/updateResourcePolicy.ts b/server/routers/policy/updateResourcePolicy.ts similarity index 97% rename from server/private/routers/policy/updateResourcePolicy.ts rename to server/routers/policy/updateResourcePolicy.ts index 1f4ff5971..77443e1a2 100644 --- a/server/private/routers/policy/updateResourcePolicy.ts +++ b/server/routers/policy/updateResourcePolicy.ts @@ -4,14 +4,7 @@ import { OpenAPITags, registry } from "@server/openApi"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import { fromError } from "zod-validation-error"; -import { - db, - orgs, - resourcePolicies, - rolePolicies, - userPolicies, - type ResourcePolicy -} from "@server/db"; +import { db, orgs, resourcePolicies, type ResourcePolicy } from "@server/db"; import { and, eq } from "drizzle-orm"; import logger from "@server/logger"; import response from "@server/lib/response"; diff --git a/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx b/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx index 61833a1f1..9c18c9b96 100644 --- a/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx +++ b/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx @@ -3,7 +3,7 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { Button } from "@app/components/ui/button"; import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; -import type { ResourcePolicy } from "@server/db"; +import { ResourcePolicyProvider } from "@app/providers/ResourcePolicyProvider"; import type { GetResourcePolicyResponse } from "@server/routers/policy"; import type { AxiosResponse } from "axios"; import { getTranslations } from "next-intl/server"; @@ -18,7 +18,7 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) { const params = await props.params; const t = await getTranslations(); - let policy: ResourcePolicy | null = null; + let policyResponse: GetResourcePolicyResponse | null = null; try { const res = await internal.get< AxiosResponse @@ -26,12 +26,12 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) { `/org/${params.orgId}/resource-policy/${params.niceId}`, await authCookieHeader() ); - policy = res.data.data.policy; + policyResponse = res.data.data; } catch { redirect(`/${params.orgId}/settings/policies/resource`); } - if (!policy) { + if (!policyResponse) { redirect(`/${params.orgId}/settings/policies/resource`); } @@ -40,7 +40,7 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) {
@@ -52,7 +52,9 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) {
- + + + ); } diff --git a/src/components/resource-policy/CreatePolicyForm.tsx b/src/components/resource-policy/CreatePolicyForm.tsx index 10982c0cc..d805e8772 100644 --- a/src/components/resource-policy/CreatePolicyForm.tsx +++ b/src/components/resource-policy/CreatePolicyForm.tsx @@ -204,14 +204,10 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { }); if (res && res.status === 201) { - const id = res.data.data.resourcePolicyId; const niceId = res.data.data.niceId; - - router.push(`/${org.org.orgId}/settings/policies/resources/`); - // should redirect to the details page - // router.push( - // `/${org.org.orgId}/settings/policies/resources/${niceId}` - // ); + router.push( + `/${org.org.orgId}/settings/policies/resource/${niceId}` + ); toast({ title: t("success"), description: t("policyCreatedSuccess") diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index 4331fb286..5477e8704 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -121,23 +121,21 @@ import { import { useCallback, useMemo, useState, useActionState } from "react"; import { UseFormReturn, useForm, useWatch } from "react-hook-form"; +import router from "next/navigation"; +import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; // ─── EditPolicyForm ───────────────────────────────────────────────────────── export type EditPolicyFormProps = { - policy: ResourcePolicy; hidePolicyNameForm?: boolean; }; -export function EditPolicyForm({ - hidePolicyNameForm, - policy -}: EditPolicyFormProps) { +export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) { const { org } = useOrgContext(); const t = useTranslations(); const { env } = useEnvContext(); const api = createApiClient({ env }); - const [, formAction, isSubmitting] = useActionState(onSubmit, null); + // const [, formAction, isSubmitting] = useActionState(onSubmit, null); const { isPaidUser } = usePaidStatus(); const router = useRouter(); @@ -145,7 +143,7 @@ export function EditPolicyForm({ const isMaxmindAvailable = !!( env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0 ); - const isMaxmindAsnAvailable = !!( + const isMaxmindASNAvailable = !!( env.server.maxmind_asn_path && env.server.maxmind_asn_path.length > 0 ); @@ -162,75 +160,76 @@ export function EditPolicyForm({ }) ); - const form = useForm({ - resolver: zodResolver(createPolicySchema) as any, - defaultValues: { - name: policy.name, - sso: true, - skipToIdpId: null, - emailWhitelistEnabled: false, - roles: [], - users: [], - emails: [], - applyRules: false, - rules: [], - password: null, - headerAuth: null, - pincode: null - } - }); + // const form = useForm({ + // resolver: zodResolver(createPolicySchema) as any, + // defaultValues: { + // name: "", + // sso: true, + // skipToIdpId: null, + // emailWhitelistEnabled: false, + // roles: [], + // users: [], + // emails: [], + // applyRules: false, + // rules: [], + // password: null, + // headerAuth: null, + // pincode: null + // } + // }); - async function onSubmit() { - const isValid = await form.trigger(); + // async function onSubmit() { + // return; + // // const isValid = await form.trigger(); - if (!isValid) return; + // // if (!isValid) return; - const payload = form.getValues(); + // // const payload = form.getValues(); - try { - const res = await api - .post>( - `/org/${org.org.orgId}/resource-policy/`, - { - name: payload.name, - sso: payload.sso, - roleIds: payload.roles.map((r) => r.id), - userIds: payload.users.map((u) => u.id) - } - ) - .catch((e) => { - toast({ - variant: "destructive", - title: t("policyErrorCreate"), - description: formatAxiosError( - e, - t("policyErrorCreateDescription") - ) - }); - }); + // // try { + // // const res = await api + // // .post>( + // // `/org/${org.org.orgId}/resource-policy/`, + // // { + // // name: payload.name, + // // sso: payload.sso, + // // roleIds: payload.roles.map((r) => r.id), + // // userIds: payload.users.map((u) => u.id) + // // } + // // ) + // // .catch((e) => { + // // toast({ + // // variant: "destructive", + // // title: t("policyErrorCreate"), + // // description: formatAxiosError( + // // e, + // // t("policyErrorCreateDescription") + // // ) + // // }); + // // }); - if (res && res.status === 201) { - const id = res.data.data.resourcePolicyId; - const niceId = res.data.data.niceId; + // // if (res && res.status === 201) { + // // const id = res.data.data.resourcePolicyId; + // // const niceId = res.data.data.niceId; - router.push(`/${org.org.orgId}/settings/policies/resources/`); - // should redirect to the details page - // router.push( - // `/${org.org.orgId}/settings/policies/resources/${niceId}` - // ); - toast({ - title: t("success"), - description: t("policyCreatedSuccess") - }); - } - } catch (e) { - toast({ - variant: "destructive", - title: t("policyErrorCreate"), - description: t("policyErrorCreateMessageDescription") - }); - } - } + // // router.push(`/${org.org.orgId}/settings/policies/resources/`); + // // // should redirect to the details page + // // // router.push( + // // // `/${org.org.orgId}/settings/policies/resources/${niceId}` + // // // ); + // // toast({ + // // title: t("success"), + // // description: t("policyCreatedSuccess") + // // }); + // // } + // // } catch (e) { + // // toast({ + // // variant: "destructive", + // // title: t("policyErrorCreate"), + // // description: t("policyErrorCreateMessageDescription") + // // }); + // // } + // } const allRoles = useMemo( () => @@ -271,12 +270,12 @@ export function EditPolicyForm({ } return ( -
- - - {/* Name */} - {!hidePolicyNameForm && } - + // + + {/* Name */} + {!hidePolicyNameForm && } + {/* - - - + isMaxmindAsnAvailable={isMaxmindASNAvailable} + /> */} +
+ // + // ); } // ─── PolicyNameSection ────────────────────────────────────────────────── -type PolicyNameSectionProps = { - form: UseFormReturn; - isEditing?: boolean; -}; -export function PolicyNameSection({ form }: PolicyNameSectionProps) { +export function PolicyNameSection() { const t = useTranslations(); - return ( - - - - {t("resourcePolicyName")} - - - {t("resourcePolicyNameDescription")} - - - - - ( - - {t("name")} - - - - - - )} - /> - - + const api = createApiClient(useEnvContext()); -
- -
-
+ const { policy } = useResourcePolicyContext(); + const { org } = useOrgContext(); + const form = useForm({ + resolver: zodResolver( + z.object({ + name: z.string() + }) + ), + defaultValues: { + name: policy.name + } + }); + + const [, formAction, isSubmitting] = useActionState(onSubmit, null); + + async function onSubmit() { + const isValid = await form.trigger(); + + if (!isValid) return; + + const payload = form.getValues(); + + try { + const res = await api + .put>( + `/resource-policy/${policy.resourcePolicyId}`, + { + name: payload.name + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: formatAxiosError( + e, + t("policyErrorUpdateDescription") + ) + }); + }); + + if (res && res.status === 200) { + toast({ + title: t("success"), + description: t("policyUpdatedSuccess") + }); + } + } catch (e) { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: t("policyErrorUpdateMessageDescription") + }); + } + } + + return ( +
+ + + + + {t("resourcePolicyName")} + + + {t("resourcePolicyNameDescription")} + + + + + ( + + {t("name")} + + + + + + )} + /> + + + +
+ +
+
+
+ ); } diff --git a/src/providers/ResourcePolicyProvider.tsx b/src/providers/ResourcePolicyProvider.tsx new file mode 100644 index 000000000..c6300304b --- /dev/null +++ b/src/providers/ResourcePolicyProvider.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { createContext, useContext, useState } from "react"; +import { useTranslations } from "next-intl"; +import type { GetResourcePolicyResponse } from "@server/routers/policy"; + +interface ResourcePolicyProviderProps { + children: React.ReactNode; + policy: GetResourcePolicyResponse; +} + +export function ResourcePolicyProvider({ + children, + policy: serverPolicy +}: ResourcePolicyProviderProps) { + const [policy, setPolicy] = + useState(serverPolicy); + + const t = useTranslations(); + + const updatePolicy = ( + updatedPolicy: Partial + ) => { + if (!policy) { + throw new Error(t("resourceErrorNoUpdate")); + } + + setPolicy((prev) => { + if (!prev) { + return prev; + } + + return { + ...prev, + ...updatedPolicy + }; + }); + }; + + return ( + + {children} + + ); +} + +export type ResourcePolicyContextType = GetResourcePolicyResponse & { + updatePolicy: (updatedPolicy: Partial) => void; +}; + +export const ResourcePolicyContext = createContext< + ResourcePolicyContextType | undefined +>(undefined); + +export function useResourcePolicyContext() { + const context = useContext(ResourcePolicyContext); + if (context === undefined) { + throw new Error( + "useResourcePolicyContext must be used within a ResourcePolicyProvider" + ); + } + return context; +} From 2ef5d90e13d46e29e4b49db8c13b01454a9d2bb9 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 27 Feb 2026 04:24:33 +0100 Subject: [PATCH 030/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20update=20policy=20?= =?UTF-8?q?in=20integration=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/integration.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/routers/integration.ts b/server/routers/integration.ts index e52e710ee..3d4f29fec 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -410,6 +410,13 @@ authenticated.post( resource.updateResource ); +authenticated.put( + "/resource-policy/:resourcePolicyId", + verifyApiKeyResourcePolicyAccess, + verifyApiKeyHasAction(ActionsEnum.updateResourcePolicy), + policy.updateResourcePolicy +); + authenticated.delete( "/resource/:resourceId", verifyApiKeyResourceAccess, From 7b02d4104d0fedd679a207e287b799cb3a329562 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 28 Feb 2026 00:47:27 +0100 Subject: [PATCH 031/771] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../policy/setResourcePolicyAccessControl.ts | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 server/routers/policy/setResourcePolicyAccessControl.ts diff --git a/server/routers/policy/setResourcePolicyAccessControl.ts b/server/routers/policy/setResourcePolicyAccessControl.ts new file mode 100644 index 000000000..72541642d --- /dev/null +++ b/server/routers/policy/setResourcePolicyAccessControl.ts @@ -0,0 +1,98 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { userResources } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { eq } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const setUserResourcesBodySchema = z.strictObject({ + userIds: z.array(z.string()) +}); + +const setResourcePolicyAccessControlParamsSchema = z.strictObject({ + resourcePolicyId: z.string().transform(Number).pipe(z.int().positive()) +}); + +registry.registerPath({ + method: "post", + path: "/resource-policy/{resourceId}/access-control", + description: + "Set access control users for a resource policy, including SSO, users, authentication.", + tags: [OpenAPITags.Resource, OpenAPITags.User], + request: { + params: setResourcePolicyAccessControlParamsSchema, + body: { + content: { + "application/json": { + schema: setUserResourcesBodySchema + } + } + } + }, + responses: {} +}); + +export async function setResourceUsers( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = setUserResourcesBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { userIds } = parsedBody.data; + + const parsedParams = setUserResourcesParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { resourceId } = parsedParams.data; + + await db.transaction(async (trx) => { + await trx + .delete(userResources) + .where(eq(userResources.resourceId, resourceId)); + + const newUserResources = await Promise.all( + userIds.map((userId) => + trx + .insert(userResources) + .values({ userId, resourceId }) + .returning() + ) + ); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Users set for resource successfully", + status: HttpCode.CREATED + }); + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} From 18964ba2a32498a3742835506502a0ebf325ca97 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 28 Feb 2026 14:22:41 +0100 Subject: [PATCH 032/771] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../policy/setResourcePolicyAccessControl.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/server/routers/policy/setResourcePolicyAccessControl.ts b/server/routers/policy/setResourcePolicyAccessControl.ts index 72541642d..14417df29 100644 --- a/server/routers/policy/setResourcePolicyAccessControl.ts +++ b/server/routers/policy/setResourcePolicyAccessControl.ts @@ -10,8 +10,11 @@ import { fromError } from "zod-validation-error"; import { eq } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; -const setUserResourcesBodySchema = z.strictObject({ - userIds: z.array(z.string()) +const setResourcePolicyAcccessControlBodySchema = z.strictObject({ + sso: z.boolean(), + userIds: z.array(z.string()), + roleIds: z.array(z.int().positive()), + skipToIdpId: z.string().optional() }); const setResourcePolicyAccessControlParamsSchema = z.strictObject({ @@ -22,14 +25,14 @@ registry.registerPath({ method: "post", path: "/resource-policy/{resourceId}/access-control", description: - "Set access control users for a resource policy, including SSO, users, authentication.", + "Set access control users for a resource policy, including SSO, users, roles, Identity provider.", tags: [OpenAPITags.Resource, OpenAPITags.User], request: { params: setResourcePolicyAccessControlParamsSchema, body: { content: { "application/json": { - schema: setUserResourcesBodySchema + schema: setResourcePolicyAcccessControlBodySchema } } } @@ -43,7 +46,9 @@ export async function setResourceUsers( next: NextFunction ): Promise { try { - const parsedBody = setUserResourcesBodySchema.safeParse(req.body); + const parsedBody = setResourcePolicyAcccessControlBodySchema.safeParse( + req.body + ); if (!parsedBody.success) { return next( createHttpError( From e7ab9b3f37dc0217611f7bdabda4b42e07e97b73 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 2 Mar 2026 18:32:08 +0100 Subject: [PATCH 033/771] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../policy/setResourcePolicyAccessControl.ts | 20 +++++++++---------- .../resource-policy/EditPolicyForm.tsx | 5 +++-- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/server/routers/policy/setResourcePolicyAccessControl.ts b/server/routers/policy/setResourcePolicyAccessControl.ts index 14417df29..430c9e59f 100644 --- a/server/routers/policy/setResourcePolicyAccessControl.ts +++ b/server/routers/policy/setResourcePolicyAccessControl.ts @@ -60,7 +60,8 @@ export async function setResourceUsers( const { userIds } = parsedBody.data; - const parsedParams = setUserResourcesParamsSchema.safeParse(req.params); + const parsedParams = + setResourcePolicyAccessControlParamsSchema.safeParse(req.params); if (!parsedParams.success) { return next( createHttpError( @@ -70,7 +71,7 @@ export async function setResourceUsers( ); } - const { resourceId } = parsedParams.data; + const { resourcePolicyId } = parsedParams.data; await db.transaction(async (trx) => { await trx @@ -85,14 +86,13 @@ export async function setResourceUsers( .returning() ) ); - - return response(res, { - data: {}, - success: true, - error: false, - message: "Users set for resource successfully", - status: HttpCode.CREATED - }); + }); + return response(res, { + data: {}, + success: true, + error: false, + message: "Users set for resource successfully", + status: HttpCode.CREATED }); } catch (error) { logger.error(error); diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index 5477e8704..16f85cdca 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -276,11 +276,12 @@ export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) { {/* Name */} {!hidePolicyNameForm && } {/* + /> */} + {/* Date: Mon, 2 Mar 2026 19:26:51 +0100 Subject: [PATCH 034/771] =?UTF-8?q?=E2=9C=A8=20update=20policy=20access=20?= =?UTF-8?q?control?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routers/policy/createResourcePolicy.ts | 6 +- .../policy/setResourcePolicyAccessControl.ts | 163 ++++++++++++++++-- 2 files changed, 150 insertions(+), 19 deletions(-) diff --git a/server/private/routers/policy/createResourcePolicy.ts b/server/private/routers/policy/createResourcePolicy.ts index 6cf710810..dc6780616 100644 --- a/server/private/routers/policy/createResourcePolicy.ts +++ b/server/private/routers/policy/createResourcePolicy.ts @@ -27,7 +27,7 @@ const createResourcePolicyParamsSchema = z.strictObject({ const createResourcePolicyBodySchema = z.strictObject({ name: z.string().min(1).max(255), sso: z.boolean(), - skipToIdpId: z.string().optional(), + skipToIdpId: z.int().positive().optional(), roleIds: z .array(z.string().transform(Number).pipe(z.int().positive())) .optional() @@ -150,7 +150,9 @@ export async function createResourcePolicy( .select() .from(users) .innerJoin(userOrgs, eq(userOrgs.userId, users.userId)) - .where(and(inArray(users.userId, userIds))); + .where( + and(eq(userOrgs.orgId, orgId), inArray(users.userId, userIds)) + ); const niceId = await getUniqueResourcePolicyName(orgId); diff --git a/server/routers/policy/setResourcePolicyAccessControl.ts b/server/routers/policy/setResourcePolicyAccessControl.ts index 430c9e59f..98f43f5fa 100644 --- a/server/routers/policy/setResourcePolicyAccessControl.ts +++ b/server/routers/policy/setResourcePolicyAccessControl.ts @@ -1,20 +1,29 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; -import { userResources } from "@server/db"; +import { + db, + idp, + idpOrg, + resourcePolicies, + rolePolicies, + roles, + userOrgs, + users +} from "@server/db"; +import { userPolicies } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { eq } from "drizzle-orm"; +import { and, eq, inArray, ne, type InferInsertModel } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; const setResourcePolicyAcccessControlBodySchema = z.strictObject({ sso: z.boolean(), userIds: z.array(z.string()), roleIds: z.array(z.int().positive()), - skipToIdpId: z.string().optional() + skipToIdpId: z.int().positive().optional() }); const setResourcePolicyAccessControlParamsSchema = z.strictObject({ @@ -58,7 +67,7 @@ export async function setResourceUsers( ); } - const { userIds } = parsedBody.data; + const { userIds, roleIds, sso, skipToIdpId: idpId } = parsedBody.data; const parsedParams = setResourcePolicyAccessControlParamsSchema.safeParse(req.params); @@ -73,26 +82,146 @@ export async function setResourceUsers( const { resourcePolicyId } = parsedParams.data; - await db.transaction(async (trx) => { - await trx - .delete(userResources) - .where(eq(userResources.resourceId, resourceId)); + const [policy] = await db + .select() + .from(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)) + .limit(1); - const newUserResources = await Promise.all( - userIds.map((userId) => - trx - .insert(userResources) - .values({ userId, resourceId }) - .returning() + if (!policy) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Resource policy not found" ) ); + } + + // Check if Identity provider in `skipToIdpId` exists + if (idpId) { + const [provider] = await db + .select() + .from(idp) + .innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId)) + .where( + and(eq(idp.idpId, idpId), eq(idpOrg.orgId, policy.orgId)) + ) + .limit(1); + + if (!provider) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Identity provider not found in this organization" + ) + ); + } + } + + // Check if any of the roleIds are admin roles + const rolesToCheck = await db + .select() + .from(roles) + .where( + and( + inArray(roles.roleId, roleIds), + eq(roles.orgId, policy.orgId) + ) + ); + + const hasAdminRole = rolesToCheck.some((role) => role.isAdmin); + + if (hasAdminRole) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Admin role cannot be assigned to resources" + ) + ); + } + + // Get all admin role IDs for this org to exclude from deletion + const adminRoles = await db + .select() + .from(roles) + .where(and(eq(roles.isAdmin, true), eq(roles.orgId, policy.orgId))); + const adminRoleIds = adminRoles.map((role) => role.roleId); + + const existingUsers = await db + .select() + .from(users) + .innerJoin(userOrgs, eq(userOrgs.userId, users.userId)) + .where( + and( + eq(userOrgs.orgId, policy.orgId), + inArray(users.userId, userIds) + ) + ); + + const existingRoles = await db + .select() + .from(roles) + .where( + and( + eq(roles.orgId, policy.orgId), + inArray(roles.roleId, roleIds) + ) + ); + + await db.transaction(async (trx) => { + // Update SSO status + await trx + .update(resourcePolicies) + .set({ + sso, + idpId + }) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)); + + // Update roles + if (adminRoleIds.length > 0) { + await trx.delete(rolePolicies).where( + and( + eq(rolePolicies.resourcePolicyId, resourcePolicyId), + ne(rolePolicies.roleId, adminRoleIds[0]) // delete all but the admin role + ) + ); + } else { + await trx + .delete(rolePolicies) + .where(eq(rolePolicies.resourcePolicyId, resourcePolicyId)); + } + + const rolesToAdd = existingRoles.map(({ roleId }) => ({ + roleId, + resourcePolicyId + })); + + if (rolesToAdd.length > 0) { + await trx.insert(rolePolicies).values(rolesToAdd); + } + + // Update users + await trx + .delete(userPolicies) + .where(eq(userPolicies.resourcePolicyId, resourcePolicyId)); + + const usersToAdd = existingUsers.map(({ user }) => ({ + userId: user.userId, + resourcePolicyId: resourcePolicyId + })); + + if (usersToAdd.length > 0) { + await trx.insert(userPolicies).values(usersToAdd); + } }); + return response(res, { data: {}, success: true, error: false, - message: "Users set for resource successfully", - status: HttpCode.CREATED + message: "Resource policy succesfully updated", + status: HttpCode.OK }); } catch (error) { logger.error(error); From 033cc62ce7dcb11c01f0e39e99510bed87afd02f Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 2 Mar 2026 19:37:23 +0100 Subject: [PATCH 035/771] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/external.ts | 8 +++++ server/routers/integration.ts | 15 ++++++++- server/routers/policy/index.ts | 1 + .../policy/setResourcePolicyAccessControl.ts | 2 +- .../resource-policy/EditPolicyForm.tsx | 31 ++++++++++++++----- 5 files changed, 47 insertions(+), 10 deletions(-) diff --git a/server/routers/external.ts b/server/routers/external.ts index 715988cf3..bb4ee7d31 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -700,6 +700,14 @@ authenticated.get( resource.listResourcePolicyRoles ); +authenticated.put( + "/resource-policy/:resourcePolicyId/access-control", + verifyResourcePolicyAccess, + verifyUserHasAction(ActionsEnum.setResourcePolicyUsers), + verifyUserHasAction(ActionsEnum.setResourcePolicyRoles), + policy.setResourcePolicyAccessControl +); + authenticated.get( "/resource-policy/:resourcePolicyId/users", verifyResourcePolicyAccess, diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 41557ba30..a68bcb555 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -30,7 +30,8 @@ import { verifyApiKeySetResourceClients, verifyLimits, verifyApiKeyDomainAccess, - verifyApiKeyResourcePolicyAccess + verifyApiKeyResourcePolicyAccess, + verifyUserHasAction } from "@server/middlewares"; import HttpCode from "@server/types/HttpCode"; import { Router } from "express"; @@ -619,6 +620,18 @@ authenticated.post( resource.setResourceUsers ); +authenticated.put( + "/resource-policy/:resourcePolicyId/access-control", + verifyApiKeyResourcePolicyAccess, + verifyApiKeyRoleAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.setResourcePolicyUsers), + verifyUserHasAction(ActionsEnum.setResourcePolicyRoles), + logActionAudit(ActionsEnum.setResourcePolicyUsers), + logActionAudit(ActionsEnum.setResourcePolicyRoles), + policy.setResourcePolicyAccessControl +); + authenticated.post( "/resource/:resourceId/roles/add", verifyApiKeyResourceAccess, diff --git a/server/routers/policy/index.ts b/server/routers/policy/index.ts index 8cd264925..9ad10eb45 100644 --- a/server/routers/policy/index.ts +++ b/server/routers/policy/index.ts @@ -1,2 +1,3 @@ export * from "./getResourcePolicy"; export * from "./updateResourcePolicy"; +export * from "./setResourcePolicyAccessControl"; diff --git a/server/routers/policy/setResourcePolicyAccessControl.ts b/server/routers/policy/setResourcePolicyAccessControl.ts index 98f43f5fa..926478db8 100644 --- a/server/routers/policy/setResourcePolicyAccessControl.ts +++ b/server/routers/policy/setResourcePolicyAccessControl.ts @@ -49,7 +49,7 @@ registry.registerPath({ responses: {} }); -export async function setResourceUsers( +export async function setResourcePolicyAccessControl( req: Request, res: Response, next: NextFunction diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index 16f85cdca..88c6ceafa 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -275,12 +275,11 @@ export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) { {/* Name */} {!hidePolicyNameForm && } - {/* */} + {/* ; allRoles: { id: string; text: string }[]; allUsers: { id: string; text: string }[]; allIdps: { id: number; text: string }[]; }; export function PolicyUsersRolesSection({ - form, allRoles, allUsers, allIdps }: PolicyUsersRolesSectionProps) { const t = useTranslations(); + + const { policy } = useResourcePolicyContext(); + + const form = useForm({ + resolver: zodResolver( + createPolicySchema.pick({ + sso: true, + skipToIdpId: true, + users: true, + roles: true + }) + ), + defaultValues: { + sso: policy.sso, + skipToIdpId: policy.idpId + } + }); + const ssoEnabled = useWatch({ control: form.control, name: "sso" }); const selectedIdpId = useWatch({ control: form.control, From 5c280b024ea15888ced726f18e66ba5c6463ac23 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 3 Mar 2026 01:33:37 +0100 Subject: [PATCH 036/771] =?UTF-8?q?=E2=9C=A8=20update=20policy=20access=20?= =?UTF-8?q?control?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routers/policy/createResourcePolicy.ts | 29 +- server/routers/policy/getResourcePolicy.ts | 64 ++- .../policy/setResourcePolicyAccessControl.ts | 4 +- .../policies/resource/[niceId]/page.tsx | 2 +- .../resource-policy/EditPolicyForm.tsx | 421 +++++++++++------- src/providers/ResourcePolicyProvider.tsx | 5 +- 6 files changed, 335 insertions(+), 190 deletions(-) diff --git a/server/private/routers/policy/createResourcePolicy.ts b/server/private/routers/policy/createResourcePolicy.ts index dc6780616..29bccd48b 100644 --- a/server/private/routers/policy/createResourcePolicy.ts +++ b/server/private/routers/policy/createResourcePolicy.ts @@ -6,6 +6,8 @@ import createHttpError from "http-errors"; import { fromError } from "zod-validation-error"; import { db, + idp, + idpOrg, orgs, resourcePolicies, rolePolicies, @@ -107,15 +109,23 @@ export async function createResourcePolicy( const { name, sso, userIds, roleIds, skipToIdpId } = parsedBody.data; - const isAuthEnabeld = sso; // other conditions will follow + // Check if Identity provider in `skipToIdpId` exists + if (skipToIdpId) { + const [provider] = await db + .select() + .from(idp) + .innerJoin(idpOrg, eq(idpOrg.idpId, idp.idpId)) + .where(and(eq(idp.idpId, skipToIdpId), eq(idpOrg.orgId, orgId))) + .limit(1); - if (!isAuthEnabeld) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - "At least one authentication policy must be set: platform SSO, an authentication method, one-time password, or a rule." - ) - ); + if (!provider) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Identity provider not found in this organization" + ) + ); + } } const adminRole = await db @@ -163,7 +173,8 @@ export async function createResourcePolicy( niceId, orgId, name, - sso + sso, + idpId: skipToIdpId }) .returning(); diff --git a/server/routers/policy/getResourcePolicy.ts b/server/routers/policy/getResourcePolicy.ts index 0d33a222e..3b42ffc2e 100644 --- a/server/routers/policy/getResourcePolicy.ts +++ b/server/routers/policy/getResourcePolicy.ts @@ -1,9 +1,17 @@ -import { db, resourcePolicies, rolePolicies, userPolicies } from "@server/db"; +import { + db, + idp, + resourcePolicies, + rolePolicies, + roles, + userPolicies, + users +} from "@server/db"; import response from "@server/lib/response"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; import HttpCode from "@server/types/HttpCode"; -import { and, eq, type SQL } from "drizzle-orm"; +import { and, eq, isNull, not, or, type SQL } from "drizzle-orm"; import type { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import z from "zod"; @@ -34,26 +42,48 @@ async function query(params: z.infer) { } const [res] = await db - .select({ - policy: resourcePolicies, - userPolicies, - rolePolicies - }) + .select() .from(resourcePolicies) - .leftJoin( - userPolicies, - eq(userPolicies.resourcePolicyId, resourcePolicies.resourcePolicyId) - ) - .leftJoin( - rolePolicies, - eq(rolePolicies.resourcePolicyId, resourcePolicies.resourcePolicyId) - ) .where(and(...conditions)) .limit(1); - return res; + + if (!res) return null; + + const policyUsers = await db + .select({ + userId: userPolicies.userId, + email: users.email, + name: users.name, + username: users.username, + type: users.type, + idpName: idp.name + }) + .from(userPolicies) + .innerJoin(users, eq(userPolicies.userId, users.userId)) + .leftJoin(idp, eq(idp.idpId, users.idpId)) + .where(eq(userPolicies.resourcePolicyId, res.resourcePolicyId)); + + const policyRoles = await db + .select({ + roleId: rolePolicies.roleId, + name: roles.name + }) + .from(rolePolicies) + .innerJoin( + roles, + and( + eq(rolePolicies.roleId, roles.roleId), + or(isNull(roles.isAdmin), not(roles.isAdmin)) + ) + ) + .where(eq(rolePolicies.resourcePolicyId, res.resourcePolicyId)); + + return { ...res, roles: policyRoles, users: policyUsers }; } -export type GetResourcePolicyResponse = Awaited>; +export type GetResourcePolicyResponse = NonNullable< + Awaited> +>; registry.registerPath({ method: "get", diff --git a/server/routers/policy/setResourcePolicyAccessControl.ts b/server/routers/policy/setResourcePolicyAccessControl.ts index 926478db8..7bbbe8a38 100644 --- a/server/routers/policy/setResourcePolicyAccessControl.ts +++ b/server/routers/policy/setResourcePolicyAccessControl.ts @@ -16,14 +16,14 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { and, eq, inArray, ne, type InferInsertModel } from "drizzle-orm"; +import { and, eq, inArray, ne } from "drizzle-orm"; import { OpenAPITags, registry } from "@server/openApi"; const setResourcePolicyAcccessControlBodySchema = z.strictObject({ sso: z.boolean(), userIds: z.array(z.string()), roleIds: z.array(z.int().positive()), - skipToIdpId: z.int().positive().optional() + skipToIdpId: z.int().positive().optional().nullish() }); const setResourcePolicyAccessControlParamsSchema = z.strictObject({ diff --git a/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx b/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx index 9c18c9b96..5519506b9 100644 --- a/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx +++ b/src/app/[orgId]/settings/(private)/policies/resource/[niceId]/page.tsx @@ -40,7 +40,7 @@ export default async function EditPolicyPage(props: EditPolicyPageProps) {
diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index 88c6ceafa..20c7a76e6 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -123,6 +123,7 @@ import { useCallback, useMemo, useState, useActionState } from "react"; import { UseFormReturn, useForm, useWatch } from "react-hook-form"; import router from "next/navigation"; import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; +import { t } from "@faker-js/faker/dist/airline-CWrCIUHH"; // ─── EditPolicyForm ───────────────────────────────────────────────────────── @@ -426,6 +427,10 @@ export function PolicyUsersRolesSection({ const { policy } = useResourcePolicyContext(); + const api = createApiClient(useEnvContext()); + + console.log({ policy }); + const form = useForm({ resolver: zodResolver( createPolicySchema.pick({ @@ -437,7 +442,15 @@ export function PolicyUsersRolesSection({ ), defaultValues: { sso: policy.sso, - skipToIdpId: policy.idpId + skipToIdpId: policy.idpId, + roles: policy.roles.map((role) => ({ + id: role.roleId.toString(), + text: role.name + })), + users: policy.users.map((user) => ({ + id: user.userId, + text: `${getUserDisplayName({ email: user.email, username: user.username })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` + })) } }); @@ -453,167 +466,257 @@ export function PolicyUsersRolesSection({ number | null >(null); + const [, formAction, isSubmitting] = useActionState(onSubmit, null); + + async function onSubmit() { + const isValid = await form.trigger(); + + if (!isValid) return; + + const payload = form.getValues(); + + try { + const res = await api + .put>( + `/resource-policy/${policy.resourcePolicyId}/access-control`, + { + sso: payload.sso, + userIds: payload.users.map((user) => user.id), + roleIds: payload.roles.map((role) => Number(role.id)), + skipToIdpId: payload.skipToIdpId + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: formatAxiosError( + e, + t("policyErrorUpdateDescription") + ) + }); + }); + + if (res && res.status === 200) { + toast({ + title: t("success"), + description: t("policyUpdatedSuccess") + }); + } + } catch (e) { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: t("policyErrorUpdateMessageDescription") + }); + } + } + return ( - - - - {t("resourceUsersRoles")} - - - {t("resourcePolicyUsersRolesDescription")} - - - - - { - console.log(`form.setValue("sso", ${val})`); - form.setValue("sso", val); - }} - /> - - {ssoEnabled && ( - <> - ( - - {t("roles")} - - { - form.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={true} - autocompleteOptions={allRoles} - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - {t("resourceRoleDescription")} - - - )} - /> - ( - - {t("users")} - - { - form.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={true} - autocompleteOptions={allUsers} - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - )} - /> - - )} - - {ssoEnabled && allIdps.length > 0 && ( -
- - -

- {t("defaultIdentityProviderDescription")} -

-
- )} -
-
-
+ ( + + + {t("users")} + + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allUsers + } + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + )} + /> + + )} + + {ssoEnabled && allIdps.length > 0 && ( +
+ + +

+ {t( + "defaultIdentityProviderDescription" + )} +

+
+ )} + + + +
+ +
+ + + ); } diff --git a/src/providers/ResourcePolicyProvider.tsx b/src/providers/ResourcePolicyProvider.tsx index c6300304b..e80704dc4 100644 --- a/src/providers/ResourcePolicyProvider.tsx +++ b/src/providers/ResourcePolicyProvider.tsx @@ -38,13 +38,14 @@ export function ResourcePolicyProvider({ }; return ( - + {children} ); } -export type ResourcePolicyContextType = GetResourcePolicyResponse & { +export type ResourcePolicyContextType = { + policy: GetResourcePolicyResponse; updatePolicy: (updatedPolicy: Partial) => void; }; From 8a54fb7f23ff462deb1695e4c85ff9fa0fe74e90 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 3 Mar 2026 02:11:05 +0100 Subject: [PATCH 037/771] =?UTF-8?q?=F0=9F=9A=A7=20auth=20methods?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resource-policy/EditPolicyForm.tsx | 282 +++++++++++------- 1 file changed, 166 insertions(+), 116 deletions(-) diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index 20c7a76e6..d138ff1a8 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -281,8 +281,8 @@ export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) { allUsers={allUsers} allIdps={allIdps} /> + {/* - ; -}; +export function PolicyAuthMethodsSection() { + const { policy } = useResourcePolicyContext(); + + const api = createApiClient(useEnvContext()); + + const form = useForm({ + resolver: zodResolver( + createPolicySchema.pick({ + password: true, + pincode: true, + headerAuth: true + }) + ), + defaultValues: {} + }); -export function PolicyAuthMethodsSection({ - form -}: PolicyAuthMethodsSectionProps) { const t = useTranslations(); - const [isOpen, setIsOpen] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); @@ -768,7 +775,18 @@ export function PolicyAuthMethodsSection({ defaultValues: { user: "", password: "", extendedCompatibility: true } }); - if (!isOpen) { + const [, formAction, isSubmitting] = useActionState(onSubmit, null); + + async function onSubmit() { + const isValid = await form.trigger(); + + if (!isValid) return; + + const payload = form.getValues(); + console.log({ payload }); + } + + if (!isExpanded) { return ( @@ -783,7 +801,7 @@ export function PolicyAuthMethodsSection({ -
+
+ {}}> + + + + {t("resourceAuthMethods")} + + + {t("resourcePolicyAuthMethodsDescription")} + + + + + {/* Password row */} +
+
+ + + {t("resourcePasswordProtection", { + status: password + ? t("enabled") + : t("disabled") + })} + +
+ +
- {/* Pincode row */} -
-
- - - {t("resourcePincodeProtection", { - status: pincode - ? t("enabled") - : t("disabled") - })} - -
- -
+ {/* Pincode row */} +
+
+ + + {t("resourcePincodeProtection", { + status: pincode + ? t("enabled") + : t("disabled") + })} + +
+ +
- {/* Header auth row */} -
-
- - - {headerAuth - ? t( - "resourceHeaderAuthProtectionEnabled" - ) - : t( - "resourceHeaderAuthProtectionDisabled" - )} - -
+ {/* Header auth row */} +
+
+ + + {headerAuth + ? t( + "resourceHeaderAuthProtectionEnabled" + ) + : t( + "resourceHeaderAuthProtectionDisabled" + )} + +
+ +
+ + + +
- - - + + + ); } @@ -1170,13 +1220,13 @@ export function PolicyOtpEmailSection({ emailEnabled }: PolicyOtpEmailSectionProps) { const t = useTranslations(); - const [isOpen, setIsOpen] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); const [whitelistEnabled, setWhitelistEnabled] = useState(false); const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< number | null >(null); - if (!isOpen) { + if (!isExpanded) { return ( @@ -1191,7 +1241,7 @@ export function PolicyOtpEmailSection({ + + + ); + } + + return ( + <> + {/* Password Credenza */} + { + setIsSetPasswordOpen(val); + if (!val) passwordForm.reset(); + }} + > + + + + {t("resourcePasswordSetupTitle")} + + + {t("resourcePasswordSetupTitleDescription")} + + + +
+ { + form.setValue("password", data); + setIsSetPasswordOpen(false); + passwordForm.reset(); + })} + className="space-y-4" + id="set-password-form" + > + ( + + + {t("password")} + + + + + + + )} + /> + + +
+ + + + + + +
+
+ + {/* Pincode Credenza */} + { + setIsSetPincodeOpen(val); + if (!val) pincodeForm.reset(); + }} + > + + + + {t("resourcePincodeSetupTitle")} + + + {t("resourcePincodeSetupTitleDescription")} + + + +
+ { + form.setValue("pincode", data); + setIsSetPincodeOpen(false); + pincodeForm.reset(); + })} + className="space-y-4" + id="set-pincode-form" + > + ( + + + {t("resourcePincode")} + + +
+ + + + + + + + + + +
+
+ +
+ )} + /> + + +
+ + + + + + +
+
+ + {/* Header Auth Credenza */} + { + setIsSetHeaderAuthOpen(val); + if (!val) headerAuthForm.reset(); + }} + > + + + + {t("resourceHeaderAuthSetupTitle")} + + + {t("resourceHeaderAuthSetupTitleDescription")} + + + +
+ { + form.setValue("headerAuth", data); + setIsSetHeaderAuthOpen(false); + headerAuthForm.reset(); + } + )} + className="space-y-4" + id="set-header-auth-form" + > + ( + + {t("user")} + + + + + + )} + /> + ( + + + {t("password")} + + + + + + + )} + /> + ( + + + + + + + )} + /> + + +
+ + + + + + +
+
+ +
+ {}}> + + + + {t("resourceAuthMethods")} + + + {t("resourcePolicyAuthMethodsDescription")} + + + + + {/* Password row */} +
+
+ + + {t("resourcePasswordProtection", { + status: hasPassword + ? t("enabled") + : t("disabled") + })} + +
+ +
+ + {/* Pincode row */} +
+
+ + + {t("resourcePincodeProtection", { + status: hasPincode + ? t("enabled") + : t("disabled") + })} + +
+ +
+ + {/* Header auth row */} +
+
+ + + {hasHeaderAuth + ? t( + "resourceHeaderAuthProtectionEnabled" + ) + : t( + "resourceHeaderAuthProtectionDisabled" + )} + +
+ +
+
+
+ +
+ +
+
+
+ + + ); +} diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index d138ff1a8..8b0376107 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -1,14 +1,6 @@ "use client"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionBody, - SettingsSectionDescription, - SettingsSectionForm, - SettingsSectionHeader, - SettingsSectionTitle -} from "@app/components/Settings"; +import { SettingsContainer } from "@app/components/Settings"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useOrgContext } from "@app/hooks/useOrgContext"; @@ -16,114 +8,19 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { orgQueries } from "@app/lib/queries"; -import { zodResolver } from "@hookform/resolvers/zod"; import { build } from "@server/build"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { UserType } from "@server/types/UserTypes"; import { useQuery } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; -import z from "zod"; - -import { type PolicyFormValues, createPolicySchema } from "."; -import { toast } from "@app/hooks/useToast"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { orgs, type ResourcePolicy } from "@server/db"; -import type { AxiosResponse } from "axios"; +import { createApiClient } from "@app/lib/api"; import { useRouter } from "next/navigation"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { Tag, TagInput } from "@app/components/tags/tag-input"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { Button } from "@app/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { InfoPopup } from "@app/components/ui/info-popup"; -import { Input } from "@app/components/ui/input"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@app/components/ui/select"; -import { Switch } from "@app/components/ui/switch"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow -} from "@app/components/ui/table"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; -import { - InputOTP, - InputOTPGroup, - InputOTPSlot -} from "@app/components/ui/input-otp"; - -import { MAJOR_ASNS } from "@server/db/asns"; -import { COUNTRIES } from "@server/db/countries"; -import { - isValidCIDR, - isValidIP, - isValidUrlGlobPattern -} from "@server/lib/validators"; -import { - ColumnDef, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable -} from "@tanstack/react-table"; -import { - ArrowUpDown, - Binary, - Bot, - Check, - ChevronsUpDown, - InfoIcon, - Key, - Plus -} from "lucide-react"; - -import { useCallback, useMemo, useState, useActionState } from "react"; -import { UseFormReturn, useForm, useWatch } from "react-hook-form"; -import router from "next/navigation"; -import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; -import { t } from "@faker-js/faker/dist/airline-CWrCIUHH"; +import { useMemo } from "react"; +import { EditPolicyAuthMethodsSectionForm } from "./EditPolicyAuthMethodsSectionForm"; +import { EditPolicyNameSectionForm } from "./EditPolicyNameSectionForm"; +import { EditPolicyUsersRolesSectionForm } from "./EditPolicyUserRolesSectionForm"; // ─── EditPolicyForm ───────────────────────────────────────────────────────── @@ -161,77 +58,6 @@ export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) { }) ); - // const form = useForm({ - // resolver: zodResolver(createPolicySchema) as any, - // defaultValues: { - // name: "", - // sso: true, - // skipToIdpId: null, - // emailWhitelistEnabled: false, - // roles: [], - // users: [], - // emails: [], - // applyRules: false, - // rules: [], - // password: null, - // headerAuth: null, - // pincode: null - // } - // }); - - // async function onSubmit() { - // return; - // // const isValid = await form.trigger(); - - // // if (!isValid) return; - - // // const payload = form.getValues(); - - // // try { - // // const res = await api - // // .post>( - // // `/org/${org.org.orgId}/resource-policy/`, - // // { - // // name: payload.name, - // // sso: payload.sso, - // // roleIds: payload.roles.map((r) => r.id), - // // userIds: payload.users.map((u) => u.id) - // // } - // // ) - // // .catch((e) => { - // // toast({ - // // variant: "destructive", - // // title: t("policyErrorCreate"), - // // description: formatAxiosError( - // // e, - // // t("policyErrorCreateDescription") - // // ) - // // }); - // // }); - - // // if (res && res.status === 201) { - // // const id = res.data.data.resourcePolicyId; - // // const niceId = res.data.data.niceId; - - // // router.push(`/${org.org.orgId}/settings/policies/resources/`); - // // // should redirect to the details page - // // // router.push( - // // // `/${org.org.orgId}/settings/policies/resources/${niceId}` - // // // ); - // // toast({ - // // title: t("success"), - // // description: t("policyCreatedSuccess") - // // }); - // // } - // // } catch (e) { - // // toast({ - // // variant: "destructive", - // // title: t("policyErrorCreate"), - // // description: t("policyErrorCreateMessageDescription") - // // }); - // // } - // } - const allRoles = useMemo( () => orgRoles @@ -271,2069 +97,26 @@ export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) { } return ( - //
- // {/* Name */} - {!hidePolicyNameForm && } - } + - + + {/* - - */} + + */} - //
- // - ); -} - -// ─── PolicyNameSection ────────────────────────────────────────────────── - -export function PolicyNameSection() { - const t = useTranslations(); - const api = createApiClient(useEnvContext()); - - const { policy } = useResourcePolicyContext(); - const { org } = useOrgContext(); - const form = useForm({ - resolver: zodResolver( - z.object({ - name: z.string() - }) - ), - defaultValues: { - name: policy.name - } - }); - - const [, formAction, isSubmitting] = useActionState(onSubmit, null); - - async function onSubmit() { - const isValid = await form.trigger(); - - if (!isValid) return; - - const payload = form.getValues(); - - try { - const res = await api - .put>( - `/resource-policy/${policy.resourcePolicyId}`, - { - name: payload.name - } - ) - .catch((e) => { - toast({ - variant: "destructive", - title: t("policyErrorUpdate"), - description: formatAxiosError( - e, - t("policyErrorUpdateDescription") - ) - }); - }); - - if (res && res.status === 200) { - toast({ - title: t("success"), - description: t("policyUpdatedSuccess") - }); - } - } catch (e) { - toast({ - variant: "destructive", - title: t("policyErrorUpdate"), - description: t("policyErrorUpdateMessageDescription") - }); - } - } - - return ( -
- - - - - {t("resourcePolicyName")} - - - {t("resourcePolicyNameDescription")} - - - - - ( - - {t("name")} - - - - - - )} - /> - - - -
- -
-
-
- - ); -} - -// ─── PolicyUsersRolesSection ────────────────────────────────────────────────── - -type PolicyUsersRolesSectionProps = { - allRoles: { id: string; text: string }[]; - allUsers: { id: string; text: string }[]; - allIdps: { id: number; text: string }[]; -}; - -export function PolicyUsersRolesSection({ - allRoles, - allUsers, - allIdps -}: PolicyUsersRolesSectionProps) { - const t = useTranslations(); - - const { policy } = useResourcePolicyContext(); - - const api = createApiClient(useEnvContext()); - - const form = useForm({ - resolver: zodResolver( - createPolicySchema.pick({ - sso: true, - skipToIdpId: true, - users: true, - roles: true - }) - ), - defaultValues: { - sso: policy.sso, - skipToIdpId: policy.idpId, - roles: policy.roles.map((role) => ({ - id: role.roleId.toString(), - text: role.name - })), - users: policy.users.map((user) => ({ - id: user.userId, - text: `${getUserDisplayName({ email: user.email, username: user.username })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` - })) - } - }); - - const ssoEnabled = useWatch({ control: form.control, name: "sso" }); - const selectedIdpId = useWatch({ - control: form.control, - name: "skipToIdpId" - }); - const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< - number | null - >(null); - const [activeUsersTagIndex, setActiveUsersTagIndex] = useState< - number | null - >(null); - - const [, formAction, isSubmitting] = useActionState(onSubmit, null); - - async function onSubmit() { - const isValid = await form.trigger(); - - if (!isValid) return; - - const payload = form.getValues(); - - try { - const res = await api - .put>( - `/resource-policy/${policy.resourcePolicyId}/access-control`, - { - sso: payload.sso, - userIds: payload.users.map((user) => user.id), - roleIds: payload.roles.map((role) => Number(role.id)), - skipToIdpId: payload.skipToIdpId - } - ) - .catch((e) => { - toast({ - variant: "destructive", - title: t("policyErrorUpdate"), - description: formatAxiosError( - e, - t("policyErrorUpdateDescription") - ) - }); - }); - - if (res && res.status === 200) { - toast({ - title: t("success"), - description: t("policyUpdatedSuccess") - }); - } - } catch (e) { - toast({ - variant: "destructive", - title: t("policyErrorUpdate"), - description: t("policyErrorUpdateMessageDescription") - }); - } - } - - return ( -
- - - - - {t("resourceUsersRoles")} - - - {t("resourcePolicyUsersRolesDescription")} - - - - - { - console.log(`form.setValue("sso", ${val})`); - form.setValue("sso", val); - }} - /> - - {ssoEnabled && ( - <> - ( - - - {t("roles")} - - - { - form.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allRoles - } - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - {t( - "resourceRoleDescription" - )} - - - )} - /> - ( - - - {t("users")} - - - { - form.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allUsers - } - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - )} - /> - - )} - - {ssoEnabled && allIdps.length > 0 && ( -
- - -

- {t( - "defaultIdentityProviderDescription" - )} -

-
- )} -
-
- -
- -
-
-
- - ); -} - -// ─── PolicyAuthMethodsSection ───────────────────────────────────────────────── - -const setPasswordSchema = z.object({ - password: z.string().min(4).max(100) -}); - -const setPincodeSchema = z.object({ - pincode: z.string().length(6) -}); - -const setHeaderAuthSchema = z.object({ - user: z.string().min(4).max(100), - password: z.string().min(4).max(100), - extendedCompatibility: z.boolean() -}); - -export function PolicyAuthMethodsSection() { - const { policy } = useResourcePolicyContext(); - - const api = createApiClient(useEnvContext()); - - const form = useForm({ - resolver: zodResolver( - createPolicySchema.pick({ - password: true, - pincode: true, - headerAuth: true - }) - ), - defaultValues: {} - }); - - const t = useTranslations(); - const [isExpanded, setIsExpanded] = useState(false); - const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); - const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); - const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); - - const password = form.watch("password"); - const pincode = form.watch("pincode"); - const headerAuth = form.watch("headerAuth"); - - const passwordForm = useForm({ - resolver: zodResolver(setPasswordSchema), - defaultValues: { password: "" } - }); - - const pincodeForm = useForm({ - resolver: zodResolver(setPincodeSchema), - defaultValues: { pincode: "" } - }); - - const headerAuthForm = useForm({ - resolver: zodResolver(setHeaderAuthSchema), - defaultValues: { user: "", password: "", extendedCompatibility: true } - }); - - const [, formAction, isSubmitting] = useActionState(onSubmit, null); - - async function onSubmit() { - const isValid = await form.trigger(); - - if (!isValid) return; - - const payload = form.getValues(); - console.log({ payload }); - } - - if (!isExpanded) { - return ( - - - - {t("resourceAuthMethods")} - - - {t("resourcePolicyAuthMethodsDescription")} - - - - - - - ); - } - - return ( - <> - {/* Password Credenza */} - { - setIsSetPasswordOpen(val); - if (!val) passwordForm.reset(); - }} - > - - - - {t("resourcePasswordSetupTitle")} - - - {t("resourcePasswordSetupTitleDescription")} - - - -
- { - form.setValue("password", data); - setIsSetPasswordOpen(false); - passwordForm.reset(); - })} - className="space-y-4" - id="set-password-form" - > - ( - - - {t("password")} - - - - - - - )} - /> - - -
- - - - - - -
-
- - {/* Pincode Credenza */} - { - setIsSetPincodeOpen(val); - if (!val) pincodeForm.reset(); - }} - > - - - - {t("resourcePincodeSetupTitle")} - - - {t("resourcePincodeSetupTitleDescription")} - - - -
- { - form.setValue("pincode", data); - setIsSetPincodeOpen(false); - pincodeForm.reset(); - })} - className="space-y-4" - id="set-pincode-form" - > - ( - - - {t("resourcePincode")} - - -
- - - - - - - - - - -
-
- -
- )} - /> - - -
- - - - - - -
-
- - {/* Header Auth Credenza */} - { - setIsSetHeaderAuthOpen(val); - if (!val) headerAuthForm.reset(); - }} - > - - - - {t("resourceHeaderAuthSetupTitle")} - - - {t("resourceHeaderAuthSetupTitleDescription")} - - - -
- { - form.setValue("headerAuth", data); - setIsSetHeaderAuthOpen(false); - headerAuthForm.reset(); - } - )} - className="space-y-4" - id="set-header-auth-form" - > - ( - - {t("user")} - - - - - - )} - /> - ( - - - {t("password")} - - - - - - - )} - /> - ( - - - - - - - )} - /> - - -
- - - - - - -
-
- -
- {}}> - - - - {t("resourceAuthMethods")} - - - {t("resourcePolicyAuthMethodsDescription")} - - - - - {/* Password row */} -
-
- - - {t("resourcePasswordProtection", { - status: password - ? t("enabled") - : t("disabled") - })} - -
- -
- - {/* Pincode row */} -
-
- - - {t("resourcePincodeProtection", { - status: pincode - ? t("enabled") - : t("disabled") - })} - -
- -
- - {/* Header auth row */} -
-
- - - {headerAuth - ? t( - "resourceHeaderAuthProtectionEnabled" - ) - : t( - "resourceHeaderAuthProtectionDisabled" - )} - -
- -
-
-
- -
- -
-
-
- - - ); -} - -// ─── PolicyOtpEmailSection ──────────────────────────────────────────────────── - -type PolicyOtpEmailSectionProps = { - form: UseFormReturn; - emailEnabled: boolean; -}; - -export function PolicyOtpEmailSection({ - form, - emailEnabled -}: PolicyOtpEmailSectionProps) { - const t = useTranslations(); - const [isExpanded, setIsExpanded] = useState(false); - const [whitelistEnabled, setWhitelistEnabled] = useState(false); - const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< - number | null - >(null); - - if (!isExpanded) { - return ( - - - - {t("otpEmailTitle")} - - - {t("otpEmailTitleDescription")} - - - - - - - ); - } - - return ( - - - - {t("otpEmailTitle")} - - - {t("otpEmailTitleDescription")} - - - - - {!emailEnabled && ( - - - - {t("otpEmailSmtpRequired")} - - - {t("otpEmailSmtpRequiredDescription")} - - - )} - { - setWhitelistEnabled(val); - form.setValue("emailWhitelistEnabled", val); - }} - disabled={!emailEnabled} - /> - - {whitelistEnabled && emailEnabled && ( - ( - - - - - - {/* @ts-ignore */} - { - return z - .email() - .or( - z - .string() - .regex( - /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, - { - message: t( - "otpEmailErrorInvalid" - ) - } - ) - ) - .safeParse(tag).success; - }} - setActiveTagIndex={ - setActiveEmailTagIndex - } - placeholder={t("otpEmailEnter")} - tags={form.getValues().emails} - setTags={(newEmails) => { - form.setValue( - "emails", - newEmails as [Tag, ...Tag[]] - ); - }} - allowDuplicates={false} - sortTags={true} - /> - - - {t("otpEmailEnterDescription")} - - - )} - /> - )} - - - - ); -} - -// ─── PolicyRulesSection ─────────────────────────────────────────────────────── - -const addRuleSchema = z.object({ - action: z.enum(["ACCEPT", "DROP", "PASS"]), - match: z.string(), - value: z.string(), - priority: z.coerce.number().int().optional() -}); - -type LocalRule = { - ruleId: number; - action: "ACCEPT" | "DROP" | "PASS"; - match: string; - value: string; - priority: number; - enabled: boolean; - new?: boolean; - updated?: boolean; -}; - -type PolicyRulesSectionProps = { - form: UseFormReturn; - isMaxmindAvailable: boolean; - isMaxmindAsnAvailable: boolean; -}; - -export function PolicyRulesSection({ - form, - isMaxmindAvailable, - isMaxmindAsnAvailable -}: PolicyRulesSectionProps) { - const t = useTranslations(); - const [isExpanded, setIsExpanded] = useState(false); - const [rules, setRules] = useState([]); - const [rulesEnabled, setRulesEnabled] = useState(false); - const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = - useState(false); - const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false); - - const addRuleForm = useForm({ - resolver: zodResolver(addRuleSchema), - defaultValues: { - action: "ACCEPT" as const, - match: "IP", - value: "" - } - }); - - const RuleAction = useMemo( - () => ({ - ACCEPT: t("alwaysAllow"), - DROP: t("alwaysDeny"), - PASS: t("passToAuth") - }), - [t] - ); - - const RuleMatch = useMemo( - () => ({ - PATH: t("path"), - IP: "IP", - CIDR: t("ipAddressRange"), - COUNTRY: t("country"), - ASN: "ASN" - }), - [t] - ); - - const syncFormRules = useCallback( - (updatedRules: LocalRule[]) => { - form.setValue( - "rules", - updatedRules.map( - ({ action, match, value, priority, enabled }) => ({ - action, - match, - value, - priority, - enabled - }) - ) - ); - }, - [form] - ); - - const addRule = useCallback( - function addRule(data: z.infer) { - const isDuplicate = rules.some( - (rule) => - rule.action === data.action && - rule.match === data.match && - rule.value === data.value - ); - if (isDuplicate) { - toast({ - variant: "destructive", - title: t("rulesErrorDuplicate"), - description: t("rulesErrorDuplicateDescription") - }); - return; - } - if (data.match === "CIDR" && !isValidCIDR(data.value)) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidIpAddressRange"), - description: t("rulesErrorInvalidIpAddressRangeDescription") - }); - return; - } - if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidUrl"), - description: t("rulesErrorInvalidUrlDescription") - }); - return; - } - if (data.match === "IP" && !isValidIP(data.value)) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidIpAddress"), - description: t("rulesErrorInvalidIpAddressDescription") - }); - return; - } - if ( - data.match === "COUNTRY" && - !COUNTRIES.some((c) => c.code === data.value) - ) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidCountry"), - description: t("rulesErrorInvalidCountryDescription") || "" - }); - return; - } - - let priority = data.priority; - if (priority === undefined) { - priority = - rules.reduce( - (acc, rule) => - rule.priority > acc ? rule.priority : acc, - 0 - ) + 1; - } - - const updatedRules = [ - ...rules, - { - ...data, - ruleId: new Date().getTime(), - new: true, - priority, - enabled: true - } - ]; - setRules(updatedRules); - syncFormRules(updatedRules); - addRuleForm.reset(); - }, - [rules, t, addRuleForm, syncFormRules] - ); - - const removeRule = useCallback( - function removeRule(ruleId: number) { - const updatedRules = rules.filter((rule) => rule.ruleId !== ruleId); - setRules(updatedRules); - syncFormRules(updatedRules); - }, - [rules, syncFormRules] - ); - - const updateRule = useCallback( - function updateRule(ruleId: number, data: Partial) { - const updatedRules = rules.map((rule) => - rule.ruleId === ruleId - ? { ...rule, ...data, updated: true } - : rule - ); - setRules(updatedRules); - syncFormRules(updatedRules); - }, - [rules, syncFormRules] - ); - - const getValueHelpText = useCallback( - function getValueHelpText(type: string) { - switch (type) { - case "CIDR": - return t("rulesMatchIpAddressRangeDescription"); - case "IP": - return t("rulesMatchIpAddress"); - case "PATH": - return t("rulesMatchUrl"); - case "COUNTRY": - return t("rulesMatchCountry"); - case "ASN": - return "Enter an Autonomous System Number (e.g., AS15169 or 15169)"; - } - }, - [t] - ); - - const columns: ColumnDef[] = useMemo( - () => [ - { - accessorKey: "priority", - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - e.currentTarget.focus()} - onBlur={(e) => { - const parsed = z.coerce - .number() - .int() - .optional() - .safeParse(e.target.value); - if (!parsed.success) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidPriority"), - description: t( - "rulesErrorInvalidPriorityDescription" - ) - }); - return; - } - updateRule(row.original.ruleId, { - priority: parsed.data - }); - }} - /> - ) - }, - { - accessorKey: "action", - header: () => {t("rulesAction")}, - cell: ({ row }) => ( - - ) - }, - { - accessorKey: "match", - header: () => ( - {t("rulesMatchType")} - ), - cell: ({ row }) => ( - - ) - }, - { - accessorKey: "value", - header: () => {t("value")}, - cell: ({ row }) => - row.original.match === "COUNTRY" ? ( - - - - - - - - - - {t("noCountryFound")} - - - {COUNTRIES.map((country) => ( - - updateRule( - row.original.ruleId, - { - value: country.code - } - ) - } - > - - {country.name} ( - {country.code}) - - ))} - - - - - - ) : row.original.match === "ASN" ? ( - - - - - - - - - - No ASN found. Enter a custom ASN - below. - - - {MAJOR_ASNS.map((asn) => ( - - updateRule( - row.original.ruleId, - { value: asn.code } - ) - } - > - - {asn.name} ({asn.code}) - - ))} - - - -
- - asn.code === - row.original.value - ) - ? row.original.value - : "" - } - onKeyDown={(e) => { - if (e.key === "Enter") { - const value = - e.currentTarget.value - .toUpperCase() - .replace(/^AS/, ""); - if (/^\d+$/.test(value)) { - updateRule( - row.original.ruleId, - { value: "AS" + value } - ); - } - } - }} - className="text-sm" - /> -
-
-
- ) : ( - - updateRule(row.original.ruleId, { - value: e.target.value - }) - } - /> - ) - }, - { - accessorKey: "enabled", - header: () => {t("enabled")}, - cell: ({ row }) => ( - - updateRule(row.original.ruleId, { enabled: val }) - } - /> - ) - }, - { - id: "actions", - header: () => {t("actions")}, - cell: ({ row }) => ( -
- -
- ) - } - ], - [ - t, - RuleAction, - RuleMatch, - isMaxmindAvailable, - isMaxmindAsnAvailable, - updateRule, - removeRule - ] - ); - - const table = useReactTable({ - data: rules, - columns, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - state: { pagination: { pageIndex: 0, pageSize: 1000 } } - }); - - if (!isExpanded) { - return ( - - - - {t("rulesResource")} - - - {t("rulesResourcePolicyDescription")} - - - - - - - ); - } - - return ( - - - - {t("rulesResource")} - - - {t("rulesResourceDescription")} - - - -
-
- { - setRulesEnabled(val); - form.setValue("applyRules", val); - }} - /> -
- -
- -
- ( - - - {t("rulesAction")} - - - - - - - )} - /> - ( - - - {t("rulesMatchType")} - - - - - - - )} - /> - ( - - - - {addRuleForm.watch("match") === - "COUNTRY" ? ( - - - - - - - - - - {t( - "noCountryFound" - )} - - - {COUNTRIES.map( - ( - country - ) => ( - { - field.onChange( - country.code - ); - setOpenAddRuleCountrySelect( - false - ); - }} - > - - { - country.name - }{" "} - ( - { - country.code - } - - ) - - ) - )} - - - - - - ) : addRuleForm.watch( - "match" - ) === "ASN" ? ( - - - - - - - - - - No ASN - found. - Use the - custom - input - below. - - - {MAJOR_ASNS.map( - ( - asn - ) => ( - { - field.onChange( - asn.code - ); - setOpenAddRuleAsnSelect( - false - ); - }} - > - - { - asn.name - }{" "} - ( - { - asn.code - } - - ) - - ) - )} - - - -
- { - if ( - e.key === - "Enter" - ) { - const value = - e.currentTarget.value - .toUpperCase() - .replace( - /^AS/, - "" - ); - if ( - /^\d+$/.test( - value - ) - ) { - field.onChange( - "AS" + - value - ); - setOpenAddRuleAsnSelect( - false - ); - } - } - }} - className="text-sm" - /> -
-
-
- ) : ( - - )} -
- -
- )} - /> - -
-
- - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - const isActionsColumn = - header.column.id === "actions"; - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column - .columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => { - const isActionsColumn = - cell.column.id === "actions"; - return ( - - {flexRender( - cell.column.columnDef - .cell, - cell.getContext() - )} - - ); - })} - - )) - ) : ( - - - {t("rulesNoOne")} - - - )} - -
-
-
-
); } diff --git a/src/components/resource-policy/EditPolicyNameSectionForm.tsx b/src/components/resource-policy/EditPolicyNameSectionForm.tsx new file mode 100644 index 000000000..1d45b9d3e --- /dev/null +++ b/src/components/resource-policy/EditPolicyNameSectionForm.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; + +import { useEnvContext } from "@app/hooks/useEnvContext"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslations } from "next-intl"; + +import z from "zod"; + +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { type ResourcePolicy } from "@server/db"; +import type { AxiosResponse } from "axios"; +import { useRouter } from "next/navigation"; + +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; + +import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; +import { useActionState } from "react"; +import { useForm } from "react-hook-form"; + +// ─── PolicyNameSection ────────────────────────────────────────────────── + +export function EditPolicyNameSectionForm() { + const t = useTranslations(); + const api = createApiClient(useEnvContext()); + const router = useRouter(); + + const { policy } = useResourcePolicyContext(); + + const form = useForm({ + resolver: zodResolver( + z.object({ + name: z.string() + }) + ), + defaultValues: { + name: policy.name + } + }); + + const [, formAction, isSubmitting] = useActionState(onSubmit, null); + + async function onSubmit() { + const isValid = await form.trigger(); + + if (!isValid) return; + + const payload = form.getValues(); + + try { + const res = await api + .put>( + `/resource-policy/${policy.resourcePolicyId}`, + { + name: payload.name + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: formatAxiosError( + e, + t("policyErrorUpdateDescription") + ) + }); + }); + + if (res && res.status === 200) { + toast({ + title: t("success"), + description: t("policyUpdatedSuccess") + }); + router.refresh(); + } + } catch (e) { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: t("policyErrorUpdateMessageDescription") + }); + } + } + + return ( +
+ + + + + {t("resourcePolicyName")} + + + {t("resourcePolicyNameDescription")} + + + + + ( + + {t("name")} + + + + + + )} + /> + + + +
+ +
+
+
+ + ); +} diff --git a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx new file mode 100644 index 000000000..917001158 --- /dev/null +++ b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; + +import { useTranslations } from "next-intl"; + +import z from "zod"; + +import { type PolicyFormValues } from "."; + +import { SwitchInput } from "@app/components/SwitchInput"; +import { Tag, TagInput } from "@app/components/tags/tag-input"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { Button } from "@app/components/ui/button"; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel +} from "@app/components/ui/form"; +import { InfoPopup } from "@app/components/ui/info-popup"; + +import { InfoIcon, Plus } from "lucide-react"; + +import { useState } from "react"; +import { UseFormReturn } from "react-hook-form"; + +// ─── PolicyOtpEmailSection ──────────────────────────────────────────────────── + +type PolicyOtpEmailSectionProps = { + form: UseFormReturn; + emailEnabled: boolean; +}; + +export function PolicyOtpEmailSection({ + form, + emailEnabled +}: PolicyOtpEmailSectionProps) { + const t = useTranslations(); + const [isExpanded, setIsExpanded] = useState(false); + const [whitelistEnabled, setWhitelistEnabled] = useState(false); + const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< + number | null + >(null); + + if (!isExpanded) { + return ( + + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + + + ); + } + + return ( + + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + {!emailEnabled && ( + + + + {t("otpEmailSmtpRequired")} + + + {t("otpEmailSmtpRequiredDescription")} + + + )} + { + setWhitelistEnabled(val); + form.setValue("emailWhitelistEnabled", val); + }} + disabled={!emailEnabled} + /> + + {whitelistEnabled && emailEnabled && ( + ( + + + + + + {/* @ts-ignore */} + { + return z + .email() + .or( + z + .string() + .regex( + /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, + { + message: t( + "otpEmailErrorInvalid" + ) + } + ) + ) + .safeParse(tag).success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder={t("otpEmailEnter")} + tags={form.getValues().emails} + setTags={(newEmails) => { + form.setValue( + "emails", + newEmails as [Tag, ...Tag[]] + ); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t("otpEmailEnterDescription")} + + + )} + /> + )} + + + + ); +} diff --git a/src/components/resource-policy/EditPolicyRulesSectionForm.tsx b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx new file mode 100644 index 000000000..692cbf463 --- /dev/null +++ b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx @@ -0,0 +1,1113 @@ +"use client"; + +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; + +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; + +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; +import { orgQueries } from "@app/lib/queries"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { build } from "@server/build"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { UserType } from "@server/types/UserTypes"; +import { useQuery } from "@tanstack/react-query"; +import { useTranslations } from "next-intl"; + +import z from "zod"; + +import { type PolicyFormValues, createPolicySchema } from "."; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { orgs, type ResourcePolicy } from "@server/db"; +import type { AxiosResponse } from "axios"; +import { useRouter } from "next/navigation"; + +import { SwitchInput } from "@app/components/SwitchInput"; +import { Tag, TagInput } from "@app/components/tags/tag-input"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { Button } from "@app/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { InfoPopup } from "@app/components/ui/info-popup"; +import { Input } from "@app/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { Switch } from "@app/components/ui/switch"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@app/components/ui/table"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot +} from "@app/components/ui/input-otp"; + +import { MAJOR_ASNS } from "@server/db/asns"; +import { COUNTRIES } from "@server/db/countries"; +import { + isValidCIDR, + isValidIP, + isValidUrlGlobPattern +} from "@server/lib/validators"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable +} from "@tanstack/react-table"; +import { + ArrowUpDown, + Binary, + Bot, + Check, + ChevronsUpDown, + InfoIcon, + Key, + Plus +} from "lucide-react"; + +import { useCallback, useMemo, useState, useActionState } from "react"; +import { UseFormReturn, useForm, useWatch } from "react-hook-form"; +import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; +import { cn } from "@app/lib/cn"; + +// ─── PolicyRulesSection ─────────────────────────────────────────────────────── + +const addRuleSchema = z.object({ + action: z.enum(["ACCEPT", "DROP", "PASS"]), + match: z.string(), + value: z.string(), + priority: z.coerce.number().int().optional() +}); + +type LocalRule = { + ruleId: number; + action: "ACCEPT" | "DROP" | "PASS"; + match: string; + value: string; + priority: number; + enabled: boolean; + new?: boolean; + updated?: boolean; +}; + +type PolicyRulesSectionProps = { + form: UseFormReturn; + isMaxmindAvailable: boolean; + isMaxmindAsnAvailable: boolean; +}; + +export function PolicyRulesSection({ + form, + isMaxmindAvailable, + isMaxmindAsnAvailable +}: PolicyRulesSectionProps) { + const t = useTranslations(); + const [isExpanded, setIsExpanded] = useState(false); + const [rules, setRules] = useState([]); + const [rulesEnabled, setRulesEnabled] = useState(false); + const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = + useState(false); + const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false); + + const addRuleForm = useForm({ + resolver: zodResolver(addRuleSchema), + defaultValues: { + action: "ACCEPT" as const, + match: "IP", + value: "" + } + }); + + const RuleAction = useMemo( + () => ({ + ACCEPT: t("alwaysAllow"), + DROP: t("alwaysDeny"), + PASS: t("passToAuth") + }), + [t] + ); + + const RuleMatch = useMemo( + () => ({ + PATH: t("path"), + IP: "IP", + CIDR: t("ipAddressRange"), + COUNTRY: t("country"), + ASN: "ASN" + }), + [t] + ); + + const syncFormRules = useCallback( + (updatedRules: LocalRule[]) => { + form.setValue( + "rules", + updatedRules.map( + ({ action, match, value, priority, enabled }) => ({ + action, + match, + value, + priority, + enabled + }) + ) + ); + }, + [form] + ); + + const addRule = useCallback( + function addRule(data: z.infer) { + const isDuplicate = rules.some( + (rule) => + rule.action === data.action && + rule.match === data.match && + rule.value === data.value + ); + if (isDuplicate) { + toast({ + variant: "destructive", + title: t("rulesErrorDuplicate"), + description: t("rulesErrorDuplicateDescription") + }); + return; + } + if (data.match === "CIDR" && !isValidCIDR(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidIpAddressRange"), + description: t("rulesErrorInvalidIpAddressRangeDescription") + }); + return; + } + if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidUrl"), + description: t("rulesErrorInvalidUrlDescription") + }); + return; + } + if (data.match === "IP" && !isValidIP(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidIpAddress"), + description: t("rulesErrorInvalidIpAddressDescription") + }); + return; + } + if ( + data.match === "COUNTRY" && + !COUNTRIES.some((c) => c.code === data.value) + ) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidCountry"), + description: t("rulesErrorInvalidCountryDescription") || "" + }); + return; + } + + let priority = data.priority; + if (priority === undefined) { + priority = + rules.reduce( + (acc, rule) => + rule.priority > acc ? rule.priority : acc, + 0 + ) + 1; + } + + const updatedRules = [ + ...rules, + { + ...data, + ruleId: new Date().getTime(), + new: true, + priority, + enabled: true + } + ]; + setRules(updatedRules); + syncFormRules(updatedRules); + addRuleForm.reset(); + }, + [rules, t, addRuleForm, syncFormRules] + ); + + const removeRule = useCallback( + function removeRule(ruleId: number) { + const updatedRules = rules.filter((rule) => rule.ruleId !== ruleId); + setRules(updatedRules); + syncFormRules(updatedRules); + }, + [rules, syncFormRules] + ); + + const updateRule = useCallback( + function updateRule(ruleId: number, data: Partial) { + const updatedRules = rules.map((rule) => + rule.ruleId === ruleId + ? { ...rule, ...data, updated: true } + : rule + ); + setRules(updatedRules); + syncFormRules(updatedRules); + }, + [rules, syncFormRules] + ); + + const getValueHelpText = useCallback( + function getValueHelpText(type: string) { + switch (type) { + case "CIDR": + return t("rulesMatchIpAddressRangeDescription"); + case "IP": + return t("rulesMatchIpAddress"); + case "PATH": + return t("rulesMatchUrl"); + case "COUNTRY": + return t("rulesMatchCountry"); + case "ASN": + return "Enter an Autonomous System Number (e.g., AS15169 or 15169)"; + } + }, + [t] + ); + + const columns: ColumnDef[] = useMemo( + () => [ + { + accessorKey: "priority", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + e.currentTarget.focus()} + onBlur={(e) => { + const parsed = z.coerce + .number() + .int() + .optional() + .safeParse(e.target.value); + if (!parsed.success) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidPriority"), + description: t( + "rulesErrorInvalidPriorityDescription" + ) + }); + return; + } + updateRule(row.original.ruleId, { + priority: parsed.data + }); + }} + /> + ) + }, + { + accessorKey: "action", + header: () => {t("rulesAction")}, + cell: ({ row }) => ( + + ) + }, + { + accessorKey: "match", + header: () => ( + {t("rulesMatchType")} + ), + cell: ({ row }) => ( + + ) + }, + { + accessorKey: "value", + header: () => {t("value")}, + cell: ({ row }) => + row.original.match === "COUNTRY" ? ( + + + + + + + + + + {t("noCountryFound")} + + + {COUNTRIES.map((country) => ( + + updateRule( + row.original.ruleId, + { + value: country.code + } + ) + } + > + + {country.name} ( + {country.code}) + + ))} + + + + + + ) : row.original.match === "ASN" ? ( + + + + + + + + + + No ASN found. Enter a custom ASN + below. + + + {MAJOR_ASNS.map((asn) => ( + + updateRule( + row.original.ruleId, + { value: asn.code } + ) + } + > + + {asn.name} ({asn.code}) + + ))} + + + +
+ + asn.code === + row.original.value + ) + ? row.original.value + : "" + } + onKeyDown={(e) => { + if (e.key === "Enter") { + const value = + e.currentTarget.value + .toUpperCase() + .replace(/^AS/, ""); + if (/^\d+$/.test(value)) { + updateRule( + row.original.ruleId, + { value: "AS" + value } + ); + } + } + }} + className="text-sm" + /> +
+
+
+ ) : ( + + updateRule(row.original.ruleId, { + value: e.target.value + }) + } + /> + ) + }, + { + accessorKey: "enabled", + header: () => {t("enabled")}, + cell: ({ row }) => ( + + updateRule(row.original.ruleId, { enabled: val }) + } + /> + ) + }, + { + id: "actions", + header: () => {t("actions")}, + cell: ({ row }) => ( +
+ +
+ ) + } + ], + [ + t, + RuleAction, + RuleMatch, + isMaxmindAvailable, + isMaxmindAsnAvailable, + updateRule, + removeRule + ] + ); + + const table = useReactTable({ + data: rules, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { pagination: { pageIndex: 0, pageSize: 1000 } } + }); + + if (!isExpanded) { + return ( + + + + {t("rulesResource")} + + + {t("rulesResourcePolicyDescription")} + + + + + + + ); + } + + return ( + + + + {t("rulesResource")} + + + {t("rulesResourceDescription")} + + + +
+
+ { + setRulesEnabled(val); + form.setValue("applyRules", val); + }} + /> +
+ +
+ +
+ ( + + + {t("rulesAction")} + + + + + + + )} + /> + ( + + + {t("rulesMatchType")} + + + + + + + )} + /> + ( + + + + {addRuleForm.watch("match") === + "COUNTRY" ? ( + + + + + + + + + + {t( + "noCountryFound" + )} + + + {COUNTRIES.map( + ( + country + ) => ( + { + field.onChange( + country.code + ); + setOpenAddRuleCountrySelect( + false + ); + }} + > + + { + country.name + }{" "} + ( + { + country.code + } + + ) + + ) + )} + + + + + + ) : addRuleForm.watch( + "match" + ) === "ASN" ? ( + + + + + + + + + + No ASN + found. + Use the + custom + input + below. + + + {MAJOR_ASNS.map( + ( + asn + ) => ( + { + field.onChange( + asn.code + ); + setOpenAddRuleAsnSelect( + false + ); + }} + > + + { + asn.name + }{" "} + ( + { + asn.code + } + + ) + + ) + )} + + + +
+ { + if ( + e.key === + "Enter" + ) { + const value = + e.currentTarget.value + .toUpperCase() + .replace( + /^AS/, + "" + ); + if ( + /^\d+$/.test( + value + ) + ) { + field.onChange( + "AS" + + value + ); + setOpenAddRuleAsnSelect( + false + ); + } + } + }} + className="text-sm" + /> +
+
+
+ ) : ( + + )} +
+ +
+ )} + /> + +
+
+ + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const isActionsColumn = + header.column.id === "actions"; + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column + .columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const isActionsColumn = + cell.column.id === "actions"; + return ( + + {flexRender( + cell.column.columnDef + .cell, + cell.getContext() + )} + + ); + })} + + )) + ) : ( + + + {t("rulesNoOne")} + + + )} + +
+
+
+
+ ); +} diff --git a/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx new file mode 100644 index 000000000..19c5bb719 --- /dev/null +++ b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx @@ -0,0 +1,358 @@ +"use client"; + +import { + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; + +import { useEnvContext } from "@app/hooks/useEnvContext"; + +import { getUserDisplayName } from "@app/lib/getUserDisplayName"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { UserType } from "@server/types/UserTypes"; +import { useTranslations } from "next-intl"; + +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import type { AxiosResponse } from "axios"; +import { useRouter } from "next/navigation"; +import { createPolicySchema } from "."; + +import { SwitchInput } from "@app/components/SwitchInput"; +import { Tag, TagInput } from "@app/components/tags/tag-input"; +import { Button } from "@app/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; + +import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; +import { useActionState, useState } from "react"; +import { useForm, useWatch } from "react-hook-form"; + +// ─── PolicyUsersRolesSection ────────────────────────────────────────────────── + +type PolicyUsersRolesSectionProps = { + allRoles: { id: string; text: string }[]; + allUsers: { id: string; text: string }[]; + allIdps: { id: number; text: string }[]; +}; + +export function EditPolicyUsersRolesSectionForm({ + allRoles, + allUsers, + allIdps +}: PolicyUsersRolesSectionProps) { + const t = useTranslations(); + + const router = useRouter(); + + const { policy } = useResourcePolicyContext(); + + const api = createApiClient(useEnvContext()); + + const form = useForm({ + resolver: zodResolver( + createPolicySchema.pick({ + sso: true, + skipToIdpId: true, + users: true, + roles: true + }) + ), + defaultValues: { + sso: policy.sso, + skipToIdpId: policy.idpId, + roles: policy.roles.map((role) => ({ + id: role.roleId.toString(), + text: role.name + })), + users: policy.users.map((user) => ({ + id: user.userId, + text: `${getUserDisplayName({ email: user.email, username: user.username })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` + })) + } + }); + + const ssoEnabled = useWatch({ control: form.control, name: "sso" }); + const selectedIdpId = useWatch({ + control: form.control, + name: "skipToIdpId" + }); + const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< + number | null + >(null); + const [activeUsersTagIndex, setActiveUsersTagIndex] = useState< + number | null + >(null); + + const [, formAction, isSubmitting] = useActionState(onSubmit, null); + + async function onSubmit() { + const isValid = await form.trigger(); + + if (!isValid) return; + + const payload = form.getValues(); + + try { + const res = await api + .put>( + `/resource-policy/${policy.resourcePolicyId}/access-control`, + { + sso: payload.sso, + userIds: payload.users.map((user) => user.id), + roleIds: payload.roles.map((role) => Number(role.id)), + skipToIdpId: payload.skipToIdpId + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: formatAxiosError( + e, + t("policyErrorUpdateDescription") + ) + }); + }); + + if (res && res.status === 200) { + toast({ + title: t("success"), + description: t("policyUpdatedSuccess") + }); + router.refresh(); + } + } catch (e) { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: t("policyErrorUpdateMessageDescription") + }); + } + } + + return ( +
+ + + + + {t("resourceUsersRoles")} + + + {t("resourcePolicyUsersRolesDescription")} + + + + + { + console.log(`form.setValue("sso", ${val})`); + form.setValue("sso", val); + }} + /> + + {ssoEnabled && ( + <> + ( + + + {t("roles")} + + + { + form.setValue( + "roles", + newRoles as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allRoles + } + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + {t( + "resourceRoleDescription" + )} + + + )} + /> + ( + + + {t("users")} + + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allUsers + } + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + )} + /> + + )} + + {ssoEnabled && allIdps.length > 0 && ( +
+ + +

+ {t( + "defaultIdentityProviderDescription" + )} +

+
+ )} +
+
+ +
+ +
+
+
+ + ); +} From 1dc8be373c6f8e35b314175d491e22c0223a24a5 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 3 Mar 2026 18:54:35 +0100 Subject: [PATCH 043/771] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20add=20password?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditPolicyAuthMethodsSectionForm.tsx | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx b/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx index 25db214e7..99d01930a 100644 --- a/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx @@ -16,7 +16,7 @@ import { useTranslations } from "next-intl"; import z from "zod"; -import { createApiClient } from "@app/lib/api"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useRouter } from "next/navigation"; import { createPolicySchema } from "."; @@ -53,6 +53,8 @@ import { cn } from "@app/lib/cn"; import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; import { useActionState, useState } from "react"; import { useForm } from "react-hook-form"; +import { toast } from "@app/hooks/useToast"; +import type { AxiosResponse } from "axios"; // ─── PolicyAuthMethodsSection ───────────────────────────────────────────────── @@ -87,7 +89,6 @@ export function EditPolicyAuthMethodsSectionForm() { }); const t = useTranslations(); - const [isExpanded, setIsExpanded] = useState(false); const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); @@ -98,6 +99,10 @@ export function EditPolicyAuthMethodsSectionForm() { form.watch("headerAuth") ?? policy.headerAuth ); + const [isExpanded, setIsExpanded] = useState( + hasPassword || hasPincode || hasHeaderAuth + ); + const passwordForm = useForm({ resolver: zodResolver(setPasswordSchema), defaultValues: { password: "" } @@ -121,7 +126,43 @@ export function EditPolicyAuthMethodsSectionForm() { if (!isValid) return; const payload = form.getValues(); - console.log({ payload }); + console.log({ payload, policy }); + + return; + + try { + const res = await api + .put>( + `/resource-policy/${policy.resourcePolicyId}/password`, + { + password: payload.password?.password ?? null + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: formatAxiosError( + e, + t("policyErrorUpdateDescription") + ) + }); + }); + + if (res && res.status === 200) { + toast({ + title: t("success"), + description: t("policyUpdatedSuccess") + }); + router.refresh(); + } + } catch (e) { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: t("policyErrorUpdateMessageDescription") + }); + } } if (!isExpanded) { @@ -407,7 +448,7 @@ export function EditPolicyAuthMethodsSectionForm() {
- {}}> + From 20b65f549e5b160312e4a2265fc02591f4976dce Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 3 Mar 2026 19:49:24 +0100 Subject: [PATCH 044/771] =?UTF-8?q?=E2=9C=A8=20Update=20resource=20policy?= =?UTF-8?q?=20pincode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../policy/setResourcePolicyHeaderAuth.ts | 20 ++-- .../EditPolicyAuthMethodsSectionForm.tsx | 108 ++++++++++++++---- 2 files changed, 97 insertions(+), 31 deletions(-) diff --git a/server/routers/policy/setResourcePolicyHeaderAuth.ts b/server/routers/policy/setResourcePolicyHeaderAuth.ts index 34c55b750..6291ee24e 100644 --- a/server/routers/policy/setResourcePolicyHeaderAuth.ts +++ b/server/routers/policy/setResourcePolicyHeaderAuth.ts @@ -15,9 +15,13 @@ const setResourcePolicyHeaderAuthParamsSchema = z.object({ }); const setResourcePolicyHeaderAuthBodySchema = z.strictObject({ - user: z.string().min(4).max(100).nullable(), - password: z.string().min(4).max(100).nullable(), - extendedCompatibility: z.boolean().nullable() + headerAuth: z + .object({ + user: z.string().min(4).max(100), + password: z.string().min(4).max(100), + extendedCompatibility: z.boolean() + }) + .nullable() }); registry.registerPath({ @@ -70,7 +74,7 @@ export async function setResourcePolicyHeaderAuth( } const { resourcePolicyId } = parsedParams.data; - const { user, password, extendedCompatibility } = parsedBody.data; + const { headerAuth } = parsedBody.data; await db.transaction(async (trx) => { await trx @@ -82,15 +86,17 @@ export async function setResourcePolicyHeaderAuth( ) ); - if (user && password && extendedCompatibility !== null) { + if (headerAuth !== null) { const headerAuthHash = await hashPassword( - Buffer.from(`${user}:${password}`).toString("base64") + Buffer.from( + `${headerAuth.user}:${headerAuth.password}` + ).toString("base64") ); await trx.insert(resourcePolicyHeaderAuth).values({ resourcePolicyId, headerAuthHash, - extendedCompatibility: extendedCompatibility + extendedCompatibility: headerAuth.extendedCompatibility }); } }); diff --git a/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx b/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx index 99d01930a..957d60978 100644 --- a/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx @@ -93,11 +93,21 @@ export function EditPolicyAuthMethodsSectionForm() { const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); - const hasPassword = Boolean(form.watch("password") ?? policy.passwordId); - const hasPincode = Boolean(form.watch("pincode") ?? policy.pincodeId); - const hasHeaderAuth = Boolean( - form.watch("headerAuth") ?? policy.headerAuth - ); + const password = form.watch("password"); + const pincode = form.watch("pincode"); + const headerAuth = form.watch("headerAuth"); + + // If explicitly removed (set to `null`) it means the value has been removed + // in the other case (`undefined` or object value), check if the value has been modified + // and fallback to the policy default value + const hasPassword = + password !== null ? Boolean(password ?? policy.passwordId) : false; + + const hasPincode = + pincode !== null ? Boolean(pincode ?? policy.pincodeId) : false; + + const hasHeaderAuth = + headerAuth !== null ? Boolean(headerAuth ?? policy.headerAuth) : false; const [isExpanded, setIsExpanded] = useState( hasPassword || hasPincode || hasHeaderAuth @@ -128,28 +138,78 @@ export function EditPolicyAuthMethodsSectionForm() { const payload = form.getValues(); console.log({ payload, policy }); - return; + const responseArray: Array | void>> = []; + + if (typeof payload.password !== "undefined") { + responseArray.push( + api + .put>( + `/resource-policy/${policy.resourcePolicyId}/password`, + { + password: payload.password?.password ?? null + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: formatAxiosError( + e, + t("policyErrorUpdateDescription") + ) + }); + }) + ); + } + + if (typeof payload.pincode !== "undefined") { + responseArray.push( + api + .put>( + `/resource-policy/${policy.resourcePolicyId}/pincode`, + { + pincode: payload.pincode?.pincode ?? null + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: formatAxiosError( + e, + t("policyErrorUpdateDescription") + ) + }); + }) + ); + } + + if (typeof payload.headerAuth !== "undefined") { + responseArray.push( + api + .put>( + `/resource-policy/${policy.resourcePolicyId}/header-auth`, + { + headerAuth: payload.headerAuth + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: formatAxiosError( + e, + t("policyErrorUpdateDescription") + ) + }); + }) + ); + } try { - const res = await api - .put>( - `/resource-policy/${policy.resourcePolicyId}/password`, - { - password: payload.password?.password ?? null - } - ) - .catch((e) => { - toast({ - variant: "destructive", - title: t("policyErrorUpdate"), - description: formatAxiosError( - e, - t("policyErrorUpdateDescription") - ) - }); - }); + const responseList = await Promise.all(responseArray); - if (res && res.status === 200) { + if (responseList.every((res) => res && res.status === 200)) { toast({ title: t("success"), description: t("policyUpdatedSuccess") From be2b1fd1ce55182054ca728ef2af337c0165b216 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 3 Mar 2026 20:26:17 +0100 Subject: [PATCH 045/771] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20email=20whitelist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.mail.yml | 13 + server/db/pg/schema/schema.ts | 22 +- server/routers/policy/getResourcePolicy.ts | 15 +- .../EditPolicyAuthMethodsSectionForm.tsx | 6 +- .../resource-policy/EditPolicyForm.tsx | 9 +- .../EditPolicyNameSectionForm.tsx | 5 +- .../EditPolicyOtpEmailSectionForm.tsx | 270 +++++++++++------- .../EditPolicyUserRolesSectionForm.tsx | 5 +- 8 files changed, 220 insertions(+), 125 deletions(-) create mode 100644 docker-compose.mail.yml diff --git a/docker-compose.mail.yml b/docker-compose.mail.yml new file mode 100644 index 000000000..49aaead9f --- /dev/null +++ b/docker-compose.mail.yml @@ -0,0 +1,13 @@ +services: + mailer: + image: axllent/mailpit + ports: + - 8025:8025 + - 1025:1025 + volumes: + - mailpit-storage:/data + environment: + - MP_DATABASE=/data/mailpit.db + +volumes: + mailpit-storage: diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 935fab7c1..80fa24ac9 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -469,6 +469,16 @@ export const userPolicies = pgTable("userPolicies", { }) }); +export const resourcePolicyWhiteList = pgTable("resourcePolicyWhitelist", { + whitelistId: serial("id").primaryKey(), + email: varchar("email").notNull(), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { + onDelete: "cascade" + }) +}); + export const userInvites = pgTable("userInvites", { inviteId: varchar("inviteId").primaryKey(), orgId: varchar("orgId") @@ -621,12 +631,7 @@ export const resourceWhitelist = pgTable("resourceWhitelist", { email: varchar("email").notNull(), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { - onDelete: "cascade" - }) + .references(() => resources.resourceId, { onDelete: "cascade" }) }); export const resourceOtp = pgTable("resourceOtp", { @@ -634,11 +639,6 @@ export const resourceOtp = pgTable("resourceOtp", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { - onDelete: "cascade" - }), email: varchar("email").notNull(), otpHash: varchar("otpHash").notNull(), expiresAt: bigint("expiresAt", { mode: "number" }).notNull() diff --git a/server/routers/policy/getResourcePolicy.ts b/server/routers/policy/getResourcePolicy.ts index cba2a9dba..124f67718 100644 --- a/server/routers/policy/getResourcePolicy.ts +++ b/server/routers/policy/getResourcePolicy.ts @@ -5,6 +5,7 @@ import { resourcePolicyHeaderAuth, resourcePolicyPassword, resourcePolicyPincode, + resourcePolicyWhiteList, rolePolicies, roles, userPolicies, @@ -116,7 +117,19 @@ async function query(params: z.infer) { ) .where(eq(rolePolicies.resourcePolicyId, res.resourcePolicyId)); - return { ...res, roles: policyRoles, users: policyUsers }; + const policyEmailWhiteList = await db + .select() + .from(resourcePolicyWhiteList) + .where( + eq(resourcePolicyWhiteList.resourcePolicyId, res.resourcePolicyId) + ); + + return { + ...res, + roles: policyRoles, + users: policyUsers, + emailWhiteList: policyEmailWhiteList + }; } export type GetResourcePolicyResponse = NonNullable< diff --git a/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx b/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx index 957d60978..8a7efce1d 100644 --- a/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx @@ -4,6 +4,7 @@ import { SettingsSection, SettingsSectionBody, SettingsSectionDescription, + SettingsSectionFooter, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle @@ -136,7 +137,6 @@ export function EditPolicyAuthMethodsSectionForm() { if (!isValid) return; const payload = form.getValues(); - console.log({ payload, policy }); const responseArray: Array | void>> = []; @@ -640,7 +640,7 @@ export function EditPolicyAuthMethodsSectionForm() { -
+ -
+
diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index 8b0376107..4647c84f1 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -21,6 +21,7 @@ import { useMemo } from "react"; import { EditPolicyAuthMethodsSectionForm } from "./EditPolicyAuthMethodsSectionForm"; import { EditPolicyNameSectionForm } from "./EditPolicyNameSectionForm"; import { EditPolicyUsersRolesSectionForm } from "./EditPolicyUserRolesSectionForm"; +import { EditPolicyOtpEmailSectionForm } from "./EditPolicyOtpEmailSectionForm"; // ─── EditPolicyForm ───────────────────────────────────────────────────────── @@ -107,11 +108,11 @@ export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) { /> + + {/* - -
+ -
+ diff --git a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx index 917001158..93cb2b295 100644 --- a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx @@ -4,6 +4,7 @@ import { SettingsSection, SettingsSectionBody, SettingsSectionDescription, + SettingsSectionFooter, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle @@ -13,13 +14,14 @@ import { useTranslations } from "next-intl"; import z from "zod"; -import { type PolicyFormValues } from "."; +import { createPolicySchema, type PolicyFormValues } from "."; import { SwitchInput } from "@app/components/SwitchInput"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Button } from "@app/components/ui/button"; import { + Form, FormControl, FormDescription, FormField, @@ -30,27 +32,64 @@ import { InfoPopup } from "@app/components/ui/info-popup"; import { InfoIcon, Plus } from "lucide-react"; -import { useState } from "react"; -import { UseFormReturn } from "react-hook-form"; +import { useActionState, useState } from "react"; +import { useForm, UseFormReturn, useWatch } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/navigation"; +import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; // ─── PolicyOtpEmailSection ──────────────────────────────────────────────────── type PolicyOtpEmailSectionProps = { - form: UseFormReturn; emailEnabled: boolean; }; -export function PolicyOtpEmailSection({ - form, +export function EditPolicyOtpEmailSectionForm({ emailEnabled }: PolicyOtpEmailSectionProps) { const t = useTranslations(); - const [isExpanded, setIsExpanded] = useState(false); - const [whitelistEnabled, setWhitelistEnabled] = useState(false); + + const { policy } = useResourcePolicyContext(); + const router = useRouter(); + + const form = useForm({ + resolver: zodResolver( + createPolicySchema.pick({ + emailWhitelistEnabled: true, + emails: true + }) + ), + defaultValues: { + emailWhitelistEnabled: policy.emailWhitelistEnabled, + emails: policy.emailWhiteList.map((email) => ({ + id: email.whitelistId.toString(), + text: email.email + })) + } + }); + + const whitelistEnabled = useWatch({ + control: form.control, + name: "emailWhitelistEnabled" + }); + + const [isExpanded, setIsExpanded] = useState(whitelistEnabled); const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< number | null >(null); + const [, formAction, isSubmitting] = useActionState(onSubmit, null); + + async function onSubmit() { + const isValid = await form.trigger(); + + if (!isValid) return; + + const payload = form.getValues(); + + console.log({ payload, policy }); + } + if (!isExpanded) { return ( @@ -77,100 +116,127 @@ export function PolicyOtpEmailSection({ } return ( - - - - {t("otpEmailTitle")} - - - {t("otpEmailTitleDescription")} - - - - - {!emailEnabled && ( - - - - {t("otpEmailSmtpRequired")} - - - {t("otpEmailSmtpRequiredDescription")} - - - )} - { - setWhitelistEnabled(val); - form.setValue("emailWhitelistEnabled", val); - }} - disabled={!emailEnabled} - /> - - {whitelistEnabled && emailEnabled && ( - ( - - - - - - {/* @ts-ignore */} - { - return z - .email() - .or( - z - .string() - .regex( - /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, - { - message: t( - "otpEmailErrorInvalid" - ) - } - ) - ) - .safeParse(tag).success; - }} - setActiveTagIndex={ - setActiveEmailTagIndex - } - placeholder={t("otpEmailEnter")} - tags={form.getValues().emails} - setTags={(newEmails) => { - form.setValue( - "emails", - newEmails as [Tag, ...Tag[]] - ); - }} - allowDuplicates={false} - sortTags={true} - /> - - - {t("otpEmailEnterDescription")} - - +
+ + + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + {!emailEnabled && ( + + + + {t("otpEmailSmtpRequired")} + + + {t("otpEmailSmtpRequiredDescription")} + + )} - /> - )} - - - + { + form.setValue("emailWhitelistEnabled", val); + }} + disabled={!emailEnabled} + /> + + {whitelistEnabled && emailEnabled && ( + ( + + + + + + {/* @ts-ignore */} + { + return z + .email() + .or( + z + .string() + .regex( + /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, + { + message: + t( + "otpEmailErrorInvalid" + ) + } + ) + ) + .safeParse(tag) + .success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder={t( + "otpEmailEnter" + )} + tags={ + form.getValues() + .emails ?? [] + } + setTags={(newEmails) => { + form.setValue( + "emails", + newEmails as [ + Tag, + ...Tag[] + ] + ); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t("otpEmailEnterDescription")} + + + )} + /> + )} + + + + + + + + + ); } diff --git a/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx index 19c5bb719..d4d9b2de2 100644 --- a/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx @@ -4,6 +4,7 @@ import { SettingsSection, SettingsSectionBody, SettingsSectionDescription, + SettingsSectionFooter, SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle @@ -342,7 +343,7 @@ export function EditPolicyUsersRolesSectionForm({
-
+ -
+
From a1eb2484748788e3fa1c6b9a7c57141ab3a1c234 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 4 Mar 2026 01:10:48 +0100 Subject: [PATCH 046/771] =?UTF-8?q?=F0=9F=94=A8=20remove=20docker=20compos?= =?UTF-8?q?e=20mail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.mail.yml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 docker-compose.mail.yml diff --git a/docker-compose.mail.yml b/docker-compose.mail.yml deleted file mode 100644 index 49aaead9f..000000000 --- a/docker-compose.mail.yml +++ /dev/null @@ -1,13 +0,0 @@ -services: - mailer: - image: axllent/mailpit - ports: - - 8025:8025 - - 1025:1025 - volumes: - - mailpit-storage:/data - environment: - - MP_DATABASE=/data/mailpit.db - -volumes: - mailpit-storage: From 7f6ca3175734decb138f52685f6e12618ce96097 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 4 Mar 2026 01:46:56 +0100 Subject: [PATCH 047/771] =?UTF-8?q?=F0=9F=9A=A7=20Email=20whiteList=20for?= =?UTF-8?q?=20resource=20policy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/auth/actions.ts | 3 +- server/routers/external.ts | 9 ++ server/routers/integration.ts | 9 ++ server/routers/policy/index.ts | 1 + .../policy/setResourcePolicyWhitelist.ts | 132 ++++++++++++++++++ 5 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 server/routers/policy/setResourcePolicyWhitelist.ts diff --git a/server/auth/actions.ts b/server/auth/actions.ts index b3e6c5f75..5c512181a 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -144,7 +144,8 @@ export enum ActionsEnum { setResourcePolicyUsers = "setResourcePolicyUsers", setResourcePolicyPassword = "setResourcePolicyPassword", setResourcePolicyPincode = "setResourcePolicyPincode", - setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth" + setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth", + setResourcePolicyWhitelist = "setResourcePolicyWhitelist" } export async function checkUserActionPermission( diff --git a/server/routers/external.ts b/server/routers/external.ts index 379ff794c..faee76486 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -761,6 +761,15 @@ authenticated.put( policy.setResourcePolicyHeaderAuth ); +authenticated.put( + "/resource-policy/:resourcePolicyId/whitelist", + verifyResourcePolicyAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.setResourcePolicyWhitelist), + logActionAudit(ActionsEnum.setResourcePolicyWhitelist), + policy.setResourcePolicyWhitelist +); + authenticated.post( `/resource/:resourceId/password`, verifyResourceAccess, diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 52c839b18..89ec2c2d7 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -659,6 +659,15 @@ authenticated.put( policy.setResourcePolicyHeaderAuth ); +authenticated.put( + "/resource-policy/:resourcePolicyId/whitelist", + verifyApiKeyResourcePolicyAccess, + verifyLimits, + verifyApiKeyHasAction(ActionsEnum.setResourcePolicyWhitelist), + logActionAudit(ActionsEnum.setResourcePolicyWhitelist), + policy.setResourcePolicyWhitelist +); + authenticated.post( "/resource/:resourceId/roles/add", verifyApiKeyResourceAccess, diff --git a/server/routers/policy/index.ts b/server/routers/policy/index.ts index 9d191af15..7719ffdfe 100644 --- a/server/routers/policy/index.ts +++ b/server/routers/policy/index.ts @@ -4,3 +4,4 @@ export * from "./setResourcePolicyAccessControl"; export * from "./setResourcePolicyPassword"; export * from "./setResourcePolicyPincode"; export * from "./setResourcePolicyHeaderAuth"; +export * from "./setResourcePolicyWhitelist"; diff --git a/server/routers/policy/setResourcePolicyWhitelist.ts b/server/routers/policy/setResourcePolicyWhitelist.ts new file mode 100644 index 000000000..63fabeaa0 --- /dev/null +++ b/server/routers/policy/setResourcePolicyWhitelist.ts @@ -0,0 +1,132 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, resourcePolicies, resourcePolicyWhiteList } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { and, eq } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; + +const setResourcePolicyWhitelistBodySchema = z.strictObject({ + emailWhitelistEnabled: z.boolean(), + emails: z + .array( + z.email().or( + z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { + error: "Invalid email address. Wildcard (*) must be the entire local part." + }) + ) + ) + .max(50) + .transform((v) => v.map((e) => e.toLowerCase())) +}); + +const setResourcePolicyWhitelistParamsSchema = z.strictObject({ + resourcePolicyId: z.string().transform(Number).pipe(z.int().positive()) +}); + +registry.registerPath({ + method: "put", + path: "/resource-policy/{resourcePolicyId}/whitelist", + description: + "Set email whitelist for a resource policy. This will replace all existing emails.", + tags: [OpenAPITags.Resource], + request: { + params: setResourcePolicyWhitelistParamsSchema, + body: { + content: { + "application/json": { + schema: setResourcePolicyWhitelistBodySchema + } + } + } + }, + responses: {} +}); + +export async function setResourcePolicyWhitelist( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = setResourcePolicyWhitelistBodySchema.safeParse( + req.body + ); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const parsedParams = setResourcePolicyWhitelistParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { resourcePolicyId } = parsedParams.data; + const { emailWhitelistEnabled, emails } = parsedBody.data; + + const [policy] = await db + .select() + .from(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)); + + if (!policy) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Resource policy not found") + ); + } + + await db.transaction(async (trx) => { + await trx + .update(resourcePolicies) + .set({ emailWhitelistEnabled }) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)); + + // delete all whitelist emails + await trx + .delete(resourcePolicyWhiteList) + .where( + eq( + resourcePolicyWhiteList.resourcePolicyId, + resourcePolicyId + ) + ); + + if (emailWhitelistEnabled && emails.length > 0) { + await trx.insert(resourcePolicyWhiteList).values( + emails.map((email) => ({ + email, + resourcePolicyId + })) + ); + } + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Whitelist set for resource policy successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} From e44b15ecd540b127dbab1324b49e800f35ab1319 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 4 Mar 2026 01:54:50 +0100 Subject: [PATCH 048/771] =?UTF-8?q?=E2=9C=A8set=20opt=20email=20whitelist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditPolicyOtpEmailSectionForm.tsx | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx index 93cb2b295..842fc0564 100644 --- a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx @@ -16,6 +16,10 @@ import z from "zod"; import { createPolicySchema, type PolicyFormValues } from "."; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import type { AxiosResponse } from "axios"; import { SwitchInput } from "@app/components/SwitchInput"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; @@ -52,6 +56,8 @@ export function EditPolicyOtpEmailSectionForm({ const { policy } = useResourcePolicyContext(); const router = useRouter(); + const api = createApiClient(useEnvContext()); + const form = useForm({ resolver: zodResolver( createPolicySchema.pick({ @@ -87,7 +93,40 @@ export function EditPolicyOtpEmailSectionForm({ const payload = form.getValues(); - console.log({ payload, policy }); + try { + const res = await api + .put>( + `/resource-policy/${policy.resourcePolicyId}/whitelist`, + { + emailWhitelistEnabled: payload.emailWhitelistEnabled, + emails: payload.emails?.map((e) => e.text) ?? [] + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: formatAxiosError( + e, + t("policyErrorUpdateDescription") + ) + }); + }); + + if (res && res.status === 200) { + toast({ + title: t("success"), + description: t("policyUpdatedSuccess") + }); + router.refresh(); + } + } catch (e) { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: t("policyErrorUpdateMessageDescription") + }); + } } if (!isExpanded) { From cbce9fae3a2206d81f5c163b4a37db28d25964cf Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 4 Mar 2026 16:36:49 +0100 Subject: [PATCH 049/771] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resource-policy/EditPolicyOtpEmailSectionForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx index 842fc0564..c120c13da 100644 --- a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx @@ -182,7 +182,7 @@ export function EditPolicyOtpEmailSectionForm({ { form.setValue("emailWhitelistEnabled", val); }} From f42c013f33359cd8a4da228a14c8c3467608e44c Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 4 Mar 2026 17:41:55 +0100 Subject: [PATCH 050/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + server/routers/policy/getResourcePolicy.ts | 5 +- .../EditPolicyAuthMethodsSectionForm.tsx | 2 +- .../resource-policy/EditPolicyForm.tsx | 11 ++-- .../EditPolicyRulesSectionForm.tsx | 66 ++++--------------- 5 files changed, 25 insertions(+), 60 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 9fa9748cc..1ad1d89be 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -660,6 +660,7 @@ "policyErrorUpdateMessageDescription": "An unexpected error occurred", "policyCreatedSuccess": "Resource policy succesfully created", "policyUpdatedSuccess": "Resource policy succesfully updated", + "authMethodsSave": "Save auth methods", "resourceErrorCreate": "Error creating resource", "resourceErrorCreateDescription": "An error occurred when creating the resource", "resourceErrorCreateMessage": "Error creating resource:", diff --git a/server/routers/policy/getResourcePolicy.ts b/server/routers/policy/getResourcePolicy.ts index 124f67718..02c370199 100644 --- a/server/routers/policy/getResourcePolicy.ts +++ b/server/routers/policy/getResourcePolicy.ts @@ -118,7 +118,10 @@ async function query(params: z.infer) { .where(eq(rolePolicies.resourcePolicyId, res.resourcePolicyId)); const policyEmailWhiteList = await db - .select() + .select({ + whiteListId: resourcePolicyWhiteList.whitelistId, + email: resourcePolicyWhiteList.email + }) .from(resourcePolicyWhiteList) .where( eq(resourcePolicyWhiteList.resourcePolicyId, res.resourcePolicyId) diff --git a/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx b/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx index 8a7efce1d..57f37c958 100644 --- a/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyAuthMethodsSectionForm.tsx @@ -646,7 +646,7 @@ export function EditPolicyAuthMethodsSectionForm() { loading={isSubmitting} disabled={isSubmitting} > - {t("saveSettings")} + {t("authMethodsSave")}
diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index 4647c84f1..e913474a9 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -22,6 +22,7 @@ import { EditPolicyAuthMethodsSectionForm } from "./EditPolicyAuthMethodsSection import { EditPolicyNameSectionForm } from "./EditPolicyNameSectionForm"; import { EditPolicyUsersRolesSectionForm } from "./EditPolicyUserRolesSectionForm"; import { EditPolicyOtpEmailSectionForm } from "./EditPolicyOtpEmailSectionForm"; +import { EditPolicyRulesSectionForm } from "./EditPolicyRulesSectionForm"; // ─── EditPolicyForm ───────────────────────────────────────────────────────── @@ -112,12 +113,10 @@ export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) { emailEnabled={env.email.emailEnabled} /> - {/* - */} + ); } diff --git a/src/components/resource-policy/EditPolicyRulesSectionForm.tsx b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx index 692cbf463..f8f044740 100644 --- a/src/components/resource-policy/EditPolicyRulesSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx @@ -1,40 +1,22 @@ "use client"; import { - SettingsContainer, SettingsSection, SettingsSectionBody, SettingsSectionDescription, - SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useOrgContext } from "@app/hooks/useOrgContext"; -import { usePaidStatus } from "@app/hooks/usePaidStatus"; - -import { getUserDisplayName } from "@app/lib/getUserDisplayName"; -import { orgQueries } from "@app/lib/queries"; import { zodResolver } from "@hookform/resolvers/zod"; -import { build } from "@server/build"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; -import { UserType } from "@server/types/UserTypes"; -import { useQuery } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; import z from "zod"; -import { type PolicyFormValues, createPolicySchema } from "."; import { toast } from "@app/hooks/useToast"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { orgs, type ResourcePolicy } from "@server/db"; -import type { AxiosResponse } from "axios"; -import { useRouter } from "next/navigation"; +import { createPolicySchema, type PolicyFormValues } from "."; import { SwitchInput } from "@app/components/SwitchInput"; -import { Tag, TagInput } from "@app/components/tags/tag-input"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Button } from "@app/components/ui/button"; import { Command, @@ -47,7 +29,6 @@ import { import { Form, FormControl, - FormDescription, FormField, FormItem, FormLabel, @@ -76,21 +57,6 @@ import { TableHeader, TableRow } from "@app/components/ui/table"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; -import { - InputOTP, - InputOTPGroup, - InputOTPSlot -} from "@app/components/ui/input-otp"; import { MAJOR_ASNS } from "@server/db/asns"; import { COUNTRIES } from "@server/db/countries"; @@ -108,21 +74,10 @@ import { getSortedRowModel, useReactTable } from "@tanstack/react-table"; -import { - ArrowUpDown, - Binary, - Bot, - Check, - ChevronsUpDown, - InfoIcon, - Key, - Plus -} from "lucide-react"; +import { ArrowUpDown, Check, ChevronsUpDown, Plus } from "lucide-react"; -import { useCallback, useMemo, useState, useActionState } from "react"; -import { UseFormReturn, useForm, useWatch } from "react-hook-form"; -import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; -import { cn } from "@app/lib/cn"; +import { useCallback, useMemo, useState } from "react"; +import { UseFormReturn, useForm } from "react-hook-form"; // ─── PolicyRulesSection ─────────────────────────────────────────────────────── @@ -145,17 +100,24 @@ type LocalRule = { }; type PolicyRulesSectionProps = { - form: UseFormReturn; isMaxmindAvailable: boolean; isMaxmindAsnAvailable: boolean; }; -export function PolicyRulesSection({ - form, +export function EditPolicyRulesSectionForm({ isMaxmindAvailable, isMaxmindAsnAvailable }: PolicyRulesSectionProps) { const t = useTranslations(); + + const form = useForm({ + resolver: zodResolver( + createPolicySchema.pick({ + rules: true, + applyRules: true + }) + ) + }); const [isExpanded, setIsExpanded] = useState(false); const [rules, setRules] = useState([]); const [rulesEnabled, setRulesEnabled] = useState(false); From 1a5e9f10053e94432abe7fb38ef539b25a128499 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 4 Mar 2026 19:31:59 +0100 Subject: [PATCH 051/771] =?UTF-8?q?=F0=9F=9A=A7=20resource=20policy=20rule?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + server/auth/actions.ts | 3 +- server/db/pg/schema/schema.ts | 1 + server/routers/external.ts | 9 + server/routers/integration.ts | 9 + server/routers/policy/index.ts | 1 + .../routers/policy/setResourcePolicyRules.ts | 162 ++++++++++++++++++ .../EditPolicyOtpEmailSectionForm.tsx | 2 +- .../EditPolicyRulesSectionForm.tsx | 14 +- 9 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 server/routers/policy/setResourcePolicyRules.ts diff --git a/messages/en-US.json b/messages/en-US.json index 1ad1d89be..af2eef9cb 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -661,6 +661,7 @@ "policyCreatedSuccess": "Resource policy succesfully created", "policyUpdatedSuccess": "Resource policy succesfully updated", "authMethodsSave": "Save auth methods", + "rulesSave": "Save Rules", "resourceErrorCreate": "Error creating resource", "resourceErrorCreateDescription": "An error occurred when creating the resource", "resourceErrorCreateMessage": "Error creating resource:", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 5c512181a..b34b3fe57 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -145,7 +145,8 @@ export enum ActionsEnum { setResourcePolicyPassword = "setResourcePolicyPassword", setResourcePolicyPincode = "setResourcePolicyPincode", setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth", - setResourcePolicyWhitelist = "setResourcePolicyWhitelist" + setResourcePolicyWhitelist = "setResourcePolicyWhitelist", + setResourcePolicyRules = "setResourcePolicyRules" } export async function checkUserActionPermission( diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index e4388b502..b33360b1f 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -678,6 +678,7 @@ export const policyRules = pgTable("policyRules", { export const resourcePolicies = pgTable("resourcePolicies", { resourcePolicyId: serial("resourcePolicyId").primaryKey(), sso: boolean("sso").notNull().default(true), + applyRules: boolean("applyRules").notNull().default(false), scope: varchar("scope") .$type<"global" | "resource">() .notNull() diff --git a/server/routers/external.ts b/server/routers/external.ts index 6e74e44a9..671bd4ac7 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -737,6 +737,15 @@ authenticated.put( policy.setResourcePolicyWhitelist ); +authenticated.put( + "/resource-policy/:resourcePolicyId/rules", + verifyResourcePolicyAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.setResourcePolicyRules), + logActionAudit(ActionsEnum.setResourcePolicyRules), + policy.setResourcePolicyRules +); + authenticated.post( `/resource/:resourceId/password`, verifyResourceAccess, diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 89ec2c2d7..92f1531ee 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -668,6 +668,15 @@ authenticated.put( policy.setResourcePolicyWhitelist ); +authenticated.put( + "/resource-policy/:resourcePolicyId/rules", + verifyApiKeyResourcePolicyAccess, + verifyLimits, + verifyApiKeyHasAction(ActionsEnum.setResourcePolicyRules), + logActionAudit(ActionsEnum.setResourcePolicyRules), + policy.setResourcePolicyRules +); + authenticated.post( "/resource/:resourceId/roles/add", verifyApiKeyResourceAccess, diff --git a/server/routers/policy/index.ts b/server/routers/policy/index.ts index 7719ffdfe..2ebe6da7e 100644 --- a/server/routers/policy/index.ts +++ b/server/routers/policy/index.ts @@ -5,3 +5,4 @@ export * from "./setResourcePolicyPassword"; export * from "./setResourcePolicyPincode"; export * from "./setResourcePolicyHeaderAuth"; export * from "./setResourcePolicyWhitelist"; +export * from "./setResourcePolicyRules"; diff --git a/server/routers/policy/setResourcePolicyRules.ts b/server/routers/policy/setResourcePolicyRules.ts new file mode 100644 index 000000000..147a67814 --- /dev/null +++ b/server/routers/policy/setResourcePolicyRules.ts @@ -0,0 +1,162 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, policyRules, resourcePolicies } from "@server/db"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { + isValidCIDR, + isValidIP, + isValidUrlGlobPattern +} from "@server/lib/validators"; +import { OpenAPITags, registry } from "@server/openApi"; + +const ruleSchema = z.strictObject({ + action: z.enum(["ACCEPT", "DROP", "PASS"]).openapi({ + type: "string", + enum: ["ACCEPT", "DROP", "PASS"], + description: "rule action" + }), + match: z.enum(["CIDR", "IP", "PATH"]).openapi({ + type: "string", + enum: ["CIDR", "IP", "PATH"], + description: "rule match" + }), + value: z.string().min(1), + priority: z.int(), + enabled: z.boolean().optional() +}); + +const setResourcePolicyRulesBodySchema = z.strictObject({ + applyRules: z.boolean(), + rules: z.array(ruleSchema) +}); + +const setResourcePolicyRulesParamsSchema = z.strictObject({ + resourcePolicyId: z.string().transform(Number).pipe(z.int().positive()) +}); + +registry.registerPath({ + method: "put", + path: "/resource-policy/{resourcePolicyId}/rules", + description: + "Set all rules for a resource policy at once. This will replace all existing rules.", + tags: [OpenAPITags.Policy], + request: { + params: setResourcePolicyRulesParamsSchema, + body: { + content: { + "application/json": { + schema: setResourcePolicyRulesBodySchema + } + } + } + }, + responses: {} +}); + +export async function setResourcePolicyRules( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = setResourcePolicyRulesParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = setResourcePolicyRulesBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { resourcePolicyId } = parsedParams.data; + const { applyRules, rules } = parsedBody.data; + + const [policy] = await db + .select() + .from(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)) + .limit(1); + + if (!policy) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Resource policy not found") + ); + } + + for (const rule of rules) { + if (rule.match === "CIDR" && !isValidCIDR(rule.value)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid CIDR provided" + ) + ); + } else if (rule.match === "IP" && !isValidIP(rule.value)) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid IP provided") + ); + } else if ( + rule.match === "PATH" && + !isValidUrlGlobPattern(rule.value) + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid URL glob pattern provided" + ) + ); + } + } + + await db.transaction(async (trx) => { + await trx + .update(resourcePolicies) + .set({ applyRules }) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)); + + await trx + .delete(policyRules) + .where(eq(policyRules.resourcePolicyId, resourcePolicyId)); + + if (rules.length > 0) { + await trx.insert(policyRules).values( + rules.map((rule) => ({ + resourcePolicyId, + ...rule + })) + ); + } + }); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Resource policy rules set successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx index c120c13da..3a117bd8a 100644 --- a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx @@ -68,7 +68,7 @@ export function EditPolicyOtpEmailSectionForm({ defaultValues: { emailWhitelistEnabled: policy.emailWhitelistEnabled, emails: policy.emailWhiteList.map((email) => ({ - id: email.whitelistId.toString(), + id: email.whiteListId.toString(), text: email.email })) } diff --git a/src/components/resource-policy/EditPolicyRulesSectionForm.tsx b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx index f8f044740..23c8a1dd9 100644 --- a/src/components/resource-policy/EditPolicyRulesSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx @@ -4,6 +4,7 @@ import { SettingsSection, SettingsSectionBody, SettingsSectionDescription, + SettingsSectionFooter, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; @@ -76,7 +77,7 @@ import { } from "@tanstack/react-table"; import { ArrowUpDown, Check, ChevronsUpDown, Plus } from "lucide-react"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useMemo, useState, useTransition } from "react"; import { UseFormReturn, useForm } from "react-hook-form"; // ─── PolicyRulesSection ─────────────────────────────────────────────────────── @@ -615,6 +616,8 @@ export function EditPolicyRulesSectionForm({ state: { pagination: { pageIndex: 0, pageSize: 1000 } } }); + const [isPending, startTransition] = useTransition(); + if (!isExpanded) { return ( @@ -1070,6 +1073,15 @@ export function EditPolicyRulesSectionForm({
+ + +
); } From 8a3c0d9a08d54d27725f3e09a50dbcdea2410e22 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 5 Mar 2026 17:51:55 +0100 Subject: [PATCH 052/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20add=20openapi=20sc?= =?UTF-8?q?hema=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/policy/getResourcePolicy.ts | 9 ++++++++- server/routers/policy/setResourcePolicyAccessControl.ts | 9 +++++++-- server/routers/policy/setResourcePolicyRules.ts | 5 ++++- server/routers/site/listSites.ts | 6 ++++-- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/server/routers/policy/getResourcePolicy.ts b/server/routers/policy/getResourcePolicy.ts index 02c370199..c5b567a16 100644 --- a/server/routers/policy/getResourcePolicy.ts +++ b/server/routers/policy/getResourcePolicy.ts @@ -28,7 +28,14 @@ const getResourcePolicySchema = z }) .or( z.strictObject({ - resourcePolicyId: z.coerce.number().int().positive() + resourcePolicyId: z.coerce + .number() + .int() + .positive() + .openapi({ + type: "integer", + description: "Resource policy ID" + }) }) ); diff --git a/server/routers/policy/setResourcePolicyAccessControl.ts b/server/routers/policy/setResourcePolicyAccessControl.ts index 1f91d8ccc..6c0e19b68 100644 --- a/server/routers/policy/setResourcePolicyAccessControl.ts +++ b/server/routers/policy/setResourcePolicyAccessControl.ts @@ -22,8 +22,13 @@ import { OpenAPITags, registry } from "@server/openApi"; const setResourcePolicyAcccessControlBodySchema = z.strictObject({ sso: z.boolean(), userIds: z.array(z.string()), - roleIds: z.array(z.int().positive()), - skipToIdpId: z.int().positive().optional().nullish() + roleIds: z.array(z.int().positive()).openapi({ + type: "array" + }), + skipToIdpId: z.int().positive().optional().nullable().openapi({ + type: "integer", + description: "Page number to retrieve" + }) }); const setResourcePolicyAccessControlParamsSchema = z.strictObject({ diff --git a/server/routers/policy/setResourcePolicyRules.ts b/server/routers/policy/setResourcePolicyRules.ts index 147a67814..61ed9bece 100644 --- a/server/routers/policy/setResourcePolicyRules.ts +++ b/server/routers/policy/setResourcePolicyRules.ts @@ -26,7 +26,10 @@ const ruleSchema = z.strictObject({ description: "rule match" }), value: z.string().min(1), - priority: z.int(), + priority: z.int().openapi({ + type: "integer", + description: "Rule priority" + }), enabled: z.boolean().optional() }); diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 9ff7a6933..dc6b62ba8 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -97,7 +97,7 @@ const listSitesSchema = z.object({ page: z.coerce .number() // for prettier formatting .int() - .min(0) + .positive() .optional() .catch(1) .default(1) @@ -278,7 +278,9 @@ export async function listSites( // we need to add `as` so that drizzle filters the result as a subquery const countQuery = db.$count( - querySitesBase().where(and(...conditions)).as("filtered_sites") + querySitesBase() + .where(and(...conditions)) + .as("filtered_sites") ); const siteListQuery = baseQuery From de2980e1bca95254e5d9c9b215cdd1667c59b219 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 5 Mar 2026 18:13:30 +0100 Subject: [PATCH 053/771] =?UTF-8?q?=E2=9C=A8=20apply=20rules=20on=20resour?= =?UTF-8?q?ce=20policies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 2 +- server/routers/policy/getResourcePolicy.ts | 17 +++- .../routers/policy/setResourcePolicyRules.ts | 10 ++- .../EditPolicyRulesSectionForm.tsx | 77 ++++++++++++++++--- 4 files changed, 90 insertions(+), 16 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index b33360b1f..934dbb6da 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -661,7 +661,7 @@ export const resourceRules = pgTable("resourceRules", { value: varchar("value").notNull() }); -export const policyRules = pgTable("policyRules", { +export const resourcePolicyRules = pgTable("resourcePolicyRules", { ruleId: serial("ruleId").primaryKey(), resourcePolicyId: integer("resourcePolicyId") .notNull() diff --git a/server/routers/policy/getResourcePolicy.ts b/server/routers/policy/getResourcePolicy.ts index c5b567a16..2a975dde7 100644 --- a/server/routers/policy/getResourcePolicy.ts +++ b/server/routers/policy/getResourcePolicy.ts @@ -1,6 +1,7 @@ import { db, idp, + resourcePolicyRules, resourcePolicies, resourcePolicyHeaderAuth, resourcePolicyPassword, @@ -56,6 +57,7 @@ async function query(params: z.infer) { .select({ resourcePolicyId: resourcePolicies.resourcePolicyId, sso: resourcePolicies.sso, + applyRules: resourcePolicies.applyRules, emailWhitelistEnabled: resourcePolicies.emailWhitelistEnabled, idpId: resourcePolicies.idpId, niceId: resourcePolicies.niceId, @@ -134,11 +136,24 @@ async function query(params: z.infer) { eq(resourcePolicyWhiteList.resourcePolicyId, res.resourcePolicyId) ); + const policyRules = await db + .select({ + ruleId: resourcePolicyRules.ruleId, + enabled: resourcePolicyRules.enabled, + priority: resourcePolicyRules.priority, + action: resourcePolicyRules.action, + match: resourcePolicyRules.match, + value: resourcePolicyRules.value + }) + .from(resourcePolicyRules) + .where(eq(resourcePolicyRules.resourcePolicyId, res.resourcePolicyId)); + return { ...res, roles: policyRoles, users: policyUsers, - emailWhiteList: policyEmailWhiteList + emailWhiteList: policyEmailWhiteList, + rules: policyRules }; } diff --git a/server/routers/policy/setResourcePolicyRules.ts b/server/routers/policy/setResourcePolicyRules.ts index 61ed9bece..533e01c0e 100644 --- a/server/routers/policy/setResourcePolicyRules.ts +++ b/server/routers/policy/setResourcePolicyRules.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, policyRules, resourcePolicies } from "@server/db"; +import { db, resourcePolicyRules, resourcePolicies } from "@server/db"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; @@ -136,11 +136,13 @@ export async function setResourcePolicyRules( .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)); await trx - .delete(policyRules) - .where(eq(policyRules.resourcePolicyId, resourcePolicyId)); + .delete(resourcePolicyRules) + .where( + eq(resourcePolicyRules.resourcePolicyId, resourcePolicyId) + ); if (rules.length > 0) { - await trx.insert(policyRules).values( + await trx.insert(resourcePolicyRules).values( rules.map((rule) => ({ resourcePolicyId, ...rule diff --git a/src/components/resource-policy/EditPolicyRulesSectionForm.tsx b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx index 23c8a1dd9..cd6f39fe1 100644 --- a/src/components/resource-policy/EditPolicyRulesSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx @@ -78,7 +78,12 @@ import { import { ArrowUpDown, Check, ChevronsUpDown, Plus } from "lucide-react"; import { useCallback, useMemo, useState, useTransition } from "react"; -import { UseFormReturn, useForm } from "react-hook-form"; +import { UseFormReturn, useForm, useWatch } from "react-hook-form"; +import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import type { AxiosResponse } from "axios"; +import { useRouter } from "next/navigation"; // ─── PolicyRulesSection ─────────────────────────────────────────────────────── @@ -111,17 +116,31 @@ export function EditPolicyRulesSectionForm({ }: PolicyRulesSectionProps) { const t = useTranslations(); + const { policy } = useResourcePolicyContext(); + const api = createApiClient(useEnvContext()); + const router = useRouter(); + const form = useForm({ resolver: zodResolver( createPolicySchema.pick({ rules: true, applyRules: true }) - ) + ), + defaultValues: { + applyRules: policy.applyRules, + rules: policy.rules + } }); - const [isExpanded, setIsExpanded] = useState(false); - const [rules, setRules] = useState([]); - const [rulesEnabled, setRulesEnabled] = useState(false); + + const rulesEnabled = useWatch({ + control: form.control, + name: "applyRules" + }); + + const [rules, setRules] = useState(policy.rules); + const [isExpanded, setIsExpanded] = useState(rulesEnabled); + const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false); const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false); @@ -618,6 +637,45 @@ export function EditPolicyRulesSectionForm({ const [isPending, startTransition] = useTransition(); + async function saveRules() { + const isValid = form.trigger(); + if (!isValid) return; + + const payload = form.getValues(); + console.log({ payload }); + + try { + const res = await api + .put< + AxiosResponse<{}> + >(`/resource-policy/${policy.resourcePolicyId}/rules`, payload) + .catch((e) => { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: formatAxiosError( + e, + t("policyErrorUpdateDescription") + ) + }); + }); + + if (res && res.status === 200) { + toast({ + title: t("success"), + description: t("policyUpdatedSuccess") + }); + router.refresh(); + } + } catch (e) { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: t("policyErrorUpdateMessageDescription") + }); + } + } + if (!isExpanded) { return ( @@ -659,9 +717,8 @@ export function EditPolicyRulesSectionForm({ { - setRulesEnabled(val); form.setValue("applyRules", val); }} /> @@ -1075,9 +1132,9 @@ export function EditPolicyRulesSectionForm({ From 51eb782831a6f1432e2b78d56ffcf6a71004b931 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 5 Mar 2026 18:14:46 +0100 Subject: [PATCH 054/771] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/resource-policy/EditPolicyForm.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index e913474a9..076e726dd 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -100,13 +100,14 @@ export function EditPolicyForm({ hidePolicyNameForm }: EditPolicyFormProps) { return ( - {/* Name */} {!hidePolicyNameForm && } + + Date: Thu, 5 Mar 2026 18:24:04 +0100 Subject: [PATCH 055/771] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20create=20resource?= =?UTF-8?q?=20policy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routers/policy/createResourcePolicy.ts | 63 ++++++++++++++++++- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/server/private/routers/policy/createResourcePolicy.ts b/server/private/routers/policy/createResourcePolicy.ts index 29bccd48b..aa9500d1c 100644 --- a/server/private/routers/policy/createResourcePolicy.ts +++ b/server/private/routers/policy/createResourcePolicy.ts @@ -26,15 +26,72 @@ const createResourcePolicyParamsSchema = z.strictObject({ orgId: z.string() }); +const ruleSchema = z.strictObject({ + action: z.enum(["ACCEPT", "DROP", "PASS"]).openapi({ + type: "string", + enum: ["ACCEPT", "DROP", "PASS"], + description: "rule action" + }), + match: z.enum(["CIDR", "IP", "PATH"]).openapi({ + type: "string", + enum: ["CIDR", "IP", "PATH"], + description: "rule match" + }), + value: z.string().min(1), + priority: z.int().openapi({ + type: "integer", + description: "Rule priority" + }), + enabled: z.boolean().optional() +}); + const createResourcePolicyBodySchema = z.strictObject({ name: z.string().min(1).max(255), - sso: z.boolean(), - skipToIdpId: z.int().positive().optional(), + // Access control + sso: z.boolean().default(true), + skipToIdpId: z + .int() + .positive() + .optional() + .nullable() + .openapi({ type: "integer" }), roleIds: z .array(z.string().transform(Number).pipe(z.int().positive())) .optional() .default([]), - userIds: z.array(z.string()).optional().default([]) + userIds: z.array(z.string()).optional().default([]), + // auth methods + password: z.string().min(4).max(100).nullable().optional(), + pincode: z + .string() + .regex(/^\d{6}$/) + .or(z.null()) + .optional(), + headerAuth: z + .object({ + user: z.string().min(4).max(100), + password: z.string().min(4).max(100), + extendedCompatibility: z.boolean() + }) + .nullable() + .optional(), + // email OTP + emailWhitelistEnabled: z.boolean().optional().default(false), + emails: z + .array( + z.email().or( + z.string().regex(/^\*@[\w.-]+\.[a-zA-Z]{2,}$/, { + error: "Invalid email address. Wildcard (*) must be the entire local part." + }) + ) + ) + .max(50) + .transform((v) => v.map((e) => e.toLowerCase())) + .optional() + .default([]), + // rules + applyRules: z.boolean().default(false), + rules: z.array(ruleSchema).optional().default([]) }); registry.registerPath({ From 595842c2c9cc5fee3bf4251e1779dab88d04e0e2 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 5 Mar 2026 18:48:33 +0100 Subject: [PATCH 056/771] =?UTF-8?q?=E2=9C=A8=20finish=20create=20policy=20?= =?UTF-8?q?endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routers/policy/createResourcePolicy.ts | 106 +++++++++++++++++- 1 file changed, 104 insertions(+), 2 deletions(-) diff --git a/server/private/routers/policy/createResourcePolicy.ts b/server/private/routers/policy/createResourcePolicy.ts index aa9500d1c..8e2757270 100644 --- a/server/private/routers/policy/createResourcePolicy.ts +++ b/server/private/routers/policy/createResourcePolicy.ts @@ -10,6 +10,11 @@ import { idpOrg, orgs, resourcePolicies, + resourcePolicyHeaderAuth, + resourcePolicyPassword, + resourcePolicyPincode, + resourcePolicyRules, + resourcePolicyWhiteList, rolePolicies, roles, userOrgs, @@ -21,6 +26,12 @@ import { and, eq, inArray, not, type InferInsertModel } from "drizzle-orm"; import logger from "@server/logger"; import { getUniqueResourcePolicyName } from "@server/db/names"; import response from "@server/lib/response"; +import { hashPassword } from "@server/auth/password"; +import { + isValidCIDR, + isValidIP, + isValidUrlGlobPattern +} from "@server/lib/validators"; const createResourcePolicyParamsSchema = z.strictObject({ orgId: z.string() @@ -164,7 +175,20 @@ export async function createResourcePolicy( ); } - const { name, sso, userIds, roleIds, skipToIdpId } = parsedBody.data; + const { + name, + sso, + userIds, + roleIds, + skipToIdpId, + applyRules, + emailWhitelistEnabled, + password, + pincode, + headerAuth, + emails, + rules + } = parsedBody.data; // Check if Identity provider in `skipToIdpId` exists if (skipToIdpId) { @@ -223,6 +247,31 @@ export async function createResourcePolicy( const niceId = await getUniqueResourcePolicyName(orgId); + for (const rule of rules) { + if (rule.match === "CIDR" && !isValidCIDR(rule.value)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid CIDR provided" + ) + ); + } else if (rule.match === "IP" && !isValidIP(rule.value)) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid IP provided") + ); + } else if ( + rule.match === "PATH" && + !isValidUrlGlobPattern(rule.value) + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid URL glob pattern provided" + ) + ); + } + } + const policy = await db.transaction(async (trx) => { const [newPolicy] = await trx .insert(resourcePolicies) @@ -231,7 +280,9 @@ export async function createResourcePolicy( orgId, name, sso, - idpId: skipToIdpId + idpId: skipToIdpId, + applyRules, + emailWhitelistEnabled }) .returning(); @@ -272,6 +323,57 @@ export async function createResourcePolicy( await trx.insert(userPolicies).values(usersToAdd); } + if (password) { + const passwordHash = await hashPassword(password); + + await trx.insert(resourcePolicyPassword).values({ + resourcePolicyId: newPolicy.resourcePolicyId, + passwordHash + }); + } + + if (pincode) { + const pincodeHash = await hashPassword(pincode); + + await trx.insert(resourcePolicyPincode).values({ + resourcePolicyId: newPolicy.resourcePolicyId, + pincodeHash, + digitLength: 6 + }); + } + + if (headerAuth) { + const headerAuthHash = await hashPassword( + Buffer.from( + `${headerAuth.user}:${headerAuth.password}` + ).toString("base64") + ); + + await trx.insert(resourcePolicyHeaderAuth).values({ + resourcePolicyId: newPolicy.resourcePolicyId, + headerAuthHash, + extendedCompatibility: headerAuth.extendedCompatibility + }); + } + + if (emailWhitelistEnabled && emails.length > 0) { + await trx.insert(resourcePolicyWhiteList).values( + emails.map((email) => ({ + email, + resourcePolicyId: newPolicy.resourcePolicyId + })) + ); + } + + if (rules.length > 0) { + await trx.insert(resourcePolicyRules).values( + rules.map((rule) => ({ + resourcePolicyId: newPolicy.resourcePolicyId, + ...rule + })) + ); + } + return newPolicy; }); From cd5a38b1eb582884ace2bc014574f8a9c9c353a1 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 5 Mar 2026 18:56:35 +0100 Subject: [PATCH 057/771] =?UTF-8?q?=F0=9F=9A=A7=20WIP:=20create=20policy?= =?UTF-8?q?=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routers/policy/createResourcePolicy.ts | 18 +- .../CreatePolicyAuthMethodsSectionForm.tsx | 487 +++++ .../resource-policy/CreatePolicyForm.tsx | 1866 +---------------- .../CreatePolicyOtpEmailSectionForm.tsx | 177 ++ .../CreatePolicyRulesSectionForm.tsx | 1073 ++++++++++ .../CreatePolicyUserRolesSectionForm.tsx | 224 ++ 6 files changed, 1993 insertions(+), 1852 deletions(-) create mode 100644 src/components/resource-policy/CreatePolicyAuthMethodsSectionForm.tsx create mode 100644 src/components/resource-policy/CreatePolicyOtpEmailSectionForm.tsx create mode 100644 src/components/resource-policy/CreatePolicyRulesSectionForm.tsx create mode 100644 src/components/resource-policy/CreatePolicyUserRolesSectionForm.tsx diff --git a/server/private/routers/policy/createResourcePolicy.ts b/server/private/routers/policy/createResourcePolicy.ts index 8e2757270..1bbdfe153 100644 --- a/server/private/routers/policy/createResourcePolicy.ts +++ b/server/private/routers/policy/createResourcePolicy.ts @@ -1,9 +1,4 @@ -import { Request, Response, NextFunction } from "express"; -import z from "zod"; -import { OpenAPITags, registry } from "@server/openApi"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import { fromError } from "zod-validation-error"; +import { hashPassword } from "@server/auth/password"; import { db, idp, @@ -22,16 +17,21 @@ import { users, type ResourcePolicy } from "@server/db"; -import { and, eq, inArray, not, type InferInsertModel } from "drizzle-orm"; -import logger from "@server/logger"; import { getUniqueResourcePolicyName } from "@server/db/names"; import response from "@server/lib/response"; -import { hashPassword } from "@server/auth/password"; import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "@server/lib/validators"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; +import HttpCode from "@server/types/HttpCode"; +import { and, eq, inArray, type InferInsertModel } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import z from "zod"; +import { fromError } from "zod-validation-error"; const createResourcePolicyParamsSchema = z.strictObject({ orgId: z.string() diff --git a/src/components/resource-policy/CreatePolicyAuthMethodsSectionForm.tsx b/src/components/resource-policy/CreatePolicyAuthMethodsSectionForm.tsx new file mode 100644 index 000000000..fb456e823 --- /dev/null +++ b/src/components/resource-policy/CreatePolicyAuthMethodsSectionForm.tsx @@ -0,0 +1,487 @@ +"use client"; + +import { + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslations } from "next-intl"; + +import z from "zod"; + +import { type PolicyFormValues } from "."; + +import { SwitchInput } from "@app/components/SwitchInput"; +import { Button } from "@app/components/ui/button"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; +import { + InputOTP, + InputOTPGroup, + InputOTPSlot +} from "@app/components/ui/input-otp"; + +import { Binary, Bot, Key, Plus } from "lucide-react"; + +import { useState } from "react"; +import { type UseFormReturn, useForm } from "react-hook-form"; + +// ─── CreatePolicyAuthMethodsSectionForm ─────────────────────────────────────── + +const setPasswordSchema = z.object({ + password: z.string().min(4).max(100) +}); + +const setPincodeSchema = z.object({ + pincode: z.string().length(6) +}); + +const setHeaderAuthSchema = z.object({ + user: z.string().min(4).max(100), + password: z.string().min(4).max(100), + extendedCompatibility: z.boolean() +}); + +export type CreatePolicyAuthMethodsSectionFormProps = { + form: UseFormReturn; +}; + +export function CreatePolicyAuthMethodsSectionForm({ + form +}: CreatePolicyAuthMethodsSectionFormProps) { + const t = useTranslations(); + const [isOpen, setIsOpen] = useState(false); + const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); + const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); + const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); + + const password = form.watch("password"); + const pincode = form.watch("pincode"); + const headerAuth = form.watch("headerAuth"); + + const passwordForm = useForm({ + resolver: zodResolver(setPasswordSchema), + defaultValues: { password: "" } + }); + + const pincodeForm = useForm({ + resolver: zodResolver(setPincodeSchema), + defaultValues: { pincode: "" } + }); + + const headerAuthForm = useForm({ + resolver: zodResolver(setHeaderAuthSchema), + defaultValues: { user: "", password: "", extendedCompatibility: true } + }); + + if (!isOpen) { + return ( + + + + {t("resourceAuthMethods")} + + + {t("resourcePolicyAuthMethodsDescription")} + + + + + + + ); + } + + return ( + <> + {/* Password Credenza */} + { + setIsSetPasswordOpen(val); + if (!val) passwordForm.reset(); + }} + > + + + + {t("resourcePasswordSetupTitle")} + + + {t("resourcePasswordSetupTitleDescription")} + + + +
+ { + form.setValue("password", data); + setIsSetPasswordOpen(false); + passwordForm.reset(); + })} + className="space-y-4" + id="set-password-form" + > + ( + + + {t("password")} + + + + + + + )} + /> + + +
+ + + + + + +
+
+ + {/* Pincode Credenza */} + { + setIsSetPincodeOpen(val); + if (!val) pincodeForm.reset(); + }} + > + + + + {t("resourcePincodeSetupTitle")} + + + {t("resourcePincodeSetupTitleDescription")} + + + +
+ { + form.setValue("pincode", data); + setIsSetPincodeOpen(false); + pincodeForm.reset(); + })} + className="space-y-4" + id="set-pincode-form" + > + ( + + + {t("resourcePincode")} + + +
+ + + + + + + + + + +
+
+ +
+ )} + /> + + +
+ + + + + + +
+
+ + {/* Header Auth Credenza */} + { + setIsSetHeaderAuthOpen(val); + if (!val) headerAuthForm.reset(); + }} + > + + + + {t("resourceHeaderAuthSetupTitle")} + + + {t("resourceHeaderAuthSetupTitleDescription")} + + + +
+ { + form.setValue("headerAuth", data); + setIsSetHeaderAuthOpen(false); + headerAuthForm.reset(); + } + )} + className="space-y-4" + id="set-header-auth-form" + > + ( + + {t("user")} + + + + + + )} + /> + ( + + + {t("password")} + + + + + + + )} + /> + ( + + + + + + + )} + /> + + +
+ + + + + + +
+
+ + + + + {t("resourceAuthMethods")} + + + {t("resourcePolicyAuthMethodsDescription")} + + + + + {/* Password row */} +
+
+ + + {t("resourcePasswordProtection", { + status: password + ? t("enabled") + : t("disabled") + })} + +
+ +
+ + {/* Pincode row */} +
+
+ + + {t("resourcePincodeProtection", { + status: pincode + ? t("enabled") + : t("disabled") + })} + +
+ +
+ + {/* Header auth row */} +
+
+ + + {headerAuth + ? t( + "resourceHeaderAuthProtectionEnabled" + ) + : t( + "resourceHeaderAuthProtectionDisabled" + )} + +
+ +
+
+
+
+ + ); +} diff --git a/src/components/resource-policy/CreatePolicyForm.tsx b/src/components/resource-policy/CreatePolicyForm.tsx index d805e8772..9cc2d06b4 100644 --- a/src/components/resource-policy/CreatePolicyForm.tsx +++ b/src/components/resource-policy/CreatePolicyForm.tsx @@ -32,95 +32,23 @@ import { orgs, type ResourcePolicy } from "@server/db"; import type { AxiosResponse } from "axios"; import { useRouter } from "next/navigation"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { Tag, TagInput } from "@app/components/tags/tag-input"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Button } from "@app/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; import { Form, FormControl, - FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@app/components/ui/form"; -import { InfoPopup } from "@app/components/ui/info-popup"; import { Input } from "@app/components/ui/input"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@app/components/ui/select"; -import { Switch } from "@app/components/ui/switch"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow -} from "@app/components/ui/table"; -import { - Credenza, - CredenzaBody, - CredenzaClose, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; -import { - InputOTP, - InputOTPGroup, - InputOTPSlot -} from "@app/components/ui/input-otp"; -import { MAJOR_ASNS } from "@server/db/asns"; -import { COUNTRIES } from "@server/db/countries"; -import { - isValidCIDR, - isValidIP, - isValidUrlGlobPattern -} from "@server/lib/validators"; -import { - ColumnDef, - flexRender, - getCoreRowModel, - getFilteredRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable -} from "@tanstack/react-table"; -import { - ArrowUpDown, - Binary, - Bot, - Check, - ChevronsUpDown, - InfoIcon, - Key, - Plus -} from "lucide-react"; - -import { useCallback, useMemo, useState, useActionState } from "react"; -import { UseFormReturn, useForm, useWatch } from "react-hook-form"; +import { useMemo, useActionState } from "react"; +import { useForm } from "react-hook-form"; +import { CreatePolicyUsersRolesSectionForm } from "./CreatePolicyUserRolesSectionForm"; +import { CreatePolicyAuthMethodsSectionForm } from "./CreatePolicyAuthMethodsSectionForm"; +import { CreatePolicyOtpEmailSectionForm } from "./CreatePolicyOtpEmailSectionForm"; +import { CreatePolicyRulesSectionForm } from "./CreatePolicyRulesSectionForm"; // ─── CreatePolicyForm ───────────────────────────────────────────────────────── @@ -187,9 +115,21 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) { `/org/${org.org.orgId}/resource-policy/`, { name: payload.name, + // access control sso: payload.sso, roleIds: payload.roles.map((r) => r.id), - userIds: payload.users.map((u) => u.id) + userIds: payload.users.map((u) => u.id), + skipToIdpId: payload.skipToIdpId, + // auth methods + password: payload.password?.password, + pincode: payload.pincode?.pincode, + headerAuth: payload.headerAuth, + // email OTP + emailWhitelistEnabled: payload.emailWhitelistEnabled, + emails: payload.emails.map((email) => email.text), + // rules + applyRules: payload.applyRules, + rules: payload.rules } ) .catch((e) => { @@ -298,18 +238,18 @@ export function CreatePolicyForm({}: CreatePolicyFormProps) {
- - - + - ); } - -// ─── PolicyUsersRolesSection ────────────────────────────────────────────────── - -type PolicyUsersRolesSectionProps = { - form: UseFormReturn; - allRoles: { id: string; text: string }[]; - allUsers: { id: string; text: string }[]; - allIdps: { id: number; text: string }[]; -}; - -export function PolicyUsersRolesSection({ - form, - allRoles, - allUsers, - allIdps -}: PolicyUsersRolesSectionProps) { - const t = useTranslations(); - const ssoEnabled = useWatch({ control: form.control, name: "sso" }); - const selectedIdpId = useWatch({ - control: form.control, - name: "skipToIdpId" - }); - const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< - number | null - >(null); - const [activeUsersTagIndex, setActiveUsersTagIndex] = useState< - number | null - >(null); - - return ( - - - - {t("resourceUsersRoles")} - - - {t("resourcePolicyUsersRolesDescription")} - - - - - { - console.log(`form.setValue("sso", ${val})`); - form.setValue("sso", val); - }} - /> - - {ssoEnabled && ( - <> - ( - - {t("roles")} - - { - form.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={true} - autocompleteOptions={allRoles} - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - {t("resourceRoleDescription")} - - - )} - /> - ( - - {t("users")} - - { - form.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={true} - autocompleteOptions={allUsers} - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - )} - /> - - )} - - {ssoEnabled && allIdps.length > 0 && ( -
- - -

- {t("defaultIdentityProviderDescription")} -

-
- )} -
-
-
- ); -} - -// ─── PolicyAuthMethodsSection ───────────────────────────────────────────────── - -const setPasswordSchema = z.object({ - password: z.string().min(4).max(100) -}); - -const setPincodeSchema = z.object({ - pincode: z.string().length(6) -}); - -const setHeaderAuthSchema = z.object({ - user: z.string().min(4).max(100), - password: z.string().min(4).max(100), - extendedCompatibility: z.boolean() -}); - -type PolicyAuthMethodsSectionProps = { - form: UseFormReturn; -}; - -export function PolicyAuthMethodsSection({ - form -}: PolicyAuthMethodsSectionProps) { - const t = useTranslations(); - const [isOpen, setIsOpen] = useState(false); - const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); - const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); - const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); - - const password = form.watch("password"); - const pincode = form.watch("pincode"); - const headerAuth = form.watch("headerAuth"); - - const passwordForm = useForm({ - resolver: zodResolver(setPasswordSchema), - defaultValues: { password: "" } - }); - - const pincodeForm = useForm({ - resolver: zodResolver(setPincodeSchema), - defaultValues: { pincode: "" } - }); - - const headerAuthForm = useForm({ - resolver: zodResolver(setHeaderAuthSchema), - defaultValues: { user: "", password: "", extendedCompatibility: true } - }); - - if (!isOpen) { - return ( - - - - {t("resourceAuthMethods")} - - - {t("resourcePolicyAuthMethodsDescription")} - - - - - - - ); - } - - return ( - <> - {/* Password Credenza */} - { - setIsSetPasswordOpen(val); - if (!val) passwordForm.reset(); - }} - > - - - - {t("resourcePasswordSetupTitle")} - - - {t("resourcePasswordSetupTitleDescription")} - - - -
- { - form.setValue("password", data); - setIsSetPasswordOpen(false); - passwordForm.reset(); - })} - className="space-y-4" - id="set-password-form" - > - ( - - - {t("password")} - - - - - - - )} - /> - - -
- - - - - - -
-
- - {/* Pincode Credenza */} - { - setIsSetPincodeOpen(val); - if (!val) pincodeForm.reset(); - }} - > - - - - {t("resourcePincodeSetupTitle")} - - - {t("resourcePincodeSetupTitleDescription")} - - - -
- { - form.setValue("pincode", data); - setIsSetPincodeOpen(false); - pincodeForm.reset(); - })} - className="space-y-4" - id="set-pincode-form" - > - ( - - - {t("resourcePincode")} - - -
- - - - - - - - - - -
-
- -
- )} - /> - - -
- - - - - - -
-
- - {/* Header Auth Credenza */} - { - setIsSetHeaderAuthOpen(val); - if (!val) headerAuthForm.reset(); - }} - > - - - - {t("resourceHeaderAuthSetupTitle")} - - - {t("resourceHeaderAuthSetupTitleDescription")} - - - -
- { - form.setValue("headerAuth", data); - setIsSetHeaderAuthOpen(false); - headerAuthForm.reset(); - } - )} - className="space-y-4" - id="set-header-auth-form" - > - ( - - {t("user")} - - - - - - )} - /> - ( - - - {t("password")} - - - - - - - )} - /> - ( - - - - - - - )} - /> - - -
- - - - - - -
-
- - - - - {t("resourceAuthMethods")} - - - {t("resourcePolicyAuthMethodsDescription")} - - - - - {/* Password row */} -
-
- - - {t("resourcePasswordProtection", { - status: password - ? t("enabled") - : t("disabled") - })} - -
- -
- - {/* Pincode row */} -
-
- - - {t("resourcePincodeProtection", { - status: pincode - ? t("enabled") - : t("disabled") - })} - -
- -
- - {/* Header auth row */} -
-
- - - {headerAuth - ? t( - "resourceHeaderAuthProtectionEnabled" - ) - : t( - "resourceHeaderAuthProtectionDisabled" - )} - -
- -
-
-
-
- - ); -} - -// ─── PolicyOtpEmailSection ──────────────────────────────────────────────────── - -type PolicyOtpEmailSectionProps = { - form: UseFormReturn; - emailEnabled: boolean; -}; - -export function PolicyOtpEmailSection({ - form, - emailEnabled -}: PolicyOtpEmailSectionProps) { - const t = useTranslations(); - const [isOpen, setIsOpen] = useState(false); - const [whitelistEnabled, setWhitelistEnabled] = useState(false); - const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< - number | null - >(null); - - if (!isOpen) { - return ( - - - - {t("otpEmailTitle")} - - - {t("otpEmailTitleDescription")} - - - - - - - ); - } - - return ( - - - - {t("otpEmailTitle")} - - - {t("otpEmailTitleDescription")} - - - - - {!emailEnabled && ( - - - - {t("otpEmailSmtpRequired")} - - - {t("otpEmailSmtpRequiredDescription")} - - - )} - { - setWhitelistEnabled(val); - form.setValue("emailWhitelistEnabled", val); - }} - disabled={!emailEnabled} - /> - - {whitelistEnabled && emailEnabled && ( - ( - - - - - - {/* @ts-ignore */} - { - return z - .email() - .or( - z - .string() - .regex( - /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, - { - message: t( - "otpEmailErrorInvalid" - ) - } - ) - ) - .safeParse(tag).success; - }} - setActiveTagIndex={ - setActiveEmailTagIndex - } - placeholder={t("otpEmailEnter")} - tags={form.getValues().emails} - setTags={(newEmails) => { - form.setValue( - "emails", - newEmails as [Tag, ...Tag[]] - ); - }} - allowDuplicates={false} - sortTags={true} - /> - - - {t("otpEmailEnterDescription")} - - - )} - /> - )} - - - - ); -} - -// ─── PolicyRulesSection ─────────────────────────────────────────────────────── - -const addRuleSchema = z.object({ - action: z.enum(["ACCEPT", "DROP", "PASS"]), - match: z.string(), - value: z.string(), - priority: z.coerce.number().int().optional() -}); - -type LocalRule = { - ruleId: number; - action: "ACCEPT" | "DROP" | "PASS"; - match: string; - value: string; - priority: number; - enabled: boolean; - new?: boolean; - updated?: boolean; -}; - -type PolicyRulesSectionProps = { - form: UseFormReturn; - isMaxmindAvailable: boolean; - isMaxmindAsnAvailable: boolean; -}; - -export function PolicyRulesSection({ - form, - isMaxmindAvailable, - isMaxmindAsnAvailable -}: PolicyRulesSectionProps) { - const t = useTranslations(); - const [isOpen, setIsOpen] = useState(false); - const [rules, setRules] = useState([]); - const [rulesEnabled, setRulesEnabled] = useState(false); - const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = - useState(false); - const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false); - - const addRuleForm = useForm({ - resolver: zodResolver(addRuleSchema), - defaultValues: { - action: "ACCEPT" as const, - match: "IP", - value: "" - } - }); - - const RuleAction = useMemo( - () => ({ - ACCEPT: t("alwaysAllow"), - DROP: t("alwaysDeny"), - PASS: t("passToAuth") - }), - [t] - ); - - const RuleMatch = useMemo( - () => ({ - PATH: t("path"), - IP: "IP", - CIDR: t("ipAddressRange"), - COUNTRY: t("country"), - ASN: "ASN" - }), - [t] - ); - - const syncFormRules = useCallback( - (updatedRules: LocalRule[]) => { - form.setValue( - "rules", - updatedRules.map( - ({ action, match, value, priority, enabled }) => ({ - action, - match, - value, - priority, - enabled - }) - ) - ); - }, - [form] - ); - - const addRule = useCallback( - function addRule(data: z.infer) { - const isDuplicate = rules.some( - (rule) => - rule.action === data.action && - rule.match === data.match && - rule.value === data.value - ); - if (isDuplicate) { - toast({ - variant: "destructive", - title: t("rulesErrorDuplicate"), - description: t("rulesErrorDuplicateDescription") - }); - return; - } - if (data.match === "CIDR" && !isValidCIDR(data.value)) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidIpAddressRange"), - description: t("rulesErrorInvalidIpAddressRangeDescription") - }); - return; - } - if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidUrl"), - description: t("rulesErrorInvalidUrlDescription") - }); - return; - } - if (data.match === "IP" && !isValidIP(data.value)) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidIpAddress"), - description: t("rulesErrorInvalidIpAddressDescription") - }); - return; - } - if ( - data.match === "COUNTRY" && - !COUNTRIES.some((c) => c.code === data.value) - ) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidCountry"), - description: t("rulesErrorInvalidCountryDescription") || "" - }); - return; - } - - let priority = data.priority; - if (priority === undefined) { - priority = - rules.reduce( - (acc, rule) => - rule.priority > acc ? rule.priority : acc, - 0 - ) + 1; - } - - const updatedRules = [ - ...rules, - { - ...data, - ruleId: new Date().getTime(), - new: true, - priority, - enabled: true - } - ]; - setRules(updatedRules); - syncFormRules(updatedRules); - addRuleForm.reset(); - }, - [rules, t, addRuleForm, syncFormRules] - ); - - const removeRule = useCallback( - function removeRule(ruleId: number) { - const updatedRules = rules.filter((rule) => rule.ruleId !== ruleId); - setRules(updatedRules); - syncFormRules(updatedRules); - }, - [rules, syncFormRules] - ); - - const updateRule = useCallback( - function updateRule(ruleId: number, data: Partial) { - const updatedRules = rules.map((rule) => - rule.ruleId === ruleId - ? { ...rule, ...data, updated: true } - : rule - ); - setRules(updatedRules); - syncFormRules(updatedRules); - }, - [rules, syncFormRules] - ); - - const getValueHelpText = useCallback( - function getValueHelpText(type: string) { - switch (type) { - case "CIDR": - return t("rulesMatchIpAddressRangeDescription"); - case "IP": - return t("rulesMatchIpAddress"); - case "PATH": - return t("rulesMatchUrl"); - case "COUNTRY": - return t("rulesMatchCountry"); - case "ASN": - return "Enter an Autonomous System Number (e.g., AS15169 or 15169)"; - } - }, - [t] - ); - - const columns: ColumnDef[] = useMemo( - () => [ - { - accessorKey: "priority", - header: ({ column }) => ( - - ), - cell: ({ row }) => ( - e.currentTarget.focus()} - onBlur={(e) => { - const parsed = z.coerce - .number() - .int() - .optional() - .safeParse(e.target.value); - if (!parsed.success) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidPriority"), - description: t( - "rulesErrorInvalidPriorityDescription" - ) - }); - return; - } - updateRule(row.original.ruleId, { - priority: parsed.data - }); - }} - /> - ) - }, - { - accessorKey: "action", - header: () => {t("rulesAction")}, - cell: ({ row }) => ( - - ) - }, - { - accessorKey: "match", - header: () => ( - {t("rulesMatchType")} - ), - cell: ({ row }) => ( - - ) - }, - { - accessorKey: "value", - header: () => {t("value")}, - cell: ({ row }) => - row.original.match === "COUNTRY" ? ( - - - - - - - - - - {t("noCountryFound")} - - - {COUNTRIES.map((country) => ( - - updateRule( - row.original.ruleId, - { - value: country.code - } - ) - } - > - - {country.name} ( - {country.code}) - - ))} - - - - - - ) : row.original.match === "ASN" ? ( - - - - - - - - - - No ASN found. Enter a custom ASN - below. - - - {MAJOR_ASNS.map((asn) => ( - - updateRule( - row.original.ruleId, - { value: asn.code } - ) - } - > - - {asn.name} ({asn.code}) - - ))} - - - -
- - asn.code === - row.original.value - ) - ? row.original.value - : "" - } - onKeyDown={(e) => { - if (e.key === "Enter") { - const value = - e.currentTarget.value - .toUpperCase() - .replace(/^AS/, ""); - if (/^\d+$/.test(value)) { - updateRule( - row.original.ruleId, - { value: "AS" + value } - ); - } - } - }} - className="text-sm" - /> -
-
-
- ) : ( - - updateRule(row.original.ruleId, { - value: e.target.value - }) - } - /> - ) - }, - { - accessorKey: "enabled", - header: () => {t("enabled")}, - cell: ({ row }) => ( - - updateRule(row.original.ruleId, { enabled: val }) - } - /> - ) - }, - { - id: "actions", - header: () => {t("actions")}, - cell: ({ row }) => ( -
- -
- ) - } - ], - [ - t, - RuleAction, - RuleMatch, - isMaxmindAvailable, - isMaxmindAsnAvailable, - updateRule, - removeRule - ] - ); - - const table = useReactTable({ - data: rules, - columns, - getCoreRowModel: getCoreRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getSortedRowModel: getSortedRowModel(), - getFilteredRowModel: getFilteredRowModel(), - state: { pagination: { pageIndex: 0, pageSize: 1000 } } - }); - - if (!isOpen) { - return ( - - - - {t("rulesResource")} - - - {t("rulesResourcePolicyDescription")} - - - - - - - ); - } - - return ( - - - - {t("rulesResource")} - - - {t("rulesResourceDescription")} - - - -
-
- { - setRulesEnabled(val); - form.setValue("applyRules", val); - }} - /> -
- -
- -
- ( - - - {t("rulesAction")} - - - - - - - )} - /> - ( - - - {t("rulesMatchType")} - - - - - - - )} - /> - ( - - - - {addRuleForm.watch("match") === - "COUNTRY" ? ( - - - - - - - - - - {t( - "noCountryFound" - )} - - - {COUNTRIES.map( - ( - country - ) => ( - { - field.onChange( - country.code - ); - setOpenAddRuleCountrySelect( - false - ); - }} - > - - { - country.name - }{" "} - ( - { - country.code - } - - ) - - ) - )} - - - - - - ) : addRuleForm.watch( - "match" - ) === "ASN" ? ( - - - - - - - - - - No ASN - found. - Use the - custom - input - below. - - - {MAJOR_ASNS.map( - ( - asn - ) => ( - { - field.onChange( - asn.code - ); - setOpenAddRuleAsnSelect( - false - ); - }} - > - - { - asn.name - }{" "} - ( - { - asn.code - } - - ) - - ) - )} - - - -
- { - if ( - e.key === - "Enter" - ) { - const value = - e.currentTarget.value - .toUpperCase() - .replace( - /^AS/, - "" - ); - if ( - /^\d+$/.test( - value - ) - ) { - field.onChange( - "AS" + - value - ); - setOpenAddRuleAsnSelect( - false - ); - } - } - }} - className="text-sm" - /> -
-
-
- ) : ( - - )} -
- -
- )} - /> - -
-
- - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - const isActionsColumn = - header.column.id === "actions"; - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column - .columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => { - const isActionsColumn = - cell.column.id === "actions"; - return ( - - {flexRender( - cell.column.columnDef - .cell, - cell.getContext() - )} - - ); - })} - - )) - ) : ( - - - {t("rulesNoOne")} - - - )} - -
-
-
-
- ); -} diff --git a/src/components/resource-policy/CreatePolicyOtpEmailSectionForm.tsx b/src/components/resource-policy/CreatePolicyOtpEmailSectionForm.tsx new file mode 100644 index 000000000..a43dfcc11 --- /dev/null +++ b/src/components/resource-policy/CreatePolicyOtpEmailSectionForm.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; + +import { useTranslations } from "next-intl"; + +import z from "zod"; + +import { type PolicyFormValues } from "."; + +import { SwitchInput } from "@app/components/SwitchInput"; +import { Tag, TagInput } from "@app/components/tags/tag-input"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; +import { Button } from "@app/components/ui/button"; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { InfoPopup } from "@app/components/ui/info-popup"; + +import { InfoIcon, Plus } from "lucide-react"; + +import { useState } from "react"; +import { type UseFormReturn } from "react-hook-form"; + +// ─── CreatePolicyOtpEmailSectionForm ────────────────────────────────────────── + +export type CreatePolicyOtpEmailSectionFormProps = { + form: UseFormReturn; + emailEnabled: boolean; +}; + +export function CreatePolicyOtpEmailSectionForm({ + form, + emailEnabled +}: CreatePolicyOtpEmailSectionFormProps) { + const t = useTranslations(); + const [isOpen, setIsOpen] = useState(false); + const [whitelistEnabled, setWhitelistEnabled] = useState(false); + const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< + number | null + >(null); + + if (!isOpen) { + return ( + + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + + + ); + } + + return ( + + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + {!emailEnabled && ( + + + + {t("otpEmailSmtpRequired")} + + + {t("otpEmailSmtpRequiredDescription")} + + + )} + { + setWhitelistEnabled(val); + form.setValue("emailWhitelistEnabled", val); + }} + disabled={!emailEnabled} + /> + + {whitelistEnabled && emailEnabled && ( + ( + + + + + + {/* @ts-ignore */} + { + return z + .email() + .or( + z + .string() + .regex( + /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, + { + message: t( + "otpEmailErrorInvalid" + ) + } + ) + ) + .safeParse(tag).success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder={t("otpEmailEnter")} + tags={form.getValues().emails} + setTags={(newEmails) => { + form.setValue( + "emails", + newEmails as [Tag, ...Tag[]] + ); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t("otpEmailEnterDescription")} + + + )} + /> + )} + + + + ); +} diff --git a/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx b/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx new file mode 100644 index 000000000..7cb703dad --- /dev/null +++ b/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx @@ -0,0 +1,1073 @@ +"use client"; + +import { + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useTranslations } from "next-intl"; + +import z from "zod"; + +import { type PolicyFormValues } from "."; +import { toast } from "@app/hooks/useToast"; + +import { SwitchInput } from "@app/components/SwitchInput"; +import { Button } from "@app/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { InfoPopup } from "@app/components/ui/info-popup"; +import { Input } from "@app/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { Switch } from "@app/components/ui/switch"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@app/components/ui/table"; + +import { MAJOR_ASNS } from "@server/db/asns"; +import { COUNTRIES } from "@server/db/countries"; +import { + isValidCIDR, + isValidIP, + isValidUrlGlobPattern +} from "@server/lib/validators"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable +} from "@tanstack/react-table"; +import { + ArrowUpDown, + Check, + ChevronsUpDown, + Plus +} from "lucide-react"; + +import { useCallback, useMemo, useState } from "react"; +import { type UseFormReturn, useForm } from "react-hook-form"; + +// ─── CreatePolicyRulesSectionForm ───────────────────────────────────────────── + +const addRuleSchema = z.object({ + action: z.enum(["ACCEPT", "DROP", "PASS"]), + match: z.string(), + value: z.string(), + priority: z.coerce.number().int().optional() +}); + +type LocalRule = { + ruleId: number; + action: "ACCEPT" | "DROP" | "PASS"; + match: string; + value: string; + priority: number; + enabled: boolean; + new?: boolean; + updated?: boolean; +}; + +export type CreatePolicyRulesSectionFormProps = { + form: UseFormReturn; + isMaxmindAvailable: boolean; + isMaxmindAsnAvailable: boolean; +}; + +export function CreatePolicyRulesSectionForm({ + form, + isMaxmindAvailable, + isMaxmindAsnAvailable +}: CreatePolicyRulesSectionFormProps) { + const t = useTranslations(); + const [isOpen, setIsOpen] = useState(false); + const [rules, setRules] = useState([]); + const [rulesEnabled, setRulesEnabled] = useState(false); + const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = + useState(false); + const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false); + + const addRuleForm = useForm({ + resolver: zodResolver(addRuleSchema), + defaultValues: { + action: "ACCEPT" as const, + match: "IP", + value: "" + } + }); + + const RuleAction = useMemo( + () => ({ + ACCEPT: t("alwaysAllow"), + DROP: t("alwaysDeny"), + PASS: t("passToAuth") + }), + [t] + ); + + const RuleMatch = useMemo( + () => ({ + PATH: t("path"), + IP: "IP", + CIDR: t("ipAddressRange"), + COUNTRY: t("country"), + ASN: "ASN" + }), + [t] + ); + + const syncFormRules = useCallback( + (updatedRules: LocalRule[]) => { + form.setValue( + "rules", + updatedRules.map( + ({ action, match, value, priority, enabled }) => ({ + action, + match, + value, + priority, + enabled + }) + ) + ); + }, + [form] + ); + + const addRule = useCallback( + function addRule(data: z.infer) { + const isDuplicate = rules.some( + (rule) => + rule.action === data.action && + rule.match === data.match && + rule.value === data.value + ); + if (isDuplicate) { + toast({ + variant: "destructive", + title: t("rulesErrorDuplicate"), + description: t("rulesErrorDuplicateDescription") + }); + return; + } + if (data.match === "CIDR" && !isValidCIDR(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidIpAddressRange"), + description: t("rulesErrorInvalidIpAddressRangeDescription") + }); + return; + } + if (data.match === "PATH" && !isValidUrlGlobPattern(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidUrl"), + description: t("rulesErrorInvalidUrlDescription") + }); + return; + } + if (data.match === "IP" && !isValidIP(data.value)) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidIpAddress"), + description: t("rulesErrorInvalidIpAddressDescription") + }); + return; + } + if ( + data.match === "COUNTRY" && + !COUNTRIES.some((c) => c.code === data.value) + ) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidCountry"), + description: t("rulesErrorInvalidCountryDescription") || "" + }); + return; + } + + let priority = data.priority; + if (priority === undefined) { + priority = + rules.reduce( + (acc, rule) => + rule.priority > acc ? rule.priority : acc, + 0 + ) + 1; + } + + const updatedRules = [ + ...rules, + { + ...data, + ruleId: new Date().getTime(), + new: true, + priority, + enabled: true + } + ]; + setRules(updatedRules); + syncFormRules(updatedRules); + addRuleForm.reset(); + }, + [rules, t, addRuleForm, syncFormRules] + ); + + const removeRule = useCallback( + function removeRule(ruleId: number) { + const updatedRules = rules.filter((rule) => rule.ruleId !== ruleId); + setRules(updatedRules); + syncFormRules(updatedRules); + }, + [rules, syncFormRules] + ); + + const updateRule = useCallback( + function updateRule(ruleId: number, data: Partial) { + const updatedRules = rules.map((rule) => + rule.ruleId === ruleId + ? { ...rule, ...data, updated: true } + : rule + ); + setRules(updatedRules); + syncFormRules(updatedRules); + }, + [rules, syncFormRules] + ); + + const getValueHelpText = useCallback( + function getValueHelpText(type: string) { + switch (type) { + case "CIDR": + return t("rulesMatchIpAddressRangeDescription"); + case "IP": + return t("rulesMatchIpAddress"); + case "PATH": + return t("rulesMatchUrl"); + case "COUNTRY": + return t("rulesMatchCountry"); + case "ASN": + return "Enter an Autonomous System Number (e.g., AS15169 or 15169)"; + } + }, + [t] + ); + + const columns: ColumnDef[] = useMemo( + () => [ + { + accessorKey: "priority", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + e.currentTarget.focus()} + onBlur={(e) => { + const parsed = z.coerce + .number() + .int() + .optional() + .safeParse(e.target.value); + if (!parsed.success) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidPriority"), + description: t( + "rulesErrorInvalidPriorityDescription" + ) + }); + return; + } + updateRule(row.original.ruleId, { + priority: parsed.data + }); + }} + /> + ) + }, + { + accessorKey: "action", + header: () => {t("rulesAction")}, + cell: ({ row }) => ( + + ) + }, + { + accessorKey: "match", + header: () => ( + {t("rulesMatchType")} + ), + cell: ({ row }) => ( + + ) + }, + { + accessorKey: "value", + header: () => {t("value")}, + cell: ({ row }) => + row.original.match === "COUNTRY" ? ( + + + + + + + + + + {t("noCountryFound")} + + + {COUNTRIES.map((country) => ( + + updateRule( + row.original.ruleId, + { + value: country.code + } + ) + } + > + + {country.name} ( + {country.code}) + + ))} + + + + + + ) : row.original.match === "ASN" ? ( + + + + + + + + + + No ASN found. Enter a custom ASN + below. + + + {MAJOR_ASNS.map((asn) => ( + + updateRule( + row.original.ruleId, + { value: asn.code } + ) + } + > + + {asn.name} ({asn.code}) + + ))} + + + +
+ + asn.code === + row.original.value + ) + ? row.original.value + : "" + } + onKeyDown={(e) => { + if (e.key === "Enter") { + const value = + e.currentTarget.value + .toUpperCase() + .replace(/^AS/, ""); + if (/^\d+$/.test(value)) { + updateRule( + row.original.ruleId, + { value: "AS" + value } + ); + } + } + }} + className="text-sm" + /> +
+
+
+ ) : ( + + updateRule(row.original.ruleId, { + value: e.target.value + }) + } + /> + ) + }, + { + accessorKey: "enabled", + header: () => {t("enabled")}, + cell: ({ row }) => ( + + updateRule(row.original.ruleId, { enabled: val }) + } + /> + ) + }, + { + id: "actions", + header: () => {t("actions")}, + cell: ({ row }) => ( +
+ +
+ ) + } + ], + [ + t, + RuleAction, + RuleMatch, + isMaxmindAvailable, + isMaxmindAsnAvailable, + updateRule, + removeRule + ] + ); + + const table = useReactTable({ + data: rules, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { pagination: { pageIndex: 0, pageSize: 1000 } } + }); + + if (!isOpen) { + return ( + + + + {t("rulesResource")} + + + {t("rulesResourcePolicyDescription")} + + + + + + + ); + } + + return ( + + + + {t("rulesResource")} + + + {t("rulesResourceDescription")} + + + +
+
+ { + setRulesEnabled(val); + form.setValue("applyRules", val); + }} + /> +
+ +
+ +
+ ( + + + {t("rulesAction")} + + + + + + + )} + /> + ( + + + {t("rulesMatchType")} + + + + + + + )} + /> + ( + + + + {addRuleForm.watch("match") === + "COUNTRY" ? ( + + + + + + + + + + {t( + "noCountryFound" + )} + + + {COUNTRIES.map( + ( + country + ) => ( + { + field.onChange( + country.code + ); + setOpenAddRuleCountrySelect( + false + ); + }} + > + + { + country.name + }{" "} + ( + { + country.code + } + + ) + + ) + )} + + + + + + ) : addRuleForm.watch( + "match" + ) === "ASN" ? ( + + + + + + + + + + No ASN + found. + Use the + custom + input + below. + + + {MAJOR_ASNS.map( + ( + asn + ) => ( + { + field.onChange( + asn.code + ); + setOpenAddRuleAsnSelect( + false + ); + }} + > + + { + asn.name + }{" "} + ( + { + asn.code + } + + ) + + ) + )} + + + +
+ { + if ( + e.key === + "Enter" + ) { + const value = + e.currentTarget.value + .toUpperCase() + .replace( + /^AS/, + "" + ); + if ( + /^\d+$/.test( + value + ) + ) { + field.onChange( + "AS" + + value + ); + setOpenAddRuleAsnSelect( + false + ); + } + } + }} + className="text-sm" + /> +
+
+
+ ) : ( + + )} +
+ +
+ )} + /> + +
+
+ + + + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + const isActionsColumn = + header.column.id === "actions"; + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column + .columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => { + const isActionsColumn = + cell.column.id === "actions"; + return ( + + {flexRender( + cell.column.columnDef + .cell, + cell.getContext() + )} + + ); + })} + + )) + ) : ( + + + {t("rulesNoOne")} + + + )} + +
+
+
+
+ ); +} diff --git a/src/components/resource-policy/CreatePolicyUserRolesSectionForm.tsx b/src/components/resource-policy/CreatePolicyUserRolesSectionForm.tsx new file mode 100644 index 000000000..48d8b94f8 --- /dev/null +++ b/src/components/resource-policy/CreatePolicyUserRolesSectionForm.tsx @@ -0,0 +1,224 @@ +"use client"; + +import { + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; + +import { SwitchInput } from "@app/components/SwitchInput"; +import { Tag, TagInput } from "@app/components/tags/tag-input"; +import { + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { type PolicyFormValues } from "."; +import { useTranslations } from "next-intl"; +import { useState } from "react"; +import { type UseFormReturn, useWatch } from "react-hook-form"; + +// ─── CreatePolicyUsersRolesSectionForm ──────────────────────────────────────── + +export type CreatePolicyUsersRolesSectionFormProps = { + form: UseFormReturn; + allRoles: { id: string; text: string }[]; + allUsers: { id: string; text: string }[]; + allIdps: { id: number; text: string }[]; +}; + +export function CreatePolicyUsersRolesSectionForm({ + form, + allRoles, + allUsers, + allIdps +}: CreatePolicyUsersRolesSectionFormProps) { + const t = useTranslations(); + const ssoEnabled = useWatch({ control: form.control, name: "sso" }); + const selectedIdpId = useWatch({ + control: form.control, + name: "skipToIdpId" + }); + const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< + number | null + >(null); + const [activeUsersTagIndex, setActiveUsersTagIndex] = useState< + number | null + >(null); + + return ( + + + + {t("resourceUsersRoles")} + + + {t("resourcePolicyUsersRolesDescription")} + + + + + { + console.log(`form.setValue("sso", ${val})`); + form.setValue("sso", val); + }} + /> + + {ssoEnabled && ( + <> + ( + + {t("roles")} + + { + form.setValue( + "roles", + newRoles as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={true} + autocompleteOptions={allRoles} + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + {t("resourceRoleDescription")} + + + )} + /> + ( + + {t("users")} + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={true} + autocompleteOptions={allUsers} + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + )} + /> + + )} + + {ssoEnabled && allIdps.length > 0 && ( +
+ + +

+ {t("defaultIdentityProviderDescription")} +

+
+ )} +
+
+
+ ); +} From c5fc49b4fae0d15ab0c68434d086947ee3cc89db Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 5 Mar 2026 19:31:19 +0100 Subject: [PATCH 058/771] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreatePolicyAuthMethodsSectionForm.tsx | 30 ++++++++++++------- .../CreatePolicyOtpEmailSectionForm.tsx | 6 ++-- .../CreatePolicyRulesSectionForm.tsx | 6 ++-- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/components/resource-policy/CreatePolicyAuthMethodsSectionForm.tsx b/src/components/resource-policy/CreatePolicyAuthMethodsSectionForm.tsx index fb456e823..f881104cb 100644 --- a/src/components/resource-policy/CreatePolicyAuthMethodsSectionForm.tsx +++ b/src/components/resource-policy/CreatePolicyAuthMethodsSectionForm.tsx @@ -43,10 +43,11 @@ import { InputOTPSlot } from "@app/components/ui/input-otp"; +import { cn } from "@app/lib/cn"; import { Binary, Bot, Key, Plus } from "lucide-react"; import { useState } from "react"; -import { type UseFormReturn, useForm } from "react-hook-form"; +import { type UseFormReturn, useForm, useWatch } from "react-hook-form"; // ─── CreatePolicyAuthMethodsSectionForm ─────────────────────────────────────── @@ -72,14 +73,23 @@ export function CreatePolicyAuthMethodsSectionForm({ form }: CreatePolicyAuthMethodsSectionFormProps) { const t = useTranslations(); - const [isOpen, setIsOpen] = useState(false); + const [isExpanded, setIsExpanded] = useState(false); const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); - const password = form.watch("password"); - const pincode = form.watch("pincode"); - const headerAuth = form.watch("headerAuth"); + const password = useWatch({ + control: form.control, + name: "password" + }); + const pincode = useWatch({ + control: form.control, + name: "pincode" + }); + const headerAuth = useWatch({ + control: form.control, + name: "headerAuth" + }); const passwordForm = useForm({ resolver: zodResolver(setPasswordSchema), @@ -96,7 +106,7 @@ export function CreatePolicyAuthMethodsSectionForm({ defaultValues: { user: "", password: "", extendedCompatibility: true } }); - if (!isOpen) { + if (!isExpanded) { return ( @@ -111,7 +121,7 @@ export function CreatePolicyAuthMethodsSectionForm({ - ); } diff --git a/src/components/resource-policy/CreatePolicyOtpEmailSectionForm.tsx b/src/components/resource-policy/CreatePolicyOtpEmailSectionForm.tsx index ce8ac54b9..fb324cced 100644 --- a/src/components/resource-policy/CreatePolicyOtpEmailSectionForm.tsx +++ b/src/components/resource-policy/CreatePolicyOtpEmailSectionForm.tsx @@ -9,30 +9,31 @@ import { SettingsSectionTitle } from "@app/components/Settings"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslations } from "next-intl"; import z from "zod"; -import { type PolicyFormValues } from "."; +import { createPolicySchema, type PolicyFormValues } from "."; import { SwitchInput } from "@app/components/SwitchInput"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Button } from "@app/components/ui/button"; import { + Form, FormControl, FormDescription, FormField, FormItem, - FormLabel, - FormMessage + FormLabel } from "@app/components/ui/form"; import { InfoPopup } from "@app/components/ui/info-popup"; import { InfoIcon, Plus } from "lucide-react"; -import { useState } from "react"; -import { type UseFormReturn } from "react-hook-form"; +import { useEffect, useState } from "react"; +import { type UseFormReturn, useForm, useWatch } from "react-hook-form"; // ─── CreatePolicyOtpEmailSectionForm ────────────────────────────────────────── @@ -42,16 +43,44 @@ export type CreatePolicyOtpEmailSectionFormProps = { }; export function CreatePolicyOtpEmailSectionForm({ - form, + form: parentForm, emailEnabled }: CreatePolicyOtpEmailSectionFormProps) { const t = useTranslations(); const [isExpanded, setIsExpanded] = useState(false); - const [whitelistEnabled, setWhitelistEnabled] = useState(false); const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< number | null >(null); + const form = useForm({ + resolver: zodResolver( + createPolicySchema.pick({ + emailWhitelistEnabled: true, + emails: true + }) + ), + defaultValues: { + emailWhitelistEnabled: false, + emails: [] + } + }); + + useEffect(() => { + const subscription = form.watch((values) => { + parentForm.setValue( + "emailWhitelistEnabled", + values.emailWhitelistEnabled as boolean + ); + parentForm.setValue("emails", values.emails as [Tag, ...Tag[]]); + }); + return () => subscription.unsubscribe(); + }, [form, parentForm]); + + const whitelistEnabled = useWatch({ + control: form.control, + name: "emailWhitelistEnabled" + }); + if (!isExpanded) { return ( @@ -78,100 +107,107 @@ export function CreatePolicyOtpEmailSectionForm({ } return ( - - - - {t("otpEmailTitle")} - - - {t("otpEmailTitleDescription")} - - - - - {!emailEnabled && ( - - - - {t("otpEmailSmtpRequired")} - - - {t("otpEmailSmtpRequiredDescription")} - - - )} - { - setWhitelistEnabled(val); - form.setValue("emailWhitelistEnabled", val); - }} - disabled={!emailEnabled} - /> - - {whitelistEnabled && emailEnabled && ( - ( - - - - - - {/* @ts-ignore */} - { - return z - .email() - .or( - z - .string() - .regex( - /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, - { - message: t( - "otpEmailErrorInvalid" - ) - } - ) - ) - .safeParse(tag).success; - }} - setActiveTagIndex={ - setActiveEmailTagIndex - } - placeholder={t("otpEmailEnter")} - tags={form.getValues().emails} - setTags={(newEmails) => { - form.setValue( - "emails", - newEmails as [Tag, ...Tag[]] - ); - }} - allowDuplicates={false} - sortTags={true} - /> - - - {t("otpEmailEnterDescription")} - - - )} +
+ + + + {t("otpEmailTitle")} + + + {t("otpEmailTitleDescription")} + + + + + {!emailEnabled && ( + + + + {t("otpEmailSmtpRequired")} + + + {t("otpEmailSmtpRequiredDescription")} + + + )} + { + form.setValue("emailWhitelistEnabled", val); + }} + disabled={!emailEnabled} /> - )} - - - + + {whitelistEnabled && emailEnabled && ( + ( + + + + + + {/* @ts-ignore */} + { + return z + .email() + .or( + z + .string() + .regex( + /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, + { + message: + t( + "otpEmailErrorInvalid" + ) + } + ) + ) + .safeParse(tag).success; + }} + setActiveTagIndex={ + setActiveEmailTagIndex + } + placeholder={t("otpEmailEnter")} + tags={form.getValues().emails} + setTags={(newEmails) => { + form.setValue( + "emails", + newEmails as [ + Tag, + ...Tag[] + ] + ); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t("otpEmailEnterDescription")} + + + )} + /> + )} + + + + ); } diff --git a/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx b/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx index 87152fa09..c8635c5a3 100644 --- a/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx +++ b/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx @@ -13,7 +13,7 @@ import { useTranslations } from "next-intl"; import z from "zod"; -import { type PolicyFormValues } from "."; +import { createPolicySchema, type PolicyFormValues } from "."; import { toast } from "@app/hooks/useToast"; import { SwitchInput } from "@app/components/SwitchInput"; @@ -81,8 +81,8 @@ import { Plus } from "lucide-react"; -import { useCallback, useMemo, useState } from "react"; -import { type UseFormReturn, useForm } from "react-hook-form"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { type UseFormReturn, useForm, useWatch } from "react-hook-form"; // ─── CreatePolicyRulesSectionForm ───────────────────────────────────────────── @@ -111,18 +111,43 @@ export type CreatePolicyRulesSectionFormProps = { }; export function CreatePolicyRulesSectionForm({ - form, + form: parentForm, isMaxmindAvailable, isMaxmindAsnAvailable }: CreatePolicyRulesSectionFormProps) { const t = useTranslations(); const [isExpanded, setIsExpanded] = useState(false); const [rules, setRules] = useState([]); - const [rulesEnabled, setRulesEnabled] = useState(false); const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false); const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = useState(false); + const form = useForm({ + resolver: zodResolver( + createPolicySchema.pick({ + applyRules: true, + rules: true + }) + ), + defaultValues: { + applyRules: false, + rules: [] + } + }); + + useEffect(() => { + const subscription = form.watch((values) => { + parentForm.setValue("applyRules", values.applyRules as boolean); + parentForm.setValue("rules", values.rules as any); + }); + return () => subscription.unsubscribe(); + }, [form, parentForm]); + + const rulesEnabled = useWatch({ + control: form.control, + name: "applyRules" + }); + const addRuleForm = useForm({ resolver: zodResolver(addRuleSchema), defaultValues: { @@ -656,7 +681,6 @@ export function CreatePolicyRulesSectionForm({ label={t("rulesEnable")} defaultChecked={false} onCheckedChange={(val) => { - setRulesEnabled(val); form.setValue("applyRules", val); }} /> diff --git a/src/components/resource-policy/CreatePolicyUserRolesSectionForm.tsx b/src/components/resource-policy/CreatePolicyUserRolesSectionForm.tsx index 48d8b94f8..132363fc1 100644 --- a/src/components/resource-policy/CreatePolicyUserRolesSectionForm.tsx +++ b/src/components/resource-policy/CreatePolicyUserRolesSectionForm.tsx @@ -9,9 +9,11 @@ import { SettingsSectionTitle } from "@app/components/Settings"; +import { zodResolver } from "@hookform/resolvers/zod"; import { SwitchInput } from "@app/components/SwitchInput"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { + Form, FormControl, FormDescription, FormField, @@ -26,10 +28,10 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; -import { type PolicyFormValues } from "."; +import { createPolicySchema, type PolicyFormValues } from "."; import { useTranslations } from "next-intl"; -import { useState } from "react"; -import { type UseFormReturn, useWatch } from "react-hook-form"; +import { useEffect, useState } from "react"; +import { type UseFormReturn, useForm, useWatch } from "react-hook-form"; // ─── CreatePolicyUsersRolesSectionForm ──────────────────────────────────────── @@ -41,12 +43,40 @@ export type CreatePolicyUsersRolesSectionFormProps = { }; export function CreatePolicyUsersRolesSectionForm({ - form, + form: parentForm, allRoles, allUsers, allIdps }: CreatePolicyUsersRolesSectionFormProps) { const t = useTranslations(); + + const form = useForm({ + resolver: zodResolver( + createPolicySchema.pick({ + sso: true, + skipToIdpId: true, + roles: true, + users: true + }) + ), + defaultValues: { + sso: true, + skipToIdpId: null, + roles: [], + users: [] + } + }); + + useEffect(() => { + const subscription = form.watch((values) => { + parentForm.setValue("sso", values.sso as boolean); + parentForm.setValue("skipToIdpId", values.skipToIdpId as number | null); + parentForm.setValue("roles", values.roles as [Tag, ...Tag[]]); + parentForm.setValue("users", values.users as [Tag, ...Tag[]]); + }); + return () => subscription.unsubscribe(); + }, [form, parentForm]); + const ssoEnabled = useWatch({ control: form.control, name: "sso" }); const selectedIdpId = useWatch({ control: form.control, @@ -60,165 +90,168 @@ export function CreatePolicyUsersRolesSectionForm({ >(null); return ( - - - - {t("resourceUsersRoles")} - - - {t("resourcePolicyUsersRolesDescription")} - - - - - { - console.log(`form.setValue("sso", ${val})`); - form.setValue("sso", val); - }} - /> +
+ + + + {t("resourceUsersRoles")} + + + {t("resourcePolicyUsersRolesDescription")} + + + + + { + form.setValue("sso", val); + }} + /> - {ssoEnabled && ( - <> - ( - - {t("roles")} - - { - form.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={true} - autocompleteOptions={allRoles} - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - {t("resourceRoleDescription")} - - - )} - /> - ( - - {t("users")} - - { - form.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={true} - autocompleteOptions={allUsers} - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - )} - /> - - )} + {ssoEnabled && ( + <> + ( + + {t("roles")} + + { + form.setValue( + "roles", + newRoles as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={true} + autocompleteOptions={allRoles} + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + {t("resourceRoleDescription")} + + + )} + /> + ( + + {t("users")} + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={true} + autocompleteOptions={allUsers} + allowDuplicates={false} + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + )} + /> + + )} - {ssoEnabled && allIdps.length > 0 && ( -
- - { + if (value === "none") { + form.setValue("skipToIdpId", null); + } else { + const id = parseInt(value); + form.setValue("skipToIdpId", id); + } + }} + value={ + selectedIdpId + ? selectedIdpId.toString() + : "none" } - }} - value={ - selectedIdpId - ? selectedIdpId.toString() - : "none" - } - > - - - - - - {t("none")} - - {allIdps.map((idp) => ( - - {idp.text} + > + + + + + + {t("none")} - ))} - - -

- {t("defaultIdentityProviderDescription")} -

-
- )} -
-
-
+ {allIdps.map((idp) => ( + + {idp.text} + + ))} + + +

+ {t("defaultIdentityProviderDescription")} +

+ + )} + + + +
); } From 136c3eff0cef5a55a40089935998a34389a2773c Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 5 Mar 2026 19:46:16 +0100 Subject: [PATCH 060/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20padding=20bottom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resource-policy/CreatePolicyRulesSectionForm.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx b/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx index c8635c5a3..b3285b284 100644 --- a/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx +++ b/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx @@ -74,12 +74,7 @@ import { getSortedRowModel, useReactTable } from "@tanstack/react-table"; -import { - ArrowUpDown, - Check, - ChevronsUpDown, - Plus -} from "lucide-react"; +import { ArrowUpDown, Check, ChevronsUpDown, Plus } from "lucide-react"; import { useCallback, useEffect, useMemo, useState } from "react"; import { type UseFormReturn, useForm, useWatch } from "react-hook-form"; @@ -674,8 +669,8 @@ export function CreatePolicyRulesSectionForm({
-
-
+
+
Date: Fri, 6 Mar 2026 04:03:25 +0100 Subject: [PATCH 061/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20show=20list=20of?= =?UTF-8?q?=20resources=20on=20policy=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routers/policy/listResourcePolicies.ts | 54 +++++++++++++++++-- server/routers/resource/types.ts | 13 +++-- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/server/private/routers/policy/listResourcePolicies.ts b/server/private/routers/policy/listResourcePolicies.ts index 72d4ff39d..a10cfb4fb 100644 --- a/server/private/routers/policy/listResourcePolicies.ts +++ b/server/private/routers/policy/listResourcePolicies.ts @@ -11,11 +11,20 @@ * This file is not licensed under the AGPLv3. */ -import { db, resourcePolicies, rolePolicies, userPolicies } from "@server/db"; +import { + db, + resourcePolicies, + resources, + rolePolicies, + userPolicies +} from "@server/db"; import response from "@server/lib/response"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; -import type { ListResourcePoliciesResponse } from "@server/routers/resource/types"; +import type { + ListResourcePoliciesResponse, + ResourcePolicyWithResources +} from "@server/routers/resource/types"; import HttpCode from "@server/types/HttpCode"; import { and, asc, eq, inArray, like, or, sql } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; @@ -191,9 +200,48 @@ export async function listResourcePolicies( countQuery ]); + const attachedResources = + rows.length === 0 + ? [] + : await db + .select({ + resourceId: resources.resourceId, + name: resources.name, + fullDomain: resources.fullDomain, + resourcePolicyId: resources.resourcePolicyId + }) + .from(resources) + .where( + inArray( + resources.resourcePolicyId, + rows.map((row) => row.resourcePolicyId) + ) + ); + const entries: ResourcePolicyWithResources[] = []; + + // avoids TS issues with reduce/never[] + const map = new Map(); + + for (const row of rows) { + let entry = map.get(row.resourcePolicyId); + if (!entry) { + entry = { + ...row, + resources: [] + }; + map.set(row.resourcePolicyId, entry); + } + + entry.resources = attachedResources.filter( + (r) => r.resourcePolicyId === entry?.resourcePolicyId + ); + } + + const policiesList = Array.from(map.values()); + return response(res, { data: { - policies: rows, + policies: policiesList, pagination: { total: totalCount, pageSize, diff --git a/server/routers/resource/types.ts b/server/routers/resource/types.ts index 223154a01..c79e78d69 100644 --- a/server/routers/resource/types.ts +++ b/server/routers/resource/types.ts @@ -1,4 +1,4 @@ -import type { ResourcePolicy } from "@server/db"; +import type { Resource, ResourcePolicy } from "@server/db"; import type { PaginatedResponse } from "@server/types/Pagination"; export type GetMaintenanceInfoResponse = { @@ -12,8 +12,13 @@ export type GetMaintenanceInfoResponse = { maintenanceEstimatedTime: string | null; }; +export type ResourcePolicyWithResources = Pick< + ResourcePolicy, + "resourcePolicyId" | "niceId" | "name" | "orgId" +> & { + resources: Array>; +}; + export type ListResourcePoliciesResponse = PaginatedResponse<{ - policies: Array< - Pick - >; + policies: Array; }>; From dfe42e90168104c7aa144de2323e2068a68b50de Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 6 Mar 2026 04:03:40 +0100 Subject: [PATCH 062/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/private/routers/policy/listResourcePolicies.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/private/routers/policy/listResourcePolicies.ts b/server/private/routers/policy/listResourcePolicies.ts index a10cfb4fb..58a83df04 100644 --- a/server/private/routers/policy/listResourcePolicies.ts +++ b/server/private/routers/policy/listResourcePolicies.ts @@ -217,7 +217,6 @@ export async function listResourcePolicies( rows.map((row) => row.resourcePolicyId) ) ); - const entries: ResourcePolicyWithResources[] = []; // avoids TS issues with reduce/never[] const map = new Map(); From 37ceba6b815cadd0ce2d5b6f9678ab90b0380247 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 6 Mar 2026 04:36:12 +0100 Subject: [PATCH 063/771] =?UTF-8?q?=F0=9F=92=84=20show=20attached=20resour?= =?UTF-8?q?ces=20in=20policy=20list?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 4 +- server/routers/resource/types.ts | 7 +- .../(private)/policies/resource/page.tsx | 1 - src/components/ResourcePoliciesTable.tsx | 84 ++++++++++++++++++- 4 files changed, 91 insertions(+), 5 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index af2eef9cb..72e8baa5e 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -167,6 +167,9 @@ "resourceAdd": "Add Resource", "resourceErrorDelte": "Error deleting resource", "resourcePoliciesTitle": "Manage Resource Policies", + "resourcePoliciesAttachedResourcesColumnTitle": "Attached resources", + "resourcePoliciesAttachedResources": "{count} resource(s)", + "resourcePoliciesAttachedResourcesEmpty": "no resources", "resourcePoliciesDescription": "Create and manage authentication policies to control access to your resources", "resourcePoliciesSearch": "Search policies...", "resourcePoliciesAdd": "Add Policy", @@ -1069,7 +1072,6 @@ "pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.", "overview": "Overview", "home": "Home", - "accessControl": "Access Control", "settings": "Settings", "usersAll": "All Users", "license": "License", diff --git a/server/routers/resource/types.ts b/server/routers/resource/types.ts index c79e78d69..eee70bd35 100644 --- a/server/routers/resource/types.ts +++ b/server/routers/resource/types.ts @@ -12,11 +12,16 @@ export type GetMaintenanceInfoResponse = { maintenanceEstimatedTime: string | null; }; +export type AttachedResource = Pick< + Resource, + "resourceId" | "name" | "fullDomain" +>; + export type ResourcePolicyWithResources = Pick< ResourcePolicy, "resourcePolicyId" | "niceId" | "name" | "orgId" > & { - resources: Array>; + resources: Array; }; export type ListResourcePoliciesResponse = PaginatedResponse<{ diff --git a/src/app/[orgId]/settings/(private)/policies/resource/page.tsx b/src/app/[orgId]/settings/(private)/policies/resource/page.tsx index 3f2ec53b0..a51bbef3a 100644 --- a/src/app/[orgId]/settings/(private)/policies/resource/page.tsx +++ b/src/app/[orgId]/settings/(private)/policies/resource/page.tsx @@ -3,7 +3,6 @@ import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { getCachedOrg } from "@app/lib/api/getCachedOrg"; -import OrgProvider from "@app/providers/OrgProvider"; import type { GetOrgResponse } from "@server/routers/org"; import type { ListResourcePoliciesResponse } from "@server/routers/resource/types"; import type { AxiosResponse } from "axios"; diff --git a/src/components/ResourcePoliciesTable.tsx b/src/components/ResourcePoliciesTable.tsx index 69dee6963..5dfc007df 100644 --- a/src/components/ResourcePoliciesTable.tsx +++ b/src/components/ResourcePoliciesTable.tsx @@ -3,9 +3,17 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { useNavigationContext } from "@app/hooks/useNavigationContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient } from "@app/lib/api"; -import type { ListResourcePoliciesResponse } from "@server/routers/resource/types"; +import type { + AttachedResource, + ListResourcePoliciesResponse +} from "@server/routers/resource/types"; import type { PaginationState } from "@tanstack/react-table"; -import { ArrowRight, MoreHorizontal } from "lucide-react"; +import { + ArrowRight, + ChevronDown, + MoreHorizontal, + Waypoints +} from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -20,6 +28,8 @@ import { DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"; +import type { targets } from "@server/db"; +import type { TargetHealth } from "./ProxyResourcesTable"; type ResourcePolicyRow = ListResourcePoliciesResponse["policies"][number]; @@ -65,6 +75,63 @@ export function ResourcePoliciesTable({ }); }; + function ResourceListCell({ + resources + }: { + resources?: AttachedResource[]; + }) { + if (!resources || resources.length === 0) { + return ( +
+ + + {t("resourcePoliciesAttachedResourcesEmpty")} + +
+ ); + } + + return ( + + + + + + {resources.map((resource) => ( + +
+ {resource.name} +
+ + {resource.fullDomain} + +
+ ))} +
+
+ ); + } + const proxyColumns: ExtendedColumnDef[] = [ { accessorKey: "name", @@ -83,6 +150,19 @@ export function ResourcePoliciesTable({ return {row.original.niceId || "-"}; } }, + { + id: "resources", + accessorKey: "resources", + friendlyName: t("resourcePoliciesAttachedResourcesColumnTitle"), + header: () => ( + + {t("resourcePoliciesAttachedResourcesColumnTitle")} + + ), + cell: ({ row }) => { + return ; + } + }, { id: "actions", enableHiding: false, From bcd6cd99ccc80a5863f80e2af43696a1342c4433 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 6 Mar 2026 04:37:57 +0100 Subject: [PATCH 064/771] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/private/routers/policy/deleteResourcePolicy.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 server/private/routers/policy/deleteResourcePolicy.ts diff --git a/server/private/routers/policy/deleteResourcePolicy.ts b/server/private/routers/policy/deleteResourcePolicy.ts new file mode 100644 index 000000000..e69de29bb From 9b43948fa4882d07ad63b4c9a896882bf9e2dadd Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 6 Mar 2026 22:39:44 +0100 Subject: [PATCH 065/771] =?UTF-8?q?=E2=9C=A8=20=20delete=20resource=20poli?= =?UTF-8?q?cy=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 2 +- server/private/routers/external.ts | 15 +++- .../routers/policy/deleteResourcePolicy.ts | 86 +++++++++++++++++++ server/private/routers/policy/index.ts | 1 + server/routers/resource/deleteResource.ts | 18 ++-- 5 files changed, 109 insertions(+), 13 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 934dbb6da..c20ec303a 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -100,7 +100,7 @@ export const resources = pgTable("resources", { resourceId: serial("resourceId").primaryKey(), resourcePolicyId: integer("resourcePolicyId").references( () => resourcePolicies.resourcePolicyId, - { onDelete: "cascade" } + { onDelete: "set null" } ), resourceGuid: varchar("resourceGuid", { length: 36 }) .unique() diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index e0d8f240f..688f565d4 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -35,7 +35,8 @@ import { verifyUserIsServerAdmin, verifySiteAccess, verifyClientAccess, - verifyLimits + verifyLimits, + verifyResourcePolicyAccess } from "@server/middlewares"; import { ActionsEnum } from "@server/auth/actions"; import { @@ -354,6 +355,18 @@ authenticated.get( policy.listResourcePolicies ); +authenticated.delete( + "/resource-policy/:resourcePolicyId", + verifyResourcePolicyAccess, + verifyValidLicense, + // verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ? + verifyOrgAccess, + verifyLimits, + verifyUserHasAction(ActionsEnum.deleteResourcePolicy), + logActionAudit(ActionsEnum.deleteResourcePolicy), + policy.deleteResourcePolicy +); + authenticated.post( "/org/:orgId/resource-policy", verifyValidLicense, diff --git a/server/private/routers/policy/deleteResourcePolicy.ts b/server/private/routers/policy/deleteResourcePolicy.ts index e69de29bb..bb5efb1f3 100644 --- a/server/private/routers/policy/deleteResourcePolicy.ts +++ b/server/private/routers/policy/deleteResourcePolicy.ts @@ -0,0 +1,86 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { db, resourcePolicies } from "@server/db"; +import response from "@server/lib/response"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; +import HttpCode from "@server/types/HttpCode"; +import { eq } from "drizzle-orm"; +import type { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import z from "zod"; +import { fromError } from "zod-validation-error"; + +// Define Zod schema for request parameters validation +const deleteResourcePolicySchema = z.strictObject({ + resourcePolicyId: z.string().transform(Number).pipe(z.int().positive()) +}); + +registry.registerPath({ + method: "delete", + path: "/resource/{resourceId}", + description: "Delete a resource.", + tags: [OpenAPITags.PublicResource], + request: { + params: deleteResourcePolicySchema + }, + responses: {} +}); + +export async function deleteResourcePolicy( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = deleteResourcePolicySchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { resourcePolicyId } = parsedParams.data; + + const [deletedResource] = await db + .delete(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)) + .returning(); + + if (!deletedResource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource Policy with ID ${resourcePolicyId} not found` + ) + ); + } + + return response(res, { + data: null, + success: true, + error: false, + message: "Resource Policy deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/policy/index.ts b/server/private/routers/policy/index.ts index 88302bcac..1fb73a58c 100644 --- a/server/private/routers/policy/index.ts +++ b/server/private/routers/policy/index.ts @@ -13,3 +13,4 @@ export * from "./createResourcePolicy"; export * from "./listResourcePolicies"; +export * from "./deleteResourcePolicy"; diff --git a/server/routers/resource/deleteResource.ts b/server/routers/resource/deleteResource.ts index e63301867..f69853a90 100644 --- a/server/routers/resource/deleteResource.ts +++ b/server/routers/resource/deleteResource.ts @@ -1,17 +1,13 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { newts, resources, sites, targets } from "@server/db"; -import { eq } from "drizzle-orm"; +import { db, resources, targets } from "@server/db"; import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; -import { addPeer } from "../gerbil/peers"; -import { removeTargets } from "../newt/targets"; -import { getAllowedIps } from "../target/helpers"; import { OpenAPITags, registry } from "@server/openApi"; +import HttpCode from "@server/types/HttpCode"; +import { eq } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; // Define Zod schema for request parameters validation const deleteResourceSchema = z.strictObject({ From 884482ec35cf87a348ee65057c1d0f190f8a5d0c Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 6 Mar 2026 23:57:23 +0100 Subject: [PATCH 066/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20delete=20resource?= =?UTF-8?q?=20policy=20endpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 4 ++ .../routers/policy/deleteResourcePolicy.ts | 6 +- src/components/ResourcePoliciesTable.tsx | 55 +++++++++++++++++-- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 72e8baa5e..a6d2d36c4 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -191,6 +191,8 @@ "notProtected": "Not Protected", "resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.", "resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?", + "resourcePolicyMessageRemove": "Once removed, the resource policy will no longer be accessible. All resources associated with the resource will be unlinked and left without authentication.", + "resourcePolicyQuestionRemove": "Are you sure you want to remove the resource policy from the organization?", "resourceHTTP": "HTTPS Resource", "resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.", "resourceRaw": "Raw TCP/UDP Resource", @@ -231,6 +233,8 @@ "resourceLearnRaw": "Learn how to configure TCP/UDP resources", "resourceBack": "Back to Resources", "resourceGoTo": "Go to Resource", + "resourcePolicyDelete": "Delete Resource Policy", + "resourcePolicyDeleteConfirm": "Confirm Delete Resource Policy", "resourceDelete": "Delete Resource", "resourceDeleteConfirm": "Confirm Delete Resource", "visibility": "Visibility", diff --git a/server/private/routers/policy/deleteResourcePolicy.ts b/server/private/routers/policy/deleteResourcePolicy.ts index bb5efb1f3..d7887e3a0 100644 --- a/server/private/routers/policy/deleteResourcePolicy.ts +++ b/server/private/routers/policy/deleteResourcePolicy.ts @@ -29,9 +29,9 @@ const deleteResourcePolicySchema = z.strictObject({ registry.registerPath({ method: "delete", - path: "/resource/{resourceId}", - description: "Delete a resource.", - tags: [OpenAPITags.PublicResource], + path: "/resource-policy/{resourcePolicyId}", + description: "Delete a resource policy.", + tags: [OpenAPITags.Policy], request: { params: deleteResourcePolicySchema }, diff --git a/src/components/ResourcePoliciesTable.tsx b/src/components/ResourcePoliciesTable.tsx index 5dfc007df..bfdc49724 100644 --- a/src/components/ResourcePoliciesTable.tsx +++ b/src/components/ResourcePoliciesTable.tsx @@ -2,7 +2,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { useNavigationContext } from "@app/hooks/useNavigationContext"; import { toast } from "@app/hooks/useToast"; -import { createApiClient } from "@app/lib/api"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import type { AttachedResource, ListResourcePoliciesResponse @@ -17,7 +17,7 @@ import { import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useTransition } from "react"; +import { useState, useTransition } from "react"; import { useDebouncedCallback } from "use-debounce"; import { Button } from "./ui/button"; import { ControlledDataTable } from "./ui/controlled-data-table"; @@ -28,8 +28,7 @@ import { DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"; -import type { targets } from "@server/db"; -import type { TargetHealth } from "./ProxyResourcesTable"; +import ConfirmDeleteDialog from "./ConfirmDeleteDialog"; type ResourcePolicyRow = ListResourcePoliciesResponse["policies"][number]; @@ -58,6 +57,27 @@ export function ResourcePoliciesTable({ const api = createApiClient({ env }); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [selectedResourcePolicy, setSelectedResourcePolicy] = + useState(null); + + const deleteResourcePolicy = async (resourcePolicyId: number) => { + await api + .delete(`/resource-policy/${resourcePolicyId}`) + .catch((e) => { + console.error(t("resourceErrorDelte"), e); + toast({ + variant: "destructive", + title: t("resourceErrorDelte"), + description: formatAxiosError(e, t("resourceErrorDelte")) + }); + }) + .then(() => { + router.refresh(); + setIsDeleteModalOpen(false); + }); + }; + const [isRefreshing, startTransition] = useTransition(); const [isNavigatingToAddPage, startNavigation] = useTransition(); @@ -191,8 +211,8 @@ export function ResourcePoliciesTable({ { - // setSelectedResource(resourceRow); - // setIsDeleteModalOpen(true); + setSelectedResourcePolicy(policyRow); + setIsDeleteModalOpen(true); }} > @@ -233,6 +253,29 @@ export function ResourcePoliciesTable({ return ( <> + {selectedResourcePolicy && ( + { + setIsDeleteModalOpen(val); + setSelectedResourcePolicy(null); + }} + dialog={ +
+

{t("resourcePolicyQuestionRemove")}

+

{t("resourcePolicyMessageRemove")}

+
+ } + buttonText={t("resourcePolicyDeleteConfirm")} + onConfirm={async () => + deleteResourcePolicy( + selectedResourcePolicy.resourcePolicyId + ) + } + string={selectedResourcePolicy.name} + title={t("resourcePolicyDelete")} + /> + )} Date: Sat, 7 Mar 2026 01:12:10 +0100 Subject: [PATCH 067/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20prevent=20deleting?= =?UTF-8?q?=20resource=20policies=20if=20they=20have=20attached=20resource?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 6 ++++ server/private/routers/external.ts | 23 +++++++------ .../routers/policy/deleteResourcePolicy.ts | 33 +++++++++++++++---- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index c20ec303a..35a1d21a3 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -102,6 +102,12 @@ export const resources = pgTable("resources", { () => resourcePolicies.resourcePolicyId, { onDelete: "set null" } ), + defaultResourcePolicyId: integer("defaultResourcePolicyId").references( + () => resourcePolicies.resourcePolicyId, + { + onDelete: "restrict" + } + ), resourceGuid: varchar("resourceGuid", { length: 36 }) .unique() .notNull() diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 688f565d4..f244a8a10 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -344,6 +344,17 @@ authenticated.get( approval.countApprovals ); +authenticated.delete( + "/resource-policy/:resourcePolicyId", + verifyResourcePolicyAccess, + verifyValidLicense, + // verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ? + verifyLimits, + verifyUserHasAction(ActionsEnum.deleteResourcePolicy), + logActionAudit(ActionsEnum.deleteResourcePolicy), + policy.deleteResourcePolicy +); + authenticated.get( "/org/:orgId/resource-policies", verifyValidLicense, @@ -355,18 +366,6 @@ authenticated.get( policy.listResourcePolicies ); -authenticated.delete( - "/resource-policy/:resourcePolicyId", - verifyResourcePolicyAccess, - verifyValidLicense, - // verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ? - verifyOrgAccess, - verifyLimits, - verifyUserHasAction(ActionsEnum.deleteResourcePolicy), - logActionAudit(ActionsEnum.deleteResourcePolicy), - policy.deleteResourcePolicy -); - authenticated.post( "/org/:orgId/resource-policy", verifyValidLicense, diff --git a/server/private/routers/policy/deleteResourcePolicy.ts b/server/private/routers/policy/deleteResourcePolicy.ts index d7887e3a0..17a9a68f9 100644 --- a/server/private/routers/policy/deleteResourcePolicy.ts +++ b/server/private/routers/policy/deleteResourcePolicy.ts @@ -11,7 +11,7 @@ * This file is not licensed under the AGPLv3. */ -import { db, resourcePolicies } from "@server/db"; +import { db, resourcePolicies, resources } from "@server/db"; import response from "@server/lib/response"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; @@ -56,12 +56,12 @@ export async function deleteResourcePolicy( const { resourcePolicyId } = parsedParams.data; - const [deletedResource] = await db - .delete(resourcePolicies) - .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)) - .returning(); + const [existingResource] = await db + .select() + .from(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)); - if (!deletedResource) { + if (!existingResource) { return next( createHttpError( HttpCode.NOT_FOUND, @@ -70,6 +70,27 @@ export async function deleteResourcePolicy( ); } + const totalAffectedResources = await db.$count( + db + .select() + .from(resources) + .where(eq(resources.resourcePolicyId, resourcePolicyId)) + ); + + if (totalAffectedResources > 0) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + `Cannot delete Policy '${existingResource.name}' as it's being used by at least one resource` + ) + ); + } + + // delete policy + await db + .delete(resourcePolicies) + .where(eq(resourcePolicies.resourcePolicyId, resourcePolicyId)); + return response(res, { data: null, success: true, From 5d956080f2ab2d3dd349e9071294a102c9e6fe0b Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 7 Mar 2026 02:29:36 +0100 Subject: [PATCH 068/771] =?UTF-8?q?=E2=9C=A8=20=20create=20default=20polic?= =?UTF-8?q?y=20when=20creating=20a=20resource?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/resource/createResource.ts | 115 ++++++++++++++++------ 1 file changed, 87 insertions(+), 28 deletions(-) diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 3ff059a28..9b2722d7d 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -6,11 +6,17 @@ import { orgs, Resource, resources, + resourcePolicies, roleResources, + rolePolicies, roles, + userPolicies, userResources } from "@server/db"; -import { getUniqueResourceName } from "@server/db/names"; +import { + getUniqueResourceName, + getUniqueResourcePolicyName +} from "@server/db/names"; import config from "@server/lib/config"; import { validateAndConstructDomain } from "@server/lib/domainUtils"; import response from "@server/lib/response"; @@ -241,8 +247,46 @@ async function createHttpResource( let resource: Resource | undefined; const niceId = await getUniqueResourceName(orgId); + const policyNiceId = await getUniqueResourcePolicyName(orgId); await db.transaction(async (trx) => { + const adminRole = await trx + .select() + .from(roles) + .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) + .limit(1); + + if (adminRole.length === 0) { + return next( + createHttpError(HttpCode.NOT_FOUND, `Admin role not found`) + ); + } + + const [defaultPolicy] = await trx + .insert(resourcePolicies) + .values({ + niceId: policyNiceId, + orgId, + name: `default policy for ${niceId}`, + sso: true, + scope: "resource" + }) + .returning(); + + // make this policy visible by the admin role + await trx.insert(rolePolicies).values({ + roleId: adminRole[0].roleId, + resourcePolicyId: defaultPolicy.resourcePolicyId + }); + + // make this policy visible by the current user + if (req.user && req.userOrgRoleId !== adminRole[0].roleId) { + await trx.insert(userPolicies).values({ + userId: req.user?.userId!, + resourcePolicyId: defaultPolicy.resourcePolicyId + }); + } + const newResource = await trx .insert(resources) .values({ @@ -256,22 +300,11 @@ async function createHttpResource( protocol: "tcp", ssl: true, stickySession: stickySession, - postAuthPath: postAuthPath + postAuthPath: postAuthPath, + defaultResourcePolicyId: defaultPolicy.resourcePolicyId }) .returning(); - const adminRole = await db - .select() - .from(roles) - .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) - .limit(1); - - if (adminRole.length === 0) { - return next( - createHttpError(HttpCode.NOT_FOUND, `Admin role not found`) - ); - } - await trx.insert(roleResources).values({ roleId: adminRole[0].roleId, resourceId: newResource[0].resourceId @@ -338,22 +371,10 @@ async function createRawResource( let resource: Resource | undefined; const niceId = await getUniqueResourceName(orgId); + const policyNiceId = await getUniqueResourcePolicyName(orgId); await db.transaction(async (trx) => { - const newResource = await trx - .insert(resources) - .values({ - niceId, - orgId, - name, - http, - protocol, - proxyPort - // enableProxy - }) - .returning(); - - const adminRole = await db + const adminRole = await trx .select() .from(roles) .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) @@ -365,6 +386,44 @@ async function createRawResource( ); } + const [defaultPolicy] = await trx + .insert(resourcePolicies) + .values({ + niceId: policyNiceId, + orgId, + name: `default policy for ${niceId}`, + sso: true, + scope: "resource" + }) + .returning(); + + // make this policy visible by the admin role + await trx.insert(rolePolicies).values({ + roleId: adminRole[0].roleId, + resourcePolicyId: defaultPolicy.resourcePolicyId + }); + + // make this policy visible by the current user + if (req.user && req.userOrgRoleId != adminRole[0].roleId) { + await trx.insert(userPolicies).values({ + userId: req.user?.userId!, + resourcePolicyId: defaultPolicy.resourcePolicyId + }); + } + + const newResource = await trx + .insert(resources) + .values({ + niceId, + orgId, + name, + http, + protocol, + proxyPort, + defaultResourcePolicyId: defaultPolicy.resourcePolicyId + }) + .returning(); + await trx.insert(roleResources).values({ roleId: adminRole[0].roleId, resourceId: newResource[0].resourceId From 4de4bf9625c8110abbc3a108e305ac0bbd1bd531 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 7 Mar 2026 03:35:26 +0100 Subject: [PATCH 069/771] =?UTF-8?q?=E2=9C=A8=20use=20resource=20policies?= =?UTF-8?q?=20for=20auth=20check?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/resource/listResources.ts | 96 ++++++++++++++---------- 1 file changed, 56 insertions(+), 40 deletions(-) diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index f9dd14e98..4becfb579 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -1,9 +1,9 @@ import { db, - resourceHeaderAuth, - resourceHeaderAuthExtendedCompatibility, - resourcePassword, - resourcePincode, + resourcePolicies, + resourcePolicyHeaderAuth, + resourcePolicyPassword, + resourcePolicyPincode, resources, roleResources, targetHealthCheck, @@ -169,38 +169,54 @@ function queryResourcesBase() { name: resources.name, ssl: resources.ssl, fullDomain: resources.fullDomain, - passwordId: resourcePassword.passwordId, - sso: resources.sso, - pincodeId: resourcePincode.pincodeId, - whitelist: resources.emailWhitelistEnabled, + passwordId: resourcePolicyPassword.passwordId, + sso: resourcePolicies.sso, + pincodeId: resourcePolicyPincode.pincodeId, + whitelist: resourcePolicies.emailWhitelistEnabled, http: resources.http, protocol: resources.protocol, proxyPort: resources.proxyPort, enabled: resources.enabled, domainId: resources.domainId, niceId: resources.niceId, - headerAuthId: resourceHeaderAuth.headerAuthId, - headerAuthExtendedCompatibilityId: - resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId + headerAuthId: resourcePolicyHeaderAuth.headerAuthId, + headerAuthExtendedCompatibility: + resourcePolicyHeaderAuth.extendedCompatibility }) .from(resources) .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) + resourcePolicies, + or( + eq( + resourcePolicies.resourcePolicyId, + resources.resourcePolicyId + ), + eq( + resourcePolicies.resourcePolicyId, + resources.defaultResourcePolicyId + ) + ) ) + .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .leftJoin( - resourceHeaderAuth, - eq(resourceHeaderAuth.resourceId, resources.resourceId) - ) - .leftJoin( - resourceHeaderAuthExtendedCompatibility, + resourcePolicyPassword, eq( - resourceHeaderAuthExtendedCompatibility.resourceId, - resources.resourceId + resourcePolicyPassword.resourcePolicyId, + resourcePolicies.resourcePolicyId + ) + ) + .leftJoin( + resourcePolicyPincode, + eq( + resourcePolicyPincode.resourcePolicyId, + resourcePolicies.resourcePolicyId + ) + ) + .leftJoin( + resourcePolicyHeaderAuth, + eq( + resourcePolicyHeaderAuth.resourcePolicyId, + resourcePolicies.resourcePolicyId ) ) .leftJoin(targets, eq(targets.resourceId, resources.resourceId)) @@ -210,10 +226,10 @@ function queryResourcesBase() { ) .groupBy( resources.resourceId, - resourcePassword.passwordId, - resourcePincode.pincodeId, - resourceHeaderAuth.headerAuthId, - resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId + resourcePolicies.resourcePolicyId, + resourcePolicyPassword.passwordId, + resourcePolicyPincode.pincodeId, + resourcePolicyHeaderAuth.headerAuthId ); } @@ -358,21 +374,21 @@ export async function listResources( case "protected": conditions.push( or( - eq(resources.sso, true), - eq(resources.emailWhitelistEnabled, true), - not(isNull(resourceHeaderAuth.headerAuthId)), - not(isNull(resourcePincode.pincodeId)), - not(isNull(resourcePassword.passwordId)) + eq(resourcePolicies.sso, true), + eq(resourcePolicies.emailWhitelistEnabled, true), + not(isNull(resourcePolicyHeaderAuth.headerAuthId)), + not(isNull(resourcePolicyPincode.pincodeId)), + not(isNull(resourcePolicyPassword.passwordId)) ) ); break; case "not_protected": conditions.push( - not(eq(resources.sso, true)), - not(eq(resources.emailWhitelistEnabled, true)), - isNull(resourceHeaderAuth.headerAuthId), - isNull(resourcePincode.pincodeId), - isNull(resourcePassword.passwordId) + not(eq(resourcePolicies.sso, true)), + not(eq(resourcePolicies.emailWhitelistEnabled, true)), + isNull(resourcePolicyHeaderAuth.headerAuthId), + isNull(resourcePolicyPincode.pincodeId), + isNull(resourcePolicyPassword.passwordId) ); break; } @@ -468,9 +484,9 @@ export async function listResources( ssl: row.ssl, fullDomain: row.fullDomain, passwordId: row.passwordId, - sso: row.sso, + sso: row.sso ?? false, pincodeId: row.pincodeId, - whitelist: row.whitelist, + whitelist: row.whitelist ?? false, http: row.http, protocol: row.protocol, proxyPort: row.proxyPort, From c5f6d822cabd4c9b458bca1d1409484a81066878 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 7 Mar 2026 03:45:10 +0100 Subject: [PATCH 070/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20auth=20?= =?UTF-8?q?info=20to=20use=20resource=20policies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../routers/resource/getResourceAuthInfo.ts | 126 ++++++++---------- 1 file changed, 57 insertions(+), 69 deletions(-) diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index 7def75d5b..2f8b10e0f 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -2,13 +2,13 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db, - resourceHeaderAuth, - resourceHeaderAuthExtendedCompatibility, - resourcePassword, - resourcePincode, + resourcePolicies, + resourcePolicyHeaderAuth, + resourcePolicyPassword, + resourcePolicyPincode, resources } from "@server/db"; -import { eq } from "drizzle-orm"; +import { eq, or } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -58,64 +58,53 @@ export async function getResourceAuthInfo( const isGuidInteger = /^\d+$/.test(resourceGuid); + const buildQuery = (whereClause: ReturnType) => + db + .select() + .from(resources) + .leftJoin( + resourcePolicies, + or( + eq( + resourcePolicies.resourcePolicyId, + resources.resourcePolicyId + ), + eq( + resourcePolicies.resourcePolicyId, + resources.defaultResourcePolicyId + ) + ) + ) + .leftJoin( + resourcePolicyPincode, + eq( + resourcePolicyPincode.resourcePolicyId, + resourcePolicies.resourcePolicyId + ) + ) + .leftJoin( + resourcePolicyPassword, + eq( + resourcePolicyPassword.resourcePolicyId, + resourcePolicies.resourcePolicyId + ) + ) + .leftJoin( + resourcePolicyHeaderAuth, + eq( + resourcePolicyHeaderAuth.resourcePolicyId, + resourcePolicies.resourcePolicyId + ) + ) + .where(whereClause) + .limit(1); + const [result] = isGuidInteger && build === "saas" - ? await db - .select() - .from(resources) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) - ) - - .leftJoin( - resourceHeaderAuth, - eq( - resourceHeaderAuth.resourceId, - resources.resourceId - ) - ) - .leftJoin( - resourceHeaderAuthExtendedCompatibility, - eq( - resourceHeaderAuthExtendedCompatibility.resourceId, - resources.resourceId - ) - ) - .where(eq(resources.resourceId, Number(resourceGuid))) - .limit(1) - : await db - .select() - .from(resources) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) - ) - - .leftJoin( - resourceHeaderAuth, - eq( - resourceHeaderAuth.resourceId, - resources.resourceId - ) - ) - .leftJoin( - resourceHeaderAuthExtendedCompatibility, - eq( - resourceHeaderAuthExtendedCompatibility.resourceId, - resources.resourceId - ) - ) - .where(eq(resources.resourceGuid, resourceGuid)) - .limit(1); + ? await buildQuery( + eq(resources.resourceId, Number(resourceGuid)) + ) + : await buildQuery(eq(resources.resourceGuid, resourceGuid)); const resource = result?.resources; if (!resource) { @@ -124,11 +113,10 @@ export async function getResourceAuthInfo( ); } - const pincode = result?.resourcePincode; - const password = result?.resourcePassword; - const headerAuth = result?.resourceHeaderAuth; - const headerAuthExtendedCompatibility = - result?.resourceHeaderAuthExtendedCompatibility; + const policy = result?.resourcePolicies; + const pincode = result?.resourcePolicyPincode; + const password = result?.resourcePolicyPassword; + const headerAuth = result?.resourcePolicyHeaderAuth; const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; @@ -142,11 +130,11 @@ export async function getResourceAuthInfo( pincode: pincode !== null, headerAuth: headerAuth !== null, headerAuthExtendedCompatibility: - headerAuthExtendedCompatibility !== null, - sso: resource.sso, + headerAuth?.extendedCompatibility ?? false, + sso: policy?.sso ?? false, blockAccess: resource.blockAccess, url, - whitelist: resource.emailWhitelistEnabled, + whitelist: policy?.emailWhitelistEnabled ?? false, skipToIdpId: resource.skipToIdpId, orgId: resource.orgId, postAuthPath: resource.postAuthPath ?? null From 2fa1bc6cdcabf71c4bfb37349939a5224dc55e8b Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 7 Mar 2026 03:55:30 +0100 Subject: [PATCH 071/771] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[orgId]/settings/resources/proxy/[niceId]/layout.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx index f410b4c8b..d2432ae1a 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx @@ -91,10 +91,10 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { title: t("authentication"), href: `/{orgId}/settings/resources/proxy/{niceId}/authentication` }); - navItems.push({ - title: t("rules"), - href: `/{orgId}/settings/resources/proxy/{niceId}/rules` - }); + // navItems.push({ + // title: t("rules"), + // href: `/{orgId}/settings/resources/proxy/{niceId}/rules` + // }); } return ( From 90d6178a0b8d84a9b54ffacbf7cb3c48a87b3631 Mon Sep 17 00:00:00 2001 From: LunarECL Date: Mon, 9 Mar 2026 02:42:21 +0900 Subject: [PATCH 072/771] Skip invalid Docker resources instead of failing entire blueprint (#1784) --- .../blueprints/applyNewtDockerBlueprint.ts | 79 +++++++++++++++++-- 1 file changed, 72 insertions(+), 7 deletions(-) diff --git a/server/lib/blueprints/applyNewtDockerBlueprint.ts b/server/lib/blueprints/applyNewtDockerBlueprint.ts index eb5fc7877..66686bcee 100644 --- a/server/lib/blueprints/applyNewtDockerBlueprint.ts +++ b/server/lib/blueprints/applyNewtDockerBlueprint.ts @@ -1,10 +1,56 @@ import { sendToClient } from "#dynamic/routers/ws"; import { processContainerLabels } from "./parseDockerContainers"; import { applyBlueprint } from "./applyBlueprint"; +import { ResourceSchema, ClientResourceSchema } from "./types"; import { db, sites } from "@server/db"; import { eq } from "drizzle-orm"; import logger from "@server/logger"; +type BlueprintResult = ReturnType; + +function filterInvalidResources(blueprint: BlueprintResult): { + skippedCount: number; + skippedKeys: string[]; +} { + const skippedKeys: string[] = []; + + for (const section of ["proxy-resources", "public-resources"] as const) { + const resources = blueprint[section]; + for (const [key, value] of Object.entries(resources)) { + const result = ResourceSchema.safeParse(value); + if (!result.success) { + const errors = result.error.issues + .map((i) => `${i.path.join(".")}: ${i.message}`) + .join("; "); + logger.warn( + `Skipping invalid Docker ${section} "${key}": ${errors}` + ); + delete resources[key]; + skippedKeys.push(`${section}.${key}`); + } + } + } + + for (const section of ["client-resources", "private-resources"] as const) { + const resources = blueprint[section]; + for (const [key, value] of Object.entries(resources)) { + const result = ClientResourceSchema.safeParse(value); + if (!result.success) { + const errors = result.error.issues + .map((i) => `${i.path.join(".")}: ${i.message}`) + .join("; "); + logger.warn( + `Skipping invalid Docker ${section} "${key}": ${errors}` + ); + delete resources[key]; + skippedKeys.push(`${section}.${key}`); + } + } + } + + return { skippedCount: skippedKeys.length, skippedKeys }; +} + export async function applyNewtDockerBlueprint( siteId: number, newtId: string, @@ -21,17 +67,24 @@ export async function applyNewtDockerBlueprint( return; } - // logger.debug(`Applying Docker blueprint to site: ${siteId}`); - // logger.debug(`Containers: ${JSON.stringify(containers, null, 2)}`); + let skippedCount = 0; + let skippedKeys: string[] = []; try { const blueprint = processContainerLabels(containers); - logger.debug(`Received Docker blueprint: ${JSON.stringify(blueprint)}`); + logger.debug( + `Received Docker blueprint with ${Object.keys(blueprint["proxy-resources"]).length} proxy, ${Object.keys(blueprint["client-resources"]).length} client resource(s)` + ); - // make sure this is not an empty object - if (isEmptyObject(blueprint)) { - return; + const filterResult = filterInvalidResources(blueprint); + skippedCount = filterResult.skippedCount; + skippedKeys = filterResult.skippedKeys; + + if (skippedCount > 0) { + logger.warn( + `Filtered ${skippedCount} invalid resource(s) from Docker blueprint: ${skippedKeys.join(", ")}` + ); } if ( @@ -40,6 +93,15 @@ export async function applyNewtDockerBlueprint( isEmptyObject(blueprint["public-resources"]) && isEmptyObject(blueprint["private-resources"]) ) { + if (skippedCount > 0) { + await sendToClient(newtId, { + type: "newt/blueprint/results", + data: { + success: false, + message: `All resources were invalid and skipped: ${skippedKeys.join(", ")}` + } + }); + } return; } @@ -66,7 +128,10 @@ export async function applyNewtDockerBlueprint( type: "newt/blueprint/results", data: { success: true, - message: "Config updated successfully" + message: + skippedCount > 0 + ? `Config updated successfully. Skipped ${skippedCount} invalid resource(s): ${skippedKeys.join(", ")}` + : "Config updated successfully" } }); } From 79636cbb3052c0deec193b10f4d8f8d50655b31b Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 10 Mar 2026 17:38:19 +0100 Subject: [PATCH 073/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20delete=20default?= =?UTF-8?q?=20resource=20policy=20ID=20when=20deleting=20a=20resource?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/resource/deleteResource.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/server/routers/resource/deleteResource.ts b/server/routers/resource/deleteResource.ts index f69853a90..da673a944 100644 --- a/server/routers/resource/deleteResource.ts +++ b/server/routers/resource/deleteResource.ts @@ -1,4 +1,4 @@ -import { db, resources, targets } from "@server/db"; +import { db, resourcePolicies, resources, targets } from "@server/db"; import response from "@server/lib/response"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; @@ -62,6 +62,18 @@ export async function deleteResource( ); } + // Also delete default resource policy + if (deletedResource.defaultResourcePolicyId) { + await db + .delete(resourcePolicies) + .where( + eq( + resourcePolicies.resourcePolicyId, + deletedResource.defaultResourcePolicyId + ) + ); + } + // const [site] = await db // .select() // .from(sites) From 6686de67881c2e56aa945268bf28128d31296345 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 10 Mar 2026 17:48:17 +0100 Subject: [PATCH 074/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/resources/proxy/create/page.tsx | 21 ++++++++----- src/components/ProxyResourcesTable.tsx | 31 +++++++++---------- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/src/app/[orgId]/settings/resources/proxy/create/page.tsx b/src/app/[orgId]/settings/resources/proxy/create/page.tsx index ff51a311b..3374d71b4 100644 --- a/src/app/[orgId]/settings/resources/proxy/create/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/create/page.tsx @@ -89,7 +89,13 @@ import { useTranslations } from "next-intl"; import Link from "next/link"; import { useParams, useRouter } from "next/navigation"; import { toASCII } from "punycode"; -import { useEffect, useMemo, useState, useCallback } from "react"; +import { + useMemo, + useState, + useCallback, + useTransition, + useEffect +} from "react"; import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; @@ -210,7 +216,7 @@ export default function Page() { orgQueries.sites({ orgId: orgId as string }) ); - const [createLoading, setCreateLoading] = useState(false); + const [createLoading, startTransition] = useTransition(); const [showSnippets, setShowSnippets] = useState(false); const [niceId, setNiceId] = useState(""); @@ -293,7 +299,10 @@ export default function Page() { { id: "raw" as ResourceType, title: t("resourceRaw"), - description: build == "saas" ? t("resourceRawDescriptionCloud") : t("resourceRawDescription") + description: + build == "saas" + ? t("resourceRawDescriptionCloud") + : t("resourceRawDescription") } ]) ]; @@ -432,8 +441,6 @@ export default function Page() { ); async function onSubmit() { - setCreateLoading(true); - const baseData = baseForm.getValues(); const isHttp = baseData.http; @@ -562,8 +569,6 @@ export default function Page() { description: t("resourceErrorCreateMessageDescription") }); } - - setCreateLoading(false); } useEffect(() => { @@ -1394,7 +1399,7 @@ export default function Page() { console.log(httpForm.getValues()); if (baseValid && settingsValid) { - onSubmit(); + startTransition(onSubmit); } }} loading={createLoading} diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index 353eddb50..b3db7a750 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -174,22 +174,17 @@ export default function ProxyResourcesTable({ }); }; - const deleteResource = (resourceId: number) => { - api.delete(`/resource/${resourceId}`) - .catch((e) => { - console.error(t("resourceErrorDelte"), e); - toast({ - variant: "destructive", - title: t("resourceErrorDelte"), - description: formatAxiosError(e, t("resourceErrorDelte")) - }); - }) - .then(() => { - startTransition(() => { - router.refresh(); - setIsDeleteModalOpen(false); - }); + const deleteResource = async (resourceId: number) => { + await api.delete(`/resource/${resourceId}`).catch((e) => { + console.error(t("resourceErrorDelte"), e); + toast({ + variant: "destructive", + title: t("resourceErrorDelte"), + description: formatAxiosError(e, t("resourceErrorDelte")) }); + }); + router.refresh(); + setIsDeleteModalOpen(false); }; async function toggleResourceEnabled(val: boolean, resourceId: number) { @@ -626,7 +621,11 @@ export default function ProxyResourcesTable({
} buttonText={t("resourceDeleteConfirm")} - onConfirm={async () => deleteResource(selectedResource!.id)} + onConfirm={async () => + startTransition(() => + deleteResource(selectedResource!.id) + ) + } string={selectedResource.name} title={t("resourceDelete")} /> From 61ec938b0012404b70a912eb08bbbd671fdf6084 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 10 Mar 2026 18:54:26 +0100 Subject: [PATCH 075/771] =?UTF-8?q?=F0=9F=9A=A7=20WIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 6 + server/auth/actions.ts | 3 +- server/routers/external.ts | 7 + server/routers/integration.ts | 7 + server/routers/policy/getResourcePolicy.ts | 8 +- .../routers/resource/getResourcePolicies.ts | 88 +++++ server/routers/resource/index.ts | 1 + solo.yml | 3 + .../proxy/[niceId]/authentication/page.tsx | 352 +++--------------- src/components/StrategySelect.tsx | 16 +- src/lib/queries.ts | 12 + 11 files changed, 191 insertions(+), 312 deletions(-) create mode 100644 server/routers/resource/getResourcePolicies.ts create mode 100644 solo.yml diff --git a/messages/en-US.json b/messages/en-US.json index a6d2d36c4..106fc400a 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -766,6 +766,12 @@ "resourcePincodeSetupTitle": "Set Pincode", "resourcePincodeSetupTitleDescription": "Set a pincode to protect this resource", "resourceRoleDescription": "Admins can always access this resource.", + "resourcePolicySelectTitle": "Resource Access Policy", + "resourcePolicySelectDescription": "Select the resource policy type for authentication", + "resourcePolicyInline": "Inline Resource Policy", + "resourcePolicyInlineDescription": "Access Policy scoped to only this resource", + "resourcePolicyShared": "Shared Resource Policy", + "resourcePolicySharedDescription": "Access Policy shared accross multiple resources", "resourceUsersRoles": "Access Controls", "resourceUsersRolesDescription": "Configure which users and roles can visit this resource", "resourceUsersRolesSubmit": "Save Access Controls", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index b34b3fe57..5549380ab 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -146,7 +146,8 @@ export enum ActionsEnum { setResourcePolicyPincode = "setResourcePolicyPincode", setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth", setResourcePolicyWhitelist = "setResourcePolicyWhitelist", - setResourcePolicyRules = "setResourcePolicyRules" + setResourcePolicyRules = "setResourcePolicyRules", + getResourcePolicies = "getResourcePolicies" } export async function checkUserActionPermission( diff --git a/server/routers/external.ts b/server/routers/external.ts index 671bd4ac7..130ebbbb4 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -636,6 +636,13 @@ authenticated.get( policy.getResourcePolicy ); +authenticated.get( + "/resource/:resourceId/policies", + verifyResourceAccess, + verifyUserHasAction(ActionsEnum.getResourcePolicies), + resource.getResourcePolicies +); + authenticated.put( "/resource-policy/:resourcePolicyId", verifyResourcePolicyAccess, diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 92f1531ee..cadda13cb 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -453,6 +453,13 @@ authenticated.get( policy.getResourcePolicy ); +authenticated.get( + "/resource/:resourceId/policies", + verifyApiKeyResourceAccess, + verifyApiKeyHasAction(ActionsEnum.getResourcePolicies), + resource.getResourcePolicies +); + authenticated.post( "/resource/:resourceId", verifyApiKeyResourceAccess, diff --git a/server/routers/policy/getResourcePolicy.ts b/server/routers/policy/getResourcePolicy.ts index 2a975dde7..d7513d58d 100644 --- a/server/routers/policy/getResourcePolicy.ts +++ b/server/routers/policy/getResourcePolicy.ts @@ -40,7 +40,9 @@ const getResourcePolicySchema = z }) ); -async function query(params: z.infer) { +export async function queryResourcePolicy( + params: z.infer +) { const conditions: SQL[] = []; if ("resourcePolicyId" in params) { conditions.push( @@ -158,7 +160,7 @@ async function query(params: z.infer) { } export type GetResourcePolicyResponse = NonNullable< - Awaited> + Awaited> >; registry.registerPath({ @@ -205,7 +207,7 @@ export async function getResourcePolicy( ); } - const policy = await query(parsedParams.data); + const policy = await queryResourcePolicy(parsedParams.data); if (!policy) { return next( diff --git a/server/routers/resource/getResourcePolicies.ts b/server/routers/resource/getResourcePolicies.ts new file mode 100644 index 000000000..c51e6f1fd --- /dev/null +++ b/server/routers/resource/getResourcePolicies.ts @@ -0,0 +1,88 @@ +import { db, resources } from "@server/db"; +import { + queryResourcePolicy, + type GetResourcePolicyResponse +} from "@server/routers/policy/getResourcePolicy"; +import response from "@server/lib/response"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; +import HttpCode from "@server/types/HttpCode"; +import { eq } from "drizzle-orm"; +import type { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import z from "zod"; +import { fromError } from "zod-validation-error"; + +const getResourcePoliciesParamsSchema = z.strictObject({ + resourceId: z.string().transform(Number).pipe(z.int().positive()) +}); + +export type GetResourcePoliciesResponse = { + defaultPolicy: GetResourcePolicyResponse | null; +}; + +registry.registerPath({ + method: "get", + path: "/resource/{resourceId}/policies", + description: "Get the default policy for a resource.", + tags: [OpenAPITags.PublicResource, OpenAPITags.Policy], + request: { + params: getResourcePoliciesParamsSchema + }, + responses: {} +}); + +export async function getResourcePolicies( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = getResourcePoliciesParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { resourceId } = parsedParams.data; + + const [resource] = await db + .select({ + defaultResourcePolicyId: resources.defaultResourcePolicyId + }) + .from(resources) + .where(eq(resources.resourceId, resourceId)) + .limit(1); + + if (!resource) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Resource not found") + ); + } + + const defaultPolicy = resource.defaultResourcePolicyId + ? await queryResourcePolicy({ + resourcePolicyId: resource.defaultResourcePolicyId + }) + : null; + + return response(res, { + data: { defaultPolicy }, + success: true, + error: false, + message: "Resource policies retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 3ada13d85..78803105f 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -31,3 +31,4 @@ export * from "./addUserToResource"; export * from "./removeUserFromResource"; export * from "./listAllResourceNames"; export * from "./removeEmailFromResourceWhitelist"; +export * from "./getResourcePolicies"; diff --git a/solo.yml b/solo.yml new file mode 100644 index 000000000..1a6f9f331 --- /dev/null +++ b/solo.yml @@ -0,0 +1,3 @@ +name: pangolin +icon: public/logo/pangolin_profile_picture.png +processes: {} diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index a533fb6c3..e2e315d35 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -12,6 +12,10 @@ import { SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; +import { + StrategySelect, + type StrategyOption +} from "@app/components/StrategySelect"; import { SwitchInput } from "@app/components/SwitchInput"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; @@ -42,6 +46,7 @@ import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { orgQueries, resourceQueries } from "@app/lib/queries"; +import { ResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; import { zodResolver } from "@hookform/resolvers/zod"; import { build } from "@server/build"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; @@ -86,6 +91,8 @@ const whitelistSchema = z.object({ ) }); +type ResourcePolicyType = StrategyOption<"inline" | "shared">; + export default function ResourceAuthenticationPage() { const { org } = useOrgContext(); const { resource, updateResource, authInfo, updateAuthInfo } = @@ -118,6 +125,11 @@ export default function ResourceAuthenticationPage() { resourceId: resource.resourceId }) ); + const { data: policies, isLoading: isLoadingPolicies } = useQuery( + resourceQueries.policies({ + resourceId: resource.resourceId + }) + ); const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery( orgQueries.roles({ @@ -142,7 +154,8 @@ export default function ResourceAuthenticationPage() { isLoadingResourceRoles || isLoadingResourceUsers || isLoadingWhiteList || - isLoadingOrgIdps; + isLoadingOrgIdps || + isLoadingPolicies; const allRoles = useMemo(() => { return orgRoles @@ -413,6 +426,22 @@ export default function ResourceAuthenticationPage() { .finally(() => setLoadingRemoveResourceHeaderAuth(false)); } + const resourcePolicyTypes: Array = [ + { + id: "inline", + title: t("resourcePolicyInline"), + description: t("resourcePolicyInlineDescription") + }, + { + id: "shared", + title: t("resourcePolicyShared"), + description: t("resourcePolicySharedDescription") + } + ]; + + const [selectedResourceType, setSelectedResourceType] = + useState("inline"); + if (pageLoading) { return <>; } @@ -465,324 +494,39 @@ export default function ResourceAuthenticationPage() { - {t("resourceUsersRoles")} + {t("resourcePolicySelectTitle")} - {t("resourceUsersRolesDescription")} + {t("resourcePolicySelectDescription")} - - setSsoEnabled(val)} - /> - -
- - {ssoEnabled && ( - <> - ( - - - {t("roles")} - - - { - usersRolesForm.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allRoles - } - allowDuplicates={ - false - } - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - {t( - "resourceRoleDescription" - )} - - - )} - /> - ( - - - {t("users")} - - - { - usersRolesForm.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allUsers - } - allowDuplicates={ - false - } - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - )} - /> - - )} - - {ssoEnabled && allIdps.length > 0 && ( -
- - -

- {t( - "defaultIdentityProviderDescription" - )} -

-
- )} - - -
+ { + // baseForm.setValue( + // "http", + // value === "http" + // ); + // // Update method default when switching resource type + }} + cols={2} + />
+ {/* - - - - {t("resourceAuthMethods")} - - - {t("resourceAuthMethodsDescriptions")} - - - - - {/* Password Protection */} -
-
- - - {t("resourcePasswordProtection", { - status: authInfo.password - ? t("enabled") - : t("disabled") - })} - -
- -
- - {/* PIN Code Protection */} -
-
- - - {t("resourcePincodeProtection", { - status: authInfo.pincode - ? t("enabled") - : t("disabled") - })} - -
- -
- - {/* Header Authentication Protection */} -
-
- - - {authInfo.headerAuth - ? t( - "resourceHeaderAuthProtectionEnabled" - ) - : t( - "resourceHeaderAuthProtectionDisabled" - )} - -
- -
-
-
-
- - +
*/} ); diff --git a/src/components/StrategySelect.tsx b/src/components/StrategySelect.tsx index 7f747360f..b4cd961d4 100644 --- a/src/components/StrategySelect.tsx +++ b/src/components/StrategySelect.tsx @@ -25,11 +25,15 @@ export function StrategySelect({ value: controlledValue, defaultValue, onChange, - cols + cols = 1 }: StrategySelectProps) { - const [uncontrolledSelected, setUncontrolledSelected] = useState(defaultValue); + const [uncontrolledSelected, setUncontrolledSelected] = useState< + TValue | undefined + >(defaultValue); const isControlled = controlledValue !== undefined; - const selected = isControlled ? (controlledValue ?? undefined) : uncontrolledSelected; + const selected = isControlled + ? (controlledValue ?? undefined) + : uncontrolledSelected; return ( ({ if (!isControlled) setUncontrolledSelected(typedValue); onChange?.(typedValue); }} - className={`grid md:grid-cols-${cols ? cols : 1} gap-4`} + style={{ + // @ts-expect-error + "--cols": `repeat(${cols}, 1fr)` + }} + className="grid md:grid-cols-(--cols) gap-4" > {options.map((option: StrategyOption) => (
@@ -741,6 +757,7 @@ export function EditPolicyRulesSectionForm({ ) : ( - + )} @@ -1053,7 +1073,7 @@ export function EditPolicyRulesSectionForm({ @@ -1134,7 +1154,7 @@ export function EditPolicyRulesSectionForm({ diff --git a/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx index d4d9b2de2..29fe486be 100644 --- a/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx @@ -53,12 +53,14 @@ type PolicyUsersRolesSectionProps = { allRoles: { id: string; text: string }[]; allUsers: { id: string; text: string }[]; allIdps: { id: number; text: string }[]; + readonly?: boolean; }; export function EditPolicyUsersRolesSectionForm({ allRoles, allUsers, - allIdps + allIdps, + readonly }: PolicyUsersRolesSectionProps) { const t = useTranslations(); @@ -106,6 +108,8 @@ export function EditPolicyUsersRolesSectionForm({ const [, formAction, isSubmitting] = useActionState(onSubmit, null); async function onSubmit() { + if (readonly) return; + const isValid = await form.trigger(); if (!isValid) return; @@ -172,6 +176,7 @@ export function EditPolicyUsersRolesSectionForm({ console.log(`form.setValue("sso", ${val})`); form.setValue("sso", val); }} + disabled={readonly} /> {ssoEnabled && ( @@ -221,6 +226,7 @@ export function EditPolicyUsersRolesSectionForm({ true } sortTags={true} + disabled={readonly} /> @@ -277,6 +283,7 @@ export function EditPolicyUsersRolesSectionForm({ true } sortTags={true} + disabled={readonly} /> @@ -292,6 +299,7 @@ export function EditPolicyUsersRolesSectionForm({ {t("defaultIdentityProvider")} + )} From b286096c7b23faa262f7bb0f5ffe92c21e050fcc Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 11 Mar 2026 03:47:31 +0100 Subject: [PATCH 080/771] =?UTF-8?q?=F0=9F=8C=90=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/messages/en-US.json b/messages/en-US.json index 106fc400a..7bd6b9c51 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -731,6 +731,11 @@ "pincodeAdd": "Add PIN Code", "pincodeRemove": "Remove PIN Code", "resourceAuthMethods": "Authentication Methods", + "resourcePolicyAuthMethodsEmpty": "No authentication method", + "resourcePolicyOtpEmpty": "No one time password", + "resourcePolicyTypeSave": "Save Resource type", + "resourcePolicySelect": "Select resource policy", + "resourcePolicyRulesEmpty": "No authentication rules", "resourceAuthMethodsDescriptions": "Allow access to the resource via additional auth methods", "resourceAuthSettingsSave": "Saved successfully", "resourceAuthSettingsSaveDescription": "Authentication settings have been saved", From 304ab1964cfd3c1dab4edfbc0263da3077b68675 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 11 Mar 2026 04:21:55 +0100 Subject: [PATCH 081/771] =?UTF-8?q?=F0=9F=9A=A7=20=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../proxy/[niceId]/authentication/page.tsx | 202 ++++++++++++------ src/components/Settings.tsx | 11 +- src/lib/queries.ts | 38 +++- 3 files changed, 184 insertions(+), 67 deletions(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index 9609d9da1..414133c86 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -21,6 +21,14 @@ import { SwitchInput } from "@app/components/SwitchInput"; import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Button } from "@app/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; import { Form, FormControl, @@ -31,6 +39,11 @@ import { FormMessage } from "@app/components/ui/form"; import { InfoPopup } from "@app/components/ui/info-popup"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; import { Select, SelectContent, @@ -45,19 +58,25 @@ import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { useResourceContext } from "@app/hooks/useResourceContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { cn } from "@app/lib/cn"; import { getUserDisplayName } from "@app/lib/getUserDisplayName"; -import { orgQueries, resourceQueries } from "@app/lib/queries"; +import { + orgQueries, + resourcePolicyQueries, + resourceQueries +} from "@app/lib/queries"; import { ResourcePolicyContext, ResourcePolicyProvider } from "@app/providers/ResourcePolicyProvider"; import { zodResolver } from "@hookform/resolvers/zod"; +import { CaretSortIcon } from "@radix-ui/react-icons"; import { build } from "@server/build"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { UserType } from "@server/types/UserTypes"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import SetResourcePasswordForm from "components/SetResourcePasswordForm"; -import { Binary, Bot, InfoIcon, Key } from "lucide-react"; +import { Binary, Bot, CheckIcon, InfoIcon, Key } from "lucide-react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; import { @@ -103,6 +122,41 @@ export default function ResourceAuthenticationPage() { }) ); + const form = useForm({ + resolver: zodResolver(resourceTypeSchema), + defaultValues: { + type: "inline" + } + }); + + const selectedResourceType = useWatch({ + control: form.control, + name: "type" + }); + + const [resourcePolicysearchQuery, setResourcePolicySearchQuery] = + useState(""); + + const { data: policiesList = [] } = useQuery({ + ...orgQueries.policies({ + orgId: org.org.orgId, + name: resourcePolicysearchQuery + }), + enabled: selectedResourceType === "shared" + }); + + const { data: sharedPolicy } = useQuery({ + ...resourcePolicyQueries.single({ + resourcePolicyId: resource.resourcePolicyId ?? 1 + }), + enabled: !!resource.resourcePolicyId + }); + + const [selectedPolicy, setSelectedPolicy] = useState<{ + name: string; + id: number; + } | null>(null); + const pageLoading = isLoadingPolicies || !defaultPolicy; const [ @@ -127,66 +181,12 @@ export default function ResourceAuthenticationPage() { } ]; - const form = useForm({ - resolver: zodResolver(resourceTypeSchema), - defaultValues: { - type: "inline" - } - }); - - const selectedResourceType = useWatch({ - control: form.control, - name: "type" - }); - if (pageLoading) { return <>; } return ( <> - {isSetPasswordOpen && ( - { - setIsSetPasswordOpen(false); - updateAuthInfo({ - password: true - }); - }} - /> - )} - - {isSetPincodeOpen && ( - { - setIsSetPincodeOpen(false); - updateAuthInfo({ - pincode: true - }); - }} - /> - )} - - {isSetHeaderAuthOpen && ( - { - setIsSetHeaderAuthOpen(false); - updateAuthInfo({ - headerAuth: true - }); - }} - /> - )} - @@ -206,20 +206,94 @@ export default function ResourceAuthenticationPage() { }} cols={2} /> + {selectedResourceType === "shared" && ( + + + + + + + + + + {t("siteNotFound")} + + + {policiesList.map((policy) => ( + + setSelectedPolicy({ + id: policy.resourcePolicyId, + name: policy.name + }) + } + > + + {policy.name} + + ))} + + + + + + )} - + - - - + {selectedResourceType === "inline" ? ( + + + + ) : ( + sharedPolicy && ( + + + + ) + )} ); diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 194d8b467..f53ddf9d0 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -61,12 +61,19 @@ export function SettingsSectionBody({ } export function SettingsSectionFooter({ - children + children, + className }: { children: React.ReactNode; + className?: string; }) { return ( -
+
{children}
); diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 8f5ccab1c..022239006 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -30,6 +30,7 @@ import z from "zod"; import { remote } from "./api"; import { durationToMs } from "./durationToMs"; import { wait } from "./wait"; +import type { ListResourcePoliciesResponse } from "@server/routers/resource/types"; export type ProductUpdate = { link: string | null; @@ -196,6 +197,41 @@ export const orgQueries = { return res.data.data.resources; } + }), + policies: ({ orgId, name }: { orgId: string; name?: string }) => + queryOptions({ + queryKey: ["ORG", orgId, "RESOURCES_POLICIES", name] as const, + queryFn: async ({ signal, meta }) => { + const sp = new URLSearchParams({ + pageSize: "10" + }); + + if (name) { + sp.set("query", name); + } + + const res = await meta!.api.get< + AxiosResponse + >(`/org/${orgId}/resource-policies?${sp.toString()}`, { + signal + }); + + return res.data.data.policies; + } + }) +}; + +export const resourcePolicyQueries = { + single: ({ resourcePolicyId }: { resourcePolicyId: number }) => + queryOptions({ + queryKey: ["RESOURCE_POLICIES", resourcePolicyId] as const, + queryFn: async ({ signal, meta }) => { + const res = await meta!.api.get< + AxiosResponse + >(`/resource-policy/${resourcePolicyId}`, { signal }); + + return res.data.data; + } }) }; @@ -325,7 +361,7 @@ export const resourceQueries = { }), defaultPolicy: ({ resourceId }: { resourceId: number }) => queryOptions({ - queryKey: ["RESOURCES", resourceId, "POLICIES"] as const, + queryKey: ["RESOURCES", resourceId, "DEFAULT_POLICY"] as const, queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< AxiosResponse From 36bcba332c30f9711e483381539df2c97b305c1a Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 11 Mar 2026 05:18:22 +0100 Subject: [PATCH 082/771] =?UTF-8?q?=F0=9F=9A=A7=20=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + server/routers/external.ts | 4 +- server/routers/integration.ts | 4 +- ...sourcePolicy.ts => getResourcePolicies.ts} | 37 ++++++--- server/routers/resource/index.ts | 2 +- .../proxy/[niceId]/authentication/page.tsx | 75 +++++++++++++------ src/lib/queries.ts | 9 ++- 7 files changed, 90 insertions(+), 42 deletions(-) rename server/routers/resource/{getDefaultResourcePolicy.ts => getResourcePolicies.ts} (68%) diff --git a/messages/en-US.json b/messages/en-US.json index 7bd6b9c51..efaf777b5 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -735,6 +735,7 @@ "resourcePolicyOtpEmpty": "No one time password", "resourcePolicyTypeSave": "Save Resource type", "resourcePolicySelect": "Select resource policy", + "resourcePolicyNotFound": "Policy not found", "resourcePolicyRulesEmpty": "No authentication rules", "resourceAuthMethodsDescriptions": "Allow access to the resource via additional auth methods", "resourceAuthSettingsSave": "Saved successfully", diff --git a/server/routers/external.ts b/server/routers/external.ts index a23ec54ad..9ca80dc34 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -637,10 +637,10 @@ authenticated.get( ); authenticated.get( - "/resource/:resourceId/default-policy", + "/resource/:resourceId/policies", verifyResourceAccess, verifyUserHasAction(ActionsEnum.getResourcePolicy), - resource.getDefaultResourcePolicy + resource.getResourcePolicies ); authenticated.put( diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 56dec9dee..6b682bff7 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -454,10 +454,10 @@ authenticated.get( ); authenticated.get( - "/resource/:resourceId/default-policy", + "/resource/:resourceId/policies", verifyApiKeyResourceAccess, verifyApiKeyHasAction(ActionsEnum.getResourcePolicy), - resource.getDefaultResourcePolicy + resource.getResourcePolicies ); authenticated.post( diff --git a/server/routers/resource/getDefaultResourcePolicy.ts b/server/routers/resource/getResourcePolicies.ts similarity index 68% rename from server/routers/resource/getDefaultResourcePolicy.ts rename to server/routers/resource/getResourcePolicies.ts index 39621c2ea..6742a0bd5 100644 --- a/server/routers/resource/getDefaultResourcePolicy.ts +++ b/server/routers/resource/getResourcePolicies.ts @@ -17,12 +17,15 @@ const getResourcePoliciesParamsSchema = z.strictObject({ resourceId: z.string().transform(Number).pipe(z.int().positive()) }); -export type GetDefaultResourcePolicyResponse = GetResourcePolicyResponse; +export type GetResourcePoliciesResponse = { + defaultPolicy: GetResourcePolicyResponse; + sharedPolicy: GetResourcePolicyResponse | null; +}; registry.registerPath({ method: "get", - path: "/resource/{resourceId}/default-policy", - description: "Get the default policy for a resource.", + path: "/resource/{resourceId}/policies", + description: "Get the inline and shared policies associated with a resource.", tags: [OpenAPITags.PublicResource, OpenAPITags.Policy], request: { params: getResourcePoliciesParamsSchema @@ -30,7 +33,7 @@ registry.registerPath({ responses: {} }); -export async function getDefaultResourcePolicy( +export async function getResourcePolicies( req: Request, res: Response, next: NextFunction @@ -52,7 +55,8 @@ export async function getDefaultResourcePolicy( const [resource] = await db .select({ - defaultResourcePolicyId: resources.defaultResourcePolicyId + defaultResourcePolicyId: resources.defaultResourcePolicyId, + resourcePolicyId: resources.resourcePolicyId }) .from(resources) .where(eq(resources.resourceId, resourceId)) @@ -73,11 +77,24 @@ export async function getDefaultResourcePolicy( ); } - const defaultPolicy = await queryResourcePolicy({ - resourcePolicyId: resource.defaultResourcePolicyId - }); - return response(res, { - data: defaultPolicy, + const [defaultPolicy, sharedPolicy] = await Promise.all([ + queryResourcePolicy({ + resourcePolicyId: resource.defaultResourcePolicyId + }), + resource.resourcePolicyId + ? queryResourcePolicy({ + resourcePolicyId: resource.resourcePolicyId + }) + : null + ]); + + return response(res, { + data: { + defaultPolicy: + // the policy will always be non nullable + defaultPolicy as unknown as GetResourcePolicyResponse, + sharedPolicy + }, success: true, error: false, message: "Resource policies retrieved successfully", diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index e602932f0..78803105f 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -31,4 +31,4 @@ export * from "./addUserToResource"; export * from "./removeUserFromResource"; export * from "./listAllResourceNames"; export * from "./removeEmailFromResourceWhitelist"; -export * from "./getDefaultResourcePolicy"; +export * from "./getResourcePolicies"; diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index 414133c86..c1eadf5f3 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -79,7 +79,7 @@ import SetResourcePasswordForm from "components/SetResourcePasswordForm"; import { Binary, Bot, CheckIcon, InfoIcon, Key } from "lucide-react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; -import { +import React, { useActionState, useEffect, useMemo, @@ -105,8 +105,7 @@ type ResourcePolicyType = StrategyOption<"inline" | "shared">; export default function ResourceAuthenticationPage() { const { org } = useOrgContext(); - const { resource, updateResource, authInfo, updateAuthInfo } = - useResourceContext(); + const { resource, updateResource } = useResourceContext(); const { env } = useEnvContext(); @@ -125,7 +124,7 @@ export default function ResourceAuthenticationPage() { const form = useForm({ resolver: zodResolver(resourceTypeSchema), defaultValues: { - type: "inline" + type: resource.resourcePolicyId ? "shared" : "inline" } }); @@ -145,7 +144,7 @@ export default function ResourceAuthenticationPage() { enabled: selectedResourceType === "shared" }); - const { data: sharedPolicy } = useQuery({ + const { data: sharedPolicy, isLoading: isLoadingSharedPolicy } = useQuery({ ...resourcePolicyQueries.single({ resourcePolicyId: resource.resourcePolicyId ?? 1 }), @@ -157,17 +156,6 @@ export default function ResourceAuthenticationPage() { id: number; } | null>(null); - const pageLoading = isLoadingPolicies || !defaultPolicy; - - const [ - loadingRemoveResourceHeaderAuth, - setLoadingRemoveResourceHeaderAuth - ] = useState(false); - - const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); - const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); - const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); - const resourcePolicyTypes: Array = [ { id: "inline", @@ -181,6 +169,49 @@ export default function ResourceAuthenticationPage() { } ]; + useEffect(() => { + if (!isLoadingSharedPolicy && sharedPolicy) { + setSelectedPolicy({ + id: sharedPolicy.resourcePolicyId, + name: sharedPolicy.name + }); + } + }, [isLoadingSharedPolicy, sharedPolicy]); + + const [isUpdatingResource, startTransition] = useTransition(); + + async function handleSaveResourcePolicyType() { + try { + if (selectedResourceType === "inline") { + await api.post(`/resource/${resource.resourceId}`, { + resourcePolicyId: null + }); + } else { + if (!selectedPolicy) { + toast({ + title: t("error"), + description: t("resourcePolicySelectError"), + variant: "destructive" + }); + return; + } + await api.post(`/resource/${resource.resourceId}`, { + resourcePolicyId: selectedPolicy.id + }); + } + router.refresh(); + } catch (e) { + toast({ + title: t("error"), + description: formatAxiosError(e), + variant: "destructive" + }); + } + } + + const pageLoading = + isLoadingPolicies || !defaultPolicy || isLoadingSharedPolicy; + if (pageLoading) { return <>; } @@ -214,9 +245,6 @@ export default function ResourceAuthenticationPage() { role="combobox" className={ "w-full md:w-1/2 justify-between" - // "w-45 justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent", - // "rounded-l-md rounded-r-xs" - // !proxyTarget.siteId && "text-muted-foreground" } > @@ -238,7 +266,7 @@ export default function ResourceAuthenticationPage() { /> - {t("siteNotFound")} + {t("resourcePolicyNotFound")} {policiesList.map((policy) => ( @@ -275,9 +303,10 @@ export default function ResourceAuthenticationPage() { diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 022239006..36e66ca8a 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -4,7 +4,7 @@ import type { ListClientsResponse } from "@server/routers/client"; import type { ListDomainsResponse } from "@server/routers/domain"; import type { GetResourceWhitelistResponse, - GetDefaultResourcePolicyResponse, + GetResourcePoliciesResponse, ListResourceNamesResponse, ListResourcesResponse, ListResourceRolesResponse, @@ -31,6 +31,7 @@ import { remote } from "./api"; import { durationToMs } from "./durationToMs"; import { wait } from "./wait"; import type { ListResourcePoliciesResponse } from "@server/routers/resource/types"; +import type { GetResourcePolicyResponse } from "@server/routers/policy"; export type ProductUpdate = { link: string | null; @@ -227,7 +228,7 @@ export const resourcePolicyQueries = { queryKey: ["RESOURCE_POLICIES", resourcePolicyId] as const, queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< - AxiosResponse + AxiosResponse >(`/resource-policy/${resourcePolicyId}`, { signal }); return res.data.data; @@ -364,8 +365,8 @@ export const resourceQueries = { queryKey: ["RESOURCES", resourceId, "DEFAULT_POLICY"] as const, queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< - AxiosResponse - >(`/resource/${resourceId}/default-policy`, { signal }); + AxiosResponse + >(`/resource/${resourceId}/policies`, { signal }); return res.data.data; } From 1906504a86896cae826d393de3c251ca23eb9e01 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 12 Mar 2026 18:35:50 +0100 Subject: [PATCH 083/771] =?UTF-8?q?=E2=9C=A8=20update=20shared=20policy=20?= =?UTF-8?q?when=20selected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../proxy/[niceId]/authentication/page.tsx | 93 ++++++------------- src/lib/queries.ts | 18 +--- 2 files changed, 30 insertions(+), 81 deletions(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index c1eadf5f3..bb6059588 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -1,15 +1,12 @@ "use client"; import { EditPolicyForm } from "@app/components/resource-policy/EditPolicyForm"; -import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm"; -import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm"; import { SettingsContainer, SettingsSection, SettingsSectionBody, SettingsSectionDescription, SettingsSectionFooter, - SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; @@ -17,9 +14,6 @@ import { StrategySelect, type StrategyOption } from "@app/components/StrategySelect"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { Tag, TagInput } from "@app/components/tags/tag-input"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Button } from "@app/components/ui/button"; import { Command, @@ -29,29 +23,11 @@ import { CommandItem, CommandList } from "@app/components/ui/command"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { InfoPopup } from "@app/components/ui/info-popup"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@app/components/ui/select"; -import type { ResourceContextType } from "@app/contexts/resourceContext"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; @@ -59,34 +35,15 @@ import { useResourceContext } from "@app/hooks/useResourceContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { cn } from "@app/lib/cn"; -import { getUserDisplayName } from "@app/lib/getUserDisplayName"; -import { - orgQueries, - resourcePolicyQueries, - resourceQueries -} from "@app/lib/queries"; -import { - ResourcePolicyContext, - ResourcePolicyProvider -} from "@app/providers/ResourcePolicyProvider"; +import { orgQueries, resourceQueries } from "@app/lib/queries"; +import { ResourcePolicyProvider } from "@app/providers/ResourcePolicyProvider"; import { zodResolver } from "@hookform/resolvers/zod"; import { CaretSortIcon } from "@radix-ui/react-icons"; -import { build } from "@server/build"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; -import { UserType } from "@server/types/UserTypes"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import SetResourcePasswordForm from "components/SetResourcePasswordForm"; -import { Binary, Bot, CheckIcon, InfoIcon, Key } from "lucide-react"; +import { CheckIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; -import React, { - useActionState, - useEffect, - useMemo, - useRef, - useState, - useTransition -} from "react"; +import { useEffect, useState, useTransition } from "react"; import { useForm, useWatch } from "react-hook-form"; import { z } from "zod"; @@ -106,6 +63,7 @@ type ResourcePolicyType = StrategyOption<"inline" | "shared">; export default function ResourceAuthenticationPage() { const { org } = useOrgContext(); const { resource, updateResource } = useResourceContext(); + const queryClient = useQueryClient(); const { env } = useEnvContext(); @@ -115,8 +73,8 @@ export default function ResourceAuthenticationPage() { const { isPaidUser } = usePaidStatus(); - const { data: defaultPolicy, isLoading: isLoadingPolicies } = useQuery( - resourceQueries.defaultPolicy({ + const { data: policies, isLoading: isLoadingPolicies } = useQuery( + resourceQueries.policies({ resourceId: resource.resourceId }) ); @@ -144,13 +102,6 @@ export default function ResourceAuthenticationPage() { enabled: selectedResourceType === "shared" }); - const { data: sharedPolicy, isLoading: isLoadingSharedPolicy } = useQuery({ - ...resourcePolicyQueries.single({ - resourcePolicyId: resource.resourcePolicyId ?? 1 - }), - enabled: !!resource.resourcePolicyId - }); - const [selectedPolicy, setSelectedPolicy] = useState<{ name: string; id: number; @@ -170,13 +121,13 @@ export default function ResourceAuthenticationPage() { ]; useEffect(() => { - if (!isLoadingSharedPolicy && sharedPolicy) { + if (!isLoadingPolicies && policies?.sharedPolicy) { setSelectedPolicy({ - id: sharedPolicy.resourcePolicyId, - name: sharedPolicy.name + id: policies?.sharedPolicy.resourcePolicyId, + name: policies?.sharedPolicy.name }); } - }, [isLoadingSharedPolicy, sharedPolicy]); + }, [isLoadingPolicies, policies?.sharedPolicy]); const [isUpdatingResource, startTransition] = useTransition(); @@ -206,16 +157,25 @@ export default function ResourceAuthenticationPage() { description: formatAxiosError(e), variant: "destructive" }); + } finally { + await queryClient.invalidateQueries( + resourceQueries.policies({ + resourceId: resource.resourceId + }) + ); } } - const pageLoading = - isLoadingPolicies || !defaultPolicy || isLoadingSharedPolicy; + const pageLoading = isLoadingPolicies || !policies; if (pageLoading) { return <>; } + console.log({ + shared: policies.sharedPolicy + }); + return ( <> @@ -313,12 +273,15 @@ export default function ResourceAuthenticationPage() { {selectedResourceType === "inline" ? ( - + ) : ( - sharedPolicy && ( - + policies.sharedPolicy && ( + ) diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 36e66ca8a..ed2584af2 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -222,20 +222,6 @@ export const orgQueries = { }) }; -export const resourcePolicyQueries = { - single: ({ resourcePolicyId }: { resourcePolicyId: number }) => - queryOptions({ - queryKey: ["RESOURCE_POLICIES", resourcePolicyId] as const, - queryFn: async ({ signal, meta }) => { - const res = await meta!.api.get< - AxiosResponse - >(`/resource-policy/${resourcePolicyId}`, { signal }); - - return res.data.data; - } - }) -}; - export const logAnalyticsFiltersSchema = z.object({ timeStart: z .string() @@ -360,9 +346,9 @@ export const resourceQueries = { return res.data.data.whitelist; } }), - defaultPolicy: ({ resourceId }: { resourceId: number }) => + policies: ({ resourceId }: { resourceId: number }) => queryOptions({ - queryKey: ["RESOURCES", resourceId, "DEFAULT_POLICY"] as const, + queryKey: ["RESOURCES", resourceId, "POLICIES"] as const, queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< AxiosResponse From fee44ce96009f4996a1850f21e3155fd0a3e6c30 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 12 Mar 2026 18:52:13 +0100 Subject: [PATCH 084/771] =?UTF-8?q?=E2=9C=A8=20navigate=20to=20policy=20to?= =?UTF-8?q?=20edit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 2 ++ .../proxy/[niceId]/authentication/page.tsx | 28 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/messages/en-US.json b/messages/en-US.json index efaf777b5..02b5440d4 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -733,6 +733,8 @@ "resourceAuthMethods": "Authentication Methods", "resourcePolicyAuthMethodsEmpty": "No authentication method", "resourcePolicyOtpEmpty": "No one time password", + "resourcePolicyReadOnly": "This policy is Read only", + "resourcePolicyReadOnlyDescription": "This resource policy is shared accross multiple resources, you cannot edit it in this page. Please go to the policy settings to edit", "resourcePolicyTypeSave": "Save Resource type", "resourcePolicySelect": "Select resource policy", "resourcePolicyNotFound": "Policy not found", diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index bb6059588..c06ce6a4e 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -1,5 +1,6 @@ "use client"; +import ActionBanner from "@app/components/ActionBanner"; import { EditPolicyForm } from "@app/components/resource-policy/EditPolicyForm"; import { SettingsContainer, @@ -40,8 +41,9 @@ import { ResourcePolicyProvider } from "@app/providers/ResourcePolicyProvider"; import { zodResolver } from "@hookform/resolvers/zod"; import { CaretSortIcon } from "@radix-ui/react-icons"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { CheckIcon } from "lucide-react"; +import { ArrowRightIcon, CheckIcon, ShieldAlertIcon } from "lucide-react"; import { useTranslations } from "next-intl"; +import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useState, useTransition } from "react"; import { useForm, useWatch } from "react-hook-form"; @@ -282,6 +284,30 @@ export default function ResourceAuthenticationPage() { policy={policies.sharedPolicy} key={policies.sharedPolicy.resourcePolicyId} > + + } + description={t( + "resourcePolicyReadOnlyDescription" + )} + actions={ + + } + /> ) From 01b068c50f884c374d1fb5590ae58f3eda2c17d6 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 12 Mar 2026 18:53:18 +0100 Subject: [PATCH 085/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20do=20not=20edit=20?= =?UTF-8?q?tags=20if=20readonly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EditPolicyOtpEmailSectionForm.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx index af6edd2ce..16a73c672 100644 --- a/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyOtpEmailSectionForm.tsx @@ -252,13 +252,15 @@ export function EditPolicyOtpEmailSectionForm({ .emails ?? [] } setTags={(newEmails) => { - form.setValue( - "emails", - newEmails as [ - Tag, - ...Tag[] - ] - ); + if (!readonly) { + form.setValue( + "emails", + newEmails as [ + Tag, + ...Tag[] + ] + ); + } }} allowDuplicates={false} sortTags={true} From b61b74b0b53630253c029c874c214234c557118e Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 12 Mar 2026 20:04:02 +0100 Subject: [PATCH 086/771] =?UTF-8?q?=F0=9F=92=AC=20=20update=20text?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/en-US.json b/messages/en-US.json index 02b5440d4..ae82b8afa 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -734,7 +734,7 @@ "resourcePolicyAuthMethodsEmpty": "No authentication method", "resourcePolicyOtpEmpty": "No one time password", "resourcePolicyReadOnly": "This policy is Read only", - "resourcePolicyReadOnlyDescription": "This resource policy is shared accross multiple resources, you cannot edit it in this page. Please go to the policy settings to edit", + "resourcePolicyReadOnlyDescription": "This resource policy is shared accross multiple resources, you cannot edit it in this page.", "resourcePolicyTypeSave": "Save Resource type", "resourcePolicySelect": "Select resource policy", "resourcePolicyNotFound": "Policy not found", From 83a36ead1019e4c3b66c57af67e6d94ecad2c78a Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 12 Mar 2026 20:22:16 +0100 Subject: [PATCH 087/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20show=20success?= =?UTF-8?q?=20toast=20on=20resource=20policy=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/resources/proxy/[niceId]/authentication/page.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index c06ce6a4e..375c85a7c 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -153,6 +153,10 @@ export default function ResourceAuthenticationPage() { }); } router.refresh(); + toast({ + title: t("resourceUpdated"), + description: t("resourceUpdatedDescription") + }); } catch (e) { toast({ title: t("error"), From d13e6896a89cc467740a7f2d07bfac1727bba24b Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 12 Mar 2026 22:11:39 +0100 Subject: [PATCH 088/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../proxy/[niceId]/authentication/page.tsx | 206 ++++++++++-------- 1 file changed, 111 insertions(+), 95 deletions(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index 375c85a7c..4b1e9f516 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -40,6 +40,8 @@ import { orgQueries, resourceQueries } from "@app/lib/queries"; import { ResourcePolicyProvider } from "@app/providers/ResourcePolicyProvider"; import { zodResolver } from "@hookform/resolvers/zod"; import { CaretSortIcon } from "@radix-ui/react-icons"; +import { build } from "@server/build"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { ArrowRightIcon, CheckIcon, ShieldAlertIcon } from "lucide-react"; import { useTranslations } from "next-intl"; @@ -73,8 +75,6 @@ export default function ResourceAuthenticationPage() { const router = useRouter(); const t = useTranslations(); - const { isPaidUser } = usePaidStatus(); - const { data: policies, isLoading: isLoadingPolicies } = useQuery( resourceQueries.policies({ resourceId: resource.resourceId @@ -84,7 +84,10 @@ export default function ResourceAuthenticationPage() { const form = useForm({ resolver: zodResolver(resourceTypeSchema), defaultValues: { - type: resource.resourcePolicyId ? "shared" : "inline" + type: + build !== "oss" && resource.resourcePolicyId + ? "shared" + : "inline" } }); @@ -185,99 +188,112 @@ export default function ResourceAuthenticationPage() { return ( <> - - - - {t("resourcePolicySelectTitle")} - - - {t("resourcePolicySelectDescription")} - - - - { - form.setValue("type", value); - }} - cols={2} - /> - {selectedResourceType === "shared" && ( - - - - - - - + + + {t("resourcePolicySelectTitle")} + + + {t("resourcePolicySelectDescription")} + + + + { + form.setValue("type", value); + }} + cols={2} + /> + {selectedResourceType === "shared" && ( + + + - - + > + + {selectedPolicy + ? selectedPolicy.name + : t("resourcePolicySelect")} + + + + + + + + + + {t( + "resourcePolicyNotFound" + )} + + + {policiesList.map( + (policy) => ( + + setSelectedPolicy( + { + id: policy.resourcePolicyId, + name: policy.name + } + ) + } + > + + {policy.name} + + ) + )} + + + + + + )} + + + + + + )} + {selectedResourceType === "inline" ? ( From ccbd793f521d729a071587d533d1ed854bedee24 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 12 Mar 2026 22:13:27 +0100 Subject: [PATCH 089/771] =?UTF-8?q?=F0=9F=92=AC=20show=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + 1 file changed, 1 insertion(+) diff --git a/messages/en-US.json b/messages/en-US.json index ae82b8afa..8573d38c6 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -737,6 +737,7 @@ "resourcePolicyReadOnlyDescription": "This resource policy is shared accross multiple resources, you cannot edit it in this page.", "resourcePolicyTypeSave": "Save Resource type", "resourcePolicySelect": "Select resource policy", + "resourcePolicySelectError": "Select a resource policy", "resourcePolicyNotFound": "Policy not found", "resourcePolicyRulesEmpty": "No authentication rules", "resourceAuthMethodsDescriptions": "Allow access to the resource via additional auth methods", From f3eb823bc30de4fa3b8bb7f993822cf197cd1c99 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 12 Mar 2026 22:36:29 +0100 Subject: [PATCH 090/771] =?UTF-8?q?=F0=9F=90=9B=20=20fix=20sqlite=20tables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/sqlite/schema/schema.ts | 154 ++++++++++++++++++++++-------- 1 file changed, 116 insertions(+), 38 deletions(-) diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index a0c75b978..805f3c20b 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -104,8 +104,16 @@ export const sites = sqliteTable("sites", { export const resources = sqliteTable("resources", { resourceId: integer("resourceId").primaryKey({ autoIncrement: true }), - resourcePolicyId: integer("resourcePolicyId") - .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId").references( + () => resourcePolicies.resourcePolicyId, + { onDelete: "set null" } + ), + defaultResourcePolicyId: integer("defaultResourcePolicyId").references( + () => resourcePolicies.resourcePolicyId, + { + onDelete: "restrict" + } + ), resourceGuid: text("resourceGuid", { length: 36 }) .unique() .notNull() @@ -764,10 +772,7 @@ export const roleResources = sqliteTable("roleResources", { .references(() => roles.roleId, { onDelete: "cascade" }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), + .references(() => resources.resourceId, { onDelete: "cascade" }) }); export const userResources = sqliteTable("userResources", { @@ -776,10 +781,7 @@ export const userResources = sqliteTable("userResources", { .references(() => users.userId, { onDelete: "cascade" }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), + .references(() => resources.resourceId, { onDelete: "cascade" }) }); export const userInvites = sqliteTable("userInvites", { @@ -802,9 +804,6 @@ export const resourcePincode = sqliteTable("resourcePincode", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), pincodeHash: text("pincodeHash").notNull(), digitLength: integer("digitLength").notNull() }); @@ -816,9 +815,6 @@ export const resourcePassword = sqliteTable("resourcePassword", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), passwordHash: text("passwordHash").notNull() }); @@ -829,12 +825,50 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), headerAuthHash: text("headerAuthHash").notNull() }); +export const resourcePolicyPincode = sqliteTable("resourcePolicyPincode", { + pincodeId: integer("pincodeId").primaryKey({ autoIncrement: true }), + pincodeHash: text("pincodeHash").notNull(), + digitLength: integer("digitLength").notNull(), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { + onDelete: "cascade" + }) +}); + +export const resourcePolicyPassword = sqliteTable("resourcePolicyPassword", { + passwordId: integer("passwordId").primaryKey({ autoIncrement: true }), + passwordHash: text("passwordHash").notNull(), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { + onDelete: "cascade" + }) +}); + +export const resourcePolicyHeaderAuth = sqliteTable( + "resourcePolicyHeaderAuth", + { + headerAuthId: integer("headerAuthId").primaryKey({ + autoIncrement: true + }), + headerAuthHash: text("headerAuthHash").notNull(), + extendedCompatibility: integer("extendedCompatibility", { + mode: "boolean" + }) + .notNull() + .default(true), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { + onDelete: "cascade" + }) + } +); + export const resourceHeaderAuthExtendedCompatibility = sqliteTable( "resourceHeaderAuthExtendedCompatibility", { @@ -846,9 +880,6 @@ export const resourceHeaderAuthExtendedCompatibility = sqliteTable( resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), extendedCompatibilityIsActivated: integer( "extendedCompatibilityIsActivated", { mode: "boolean" } @@ -920,10 +951,7 @@ export const resourceWhitelist = sqliteTable("resourceWhitelist", { email: text("email").notNull(), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), + .references(() => resources.resourceId, { onDelete: "cascade" }) }); export const resourceOtp = sqliteTable("resourceOtp", { @@ -933,9 +961,6 @@ export const resourceOtp = sqliteTable("resourceOtp", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), email: text("email").notNull(), otpHash: text("otpHash").notNull(), expiresAt: integer("expiresAt").notNull() @@ -951,9 +976,6 @@ export const resourceRules = sqliteTable("resourceRules", { resourceId: integer("resourceId") .notNull() .references(() => resources.resourceId, { onDelete: "cascade" }), - resourcePolicyId: integer("resourcePolicyId") - .notNull() - .references(() => resourcePolicies.resourcePolicyId, { onDelete: "cascade" }), enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), priority: integer("priority").notNull(), action: text("action").notNull(), // ACCEPT, DROP, PASS @@ -961,12 +983,66 @@ export const resourceRules = sqliteTable("resourceRules", { value: text("value").notNull() }); +export const rolePolicies = sqliteTable("rolePolicies", { + roleId: integer("roleId") + .notNull() + .references(() => roles.roleId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { + onDelete: "cascade" + }) +}); + +export const userPolicies = sqliteTable("userPolicies", { + userId: text("userId") + .notNull() + .references(() => users.userId, { onDelete: "cascade" }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { + onDelete: "cascade" + }) +}); + +export const resourcePolicyWhiteList = sqliteTable("resourcePolicyWhitelist", { + whitelistId: integer("id").primaryKey({ autoIncrement: true }), + email: text("email").notNull(), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { + onDelete: "cascade" + }) +}); + +export const resourcePolicyRules = sqliteTable("resourcePolicyRules", { + ruleId: integer("ruleId").primaryKey({ autoIncrement: true }), + resourcePolicyId: integer("resourcePolicyId") + .notNull() + .references(() => resourcePolicies.resourcePolicyId, { + onDelete: "cascade" + }), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + priority: integer("priority").notNull(), + action: text("action").$type<"ACCEPT" | "DROP" | "PASS">().notNull(), + match: text("match").$type<"CIDR" | "PATH" | "IP">().notNull(), + value: text("value").notNull() +}); + export const resourcePolicies = sqliteTable("resourcePolicies", { - resourcePolicyId: integer('resourcePolicyId').primaryKey(), - sso: integer("sso", { mode: 'boolean' }).notNull().default(true), - emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: 'boolean' }).notNull().default(false), + resourcePolicyId: integer("resourcePolicyId").primaryKey(), + sso: integer("sso", { mode: "boolean" }).notNull().default(true), + applyRules: integer("applyRules", { mode: "boolean" }) + .notNull() + .default(false), + scope: text("scope") + .$type<"global" | "resource">() + .notNull() + .default("global"), + emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" }) + .notNull() + .default(false), niceId: text("niceId").notNull(), - isDefault: integer("isDefault", { mode: 'boolean' }).notNull().default(true), idpId: integer("idpId").references(() => idp.idpId, { onDelete: "set null" }), @@ -975,10 +1051,9 @@ export const resourcePolicies = sqliteTable("resourcePolicies", { .references(() => orgs.orgId, { onDelete: "cascade" }) - .notNull(), + .notNull() }); - export const supporterKey = sqliteTable("supporterKey", { keyId: integer("keyId").primaryKey({ autoIncrement: true }), key: text("key").notNull(), @@ -1215,3 +1290,6 @@ export type DeviceWebAuthCode = InferSelectModel; export type RoundTripMessageTracker = InferSelectModel< typeof roundTripMessageTracker >; +export type ResourcePolicy = InferSelectModel; +export type RolePolicy = InferSelectModel; +export type UserPolicy = InferSelectModel; From 81274960f67164b8d82d24237be80ace045396cd Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 4 May 2026 20:22:16 +0200 Subject: [PATCH 091/771] =?UTF-8?q?=F0=9F=9A=A7=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[orgId]/settings/logs/request/page.tsx | 7 +-- src/lib/queries.ts | 59 ++++++++++++++++++- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/app/[orgId]/settings/logs/request/page.tsx b/src/app/[orgId]/settings/logs/request/page.tsx index 6380d0473..86d1a9c25 100644 --- a/src/app/[orgId]/settings/logs/request/page.tsx +++ b/src/app/[orgId]/settings/logs/request/page.tsx @@ -26,7 +26,7 @@ export default function GeneralPage() { const searchParams = useSearchParams(); const [rows, setRows] = useState([]); - const [isRefreshing, setIsRefreshing] = useState(false); + const [isRefreshing, startRefreshingTransition] = useTransition(); const [isExporting, startTransition] = useTransition(); // Pagination state @@ -279,7 +279,6 @@ export default function GeneralPage() { const refreshData = async () => { console.log("Data refreshed"); - setIsRefreshing(true); try { // Refresh data with current date range and pagination await queryDateTime( @@ -294,8 +293,6 @@ export default function GeneralPage() { description: t("refreshError"), variant: "destructive" }); - } finally { - setIsRefreshing(false); } }; @@ -781,7 +778,7 @@ export default function GeneralPage() { title={t("requestLogs")} searchPlaceholder={t("searchLogs")} searchColumn="host" - onRefresh={refreshData} + onRefresh={() => startRefreshingTransition(refreshData)} isRefreshing={isRefreshing} onExport={() => startTransition(exportData)} isExporting={isExporting} diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 3e38a7ba0..9f69f1c34 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -1,5 +1,6 @@ import { build } from "@server/build"; import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs"; +import type { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types"; import type { ListClientsResponse } from "@server/routers/client"; import type { ListDomainsResponse, @@ -529,7 +530,36 @@ export const logAnalyticsFiltersSchema = z.object({ resourceId: z.coerce.number().optional().catch(undefined) }); -export type LogAnalyticsFilters = z.TypeOf; +export type LogAnalyticsFilters = z.output; + +export const logsFiltersSchema = z.object({ + timeStart: z + .string() + .refine((val) => !isNaN(Date.parse(val)), { + error: "timeStart must be a valid ISO date string" + }) + .optional() + .catch(undefined), + timeEnd: z + .string() + .refine((val) => !isNaN(Date.parse(val)), { + error: "timeEnd must be a valid ISO date string" + }) + .optional() + .catch(undefined), + page: z.coerce.number().optional().catch(0).default(0), + pageSize: z.coerce.number().optional().catch(20).default(20), + resourceId: z.coerce.number().optional().catch(undefined), + action: z.string().optional().catch(undefined), + host: z.string().optional().catch(undefined), + location: z.string().optional().catch(undefined), + actor: z.string().optional().catch(undefined), + method: z.string().optional().catch(undefined), + reason: z.string().optional().catch(undefined), + path: z.string().optional().catch(undefined) +}); + +export type LogFilters = z.output; export const logQueries = { requestAnalytics: ({ @@ -540,7 +570,7 @@ export const logQueries = { filters: LogAnalyticsFilters; }) => queryOptions({ - queryKey: ["REQUEST_LOG_ANALYTICS", orgId, filters] as const, + queryKey: ["REQUEST_LOGS", orgId, "ANALYTICS", filters] as const, queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< AxiosResponse @@ -556,6 +586,31 @@ export const logQueries = { } return false; } + }), + + requests: ({ orgId, filters }: { orgId: string; filters: LogFilters }) => + queryOptions({ + queryKey: ["REQUEST_LOGS", orgId, "ALL", filters] as const, + queryFn: async ({ signal, meta }) => { + const { page, pageSize, ...rest } = filters; + const res = await meta!.api.get< + AxiosResponse + >(`/org/${orgId}/logs/request`, { + params: { + ...rest, + limit: pageSize, + offset: page * pageSize + }, + signal + }); + return res.data.data; + }, + refetchInterval: (query) => { + if (query.state.data) { + return durationToMs(30, "seconds"); + } + return false; + } }) }; From a80ae49a33f000b02e211f1555b0af195f7f7917 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 May 2026 14:54:20 -0700 Subject: [PATCH 092/771] Support multiple roles --- server/middlewares/verifyResourcePolicyAccess.ts | 12 +++++++----- .../private/routers/policy/createResourcePolicy.ts | 7 +++++-- .../private/routers/policy/listResourcePolicies.ts | 2 +- server/routers/policy/updateResourcePolicy.ts | 2 +- server/routers/resource/createResource.ts | 4 ++-- 5 files changed, 16 insertions(+), 11 deletions(-) diff --git a/server/middlewares/verifyResourcePolicyAccess.ts b/server/middlewares/verifyResourcePolicyAccess.ts index 83eb69d7f..142468d15 100644 --- a/server/middlewares/verifyResourcePolicyAccess.ts +++ b/server/middlewares/verifyResourcePolicyAccess.ts @@ -5,6 +5,7 @@ import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; +import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; export async function verifyResourcePolicyAccess( req: Request, @@ -16,10 +17,8 @@ export async function verifyResourcePolicyAccess( req.params?.resourcePolicyId || req.body?.resourcePolicyId || req.query?.resourcePolicyId; - const niceId = - req.params?.niceId || req.body?.niceId || req.query?.niceId; - const orgId = - req.params?.orgId || req.body?.orgId || req.query?.orgId; + const niceId = req.params?.niceId || req.body?.niceId || req.query?.niceId; + const orgId = req.params?.orgId || req.body?.orgId || req.query?.orgId; try { if (!userId) { @@ -110,7 +109,10 @@ export async function verifyResourcePolicyAccess( } } - req.userOrgRoleId = req.userOrg.roleId; + req.userOrgRoleIds = await getUserOrgRoleIds( + req.userOrg.userId, + orgId! + ); req.userOrgId = policy.orgId; return next(); diff --git a/server/private/routers/policy/createResourcePolicy.ts b/server/private/routers/policy/createResourcePolicy.ts index 1bbdfe153..48b336f1f 100644 --- a/server/private/routers/policy/createResourcePolicy.ts +++ b/server/private/routers/policy/createResourcePolicy.ts @@ -143,7 +143,7 @@ export async function createResourcePolicy( } const { orgId } = parsedParams.data; - if (req.user && !req.userOrgRoleId) { + if (req.user && req.userOrgRoleIds?.length === 0) { return next( createHttpError(HttpCode.FORBIDDEN, "User does not have a role") ); @@ -304,7 +304,10 @@ export async function createResourcePolicy( const usersToAdd: InferInsertModel[] = []; - if (req.user && req.userOrgRoleId != adminRole[0].roleId) { + if ( + req.user && + !req.userOrgRoleIds?.includes(adminRole[0].roleId) + ) { // make sure the user can access the policy usersToAdd.push({ userId: req.user?.userId!, diff --git a/server/private/routers/policy/listResourcePolicies.ts b/server/private/routers/policy/listResourcePolicies.ts index 58a83df04..5e6a4982c 100644 --- a/server/private/routers/policy/listResourcePolicies.ts +++ b/server/private/routers/policy/listResourcePolicies.ts @@ -145,7 +145,7 @@ export async function listResourcePolicies( .where( or( eq(userPolicies.userId, req.user!.userId), - eq(rolePolicies.roleId, req.userOrgRoleId!) + inArray(rolePolicies.roleId, req.userOrgRoleIds || []) ) ); } else { diff --git a/server/routers/policy/updateResourcePolicy.ts b/server/routers/policy/updateResourcePolicy.ts index 77443e1a2..ad8b19639 100644 --- a/server/routers/policy/updateResourcePolicy.ts +++ b/server/routers/policy/updateResourcePolicy.ts @@ -54,7 +54,7 @@ export async function updateResourcePolicy( ); } - if (req.user && !req.userOrgRoleId) { + if (req.user && req.userOrgRoleIds?.length === 0) { return next( createHttpError(HttpCode.FORBIDDEN, "User does not have a role") ); diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index b14da3743..a0258c751 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -353,7 +353,7 @@ async function createHttpResource( }); // make this policy visible by the current user - if (req.user && req.userOrgRoleId !== adminRole[0].roleId) { + if (req.user && !req.userOrgRoleIds?.includes(adminRole[0].roleId)) { await trx.insert(userPolicies).values({ userId: req.user?.userId!, resourcePolicyId: defaultPolicy.resourcePolicyId @@ -479,7 +479,7 @@ async function createRawResource( }); // make this policy visible by the current user - if (req.user && req.userOrgRoleId != adminRole[0].roleId) { + if (req.user && !req.userOrgRoleIds?.includes(adminRole[0].roleId)) { await trx.insert(userPolicies).values({ userId: req.user?.userId!, resourcePolicyId: defaultPolicy.resourcePolicyId From 20ebdc6289d5aee3f03b7389cc8cff9748a69962 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 May 2026 15:04:54 -0700 Subject: [PATCH 093/771] Fix openapi zod issue error --- .../private/routers/policy/listResourcePolicies.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/server/private/routers/policy/listResourcePolicies.ts b/server/private/routers/policy/listResourcePolicies.ts index 5e6a4982c..64cd5c61a 100644 --- a/server/private/routers/policy/listResourcePolicies.ts +++ b/server/private/routers/policy/listResourcePolicies.ts @@ -43,14 +43,24 @@ const listResourcePoliciesSchema = z.object({ .positive() .optional() .catch(20) - .default(20), + .default(20) + .openapi({ + type: "integer", + default: 20, + description: "Number of items per page" + }), page: z.coerce .number() // for prettier formatting .int() .min(0) .optional() .catch(1) - .default(1), + .default(1) + .openapi({ + type: "integer", + default: 1, + description: "Page number to retrieve" + }), query: z.string().optional() }); From 43f2e32231622be0214ef29d51b3282ad4aa7fe9 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 May 2026 15:30:49 -0700 Subject: [PATCH 094/771] Paywall resource policies --- server/lib/billing/tierMatrix.ts | 6 +- server/private/routers/external.ts | 6 +- .../routers/policy/createResourcePolicy.ts | 13 ++ .../routers/policy/deleteResourcePolicy.ts | 2 +- server/private/routers/policy/index.ts | 2 +- .../routers/policy/listResourcePolicies.ts | 2 +- server/routers/resource/updateResource.ts | 41 +++- .../proxy/[niceId]/authentication/page.tsx | 216 +++++++++--------- src/components/ResourcePoliciesTable.tsx | 5 + .../resource-policy/CreatePolicyForm.tsx | 136 ++++++----- 10 files changed, 243 insertions(+), 186 deletions(-) diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts index f44cb8bf6..fa0b63a33 100644 --- a/server/lib/billing/tierMatrix.ts +++ b/server/lib/billing/tierMatrix.ts @@ -24,7 +24,8 @@ export enum TierFeature { DomainNamespaces = "domainNamespaces", // handle downgrade by removing custom domain namespaces StandaloneHealthChecks = "standaloneHealthChecks", AlertingRules = "alertingRules", - WildcardSubdomain = "wildcardSubdomain" + WildcardSubdomain = "wildcardSubdomain", + ResourcePolicies = "resourcePolicies" } export const tierMatrix: Record = { @@ -66,5 +67,6 @@ export const tierMatrix: Record = { [TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"], [TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"], [TierFeature.AlertingRules]: ["tier3", "enterprise"], - [TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"] + [TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"], + [TierFeature.ResourcePolicies]: ["tier3", "enterprise"] }; diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 7e5fc8f67..ba96c4edf 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -389,7 +389,7 @@ authenticated.delete( "/resource-policy/:resourcePolicyId", verifyResourcePolicyAccess, verifyValidLicense, - // verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ? + verifyValidSubscription(tierMatrix.resourcePolicies), verifyLimits, verifyUserHasAction(ActionsEnum.deleteResourcePolicy), logActionAudit(ActionsEnum.deleteResourcePolicy), @@ -399,7 +399,7 @@ authenticated.delete( authenticated.get( "/org/:orgId/resource-policies", verifyValidLicense, - // verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ? + verifyValidSubscription(tierMatrix.resourcePolicies), verifyOrgAccess, verifyLimits, verifyUserHasAction(ActionsEnum.listResourcePolicies), @@ -410,7 +410,7 @@ authenticated.get( authenticated.post( "/org/:orgId/resource-policy", verifyValidLicense, - // verifyValidSubscription(tierMatrix.loginPageDomain), // todo: use the correct subscription ? + verifyValidSubscription(tierMatrix.resourcePolicies), verifyOrgAccess, verifyLimits, verifyUserHasAction(ActionsEnum.createResourcePolicy), diff --git a/server/private/routers/policy/createResourcePolicy.ts b/server/private/routers/policy/createResourcePolicy.ts index 48b336f1f..2b4678331 100644 --- a/server/private/routers/policy/createResourcePolicy.ts +++ b/server/private/routers/policy/createResourcePolicy.ts @@ -1,3 +1,16 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + import { hashPassword } from "@server/auth/password"; import { db, diff --git a/server/private/routers/policy/deleteResourcePolicy.ts b/server/private/routers/policy/deleteResourcePolicy.ts index 17a9a68f9..a586cf3b4 100644 --- a/server/private/routers/policy/deleteResourcePolicy.ts +++ b/server/private/routers/policy/deleteResourcePolicy.ts @@ -1,7 +1,7 @@ /* * This file is part of a proprietary work. * - * Copyright (c) 2025 Fossorial, Inc. + * Copyright (c) 2025-2026 Fossorial, Inc. * All rights reserved. * * This file is licensed under the Fossorial Commercial License. diff --git a/server/private/routers/policy/index.ts b/server/private/routers/policy/index.ts index 1fb73a58c..c780ebfe4 100644 --- a/server/private/routers/policy/index.ts +++ b/server/private/routers/policy/index.ts @@ -1,7 +1,7 @@ /* * This file is part of a proprietary work. * - * Copyright (c) 2025 Fossorial, Inc. + * Copyright (c) 2025-2026 Fossorial, Inc. * All rights reserved. * * This file is licensed under the Fossorial Commercial License. diff --git a/server/private/routers/policy/listResourcePolicies.ts b/server/private/routers/policy/listResourcePolicies.ts index 64cd5c61a..beb1b68c3 100644 --- a/server/private/routers/policy/listResourcePolicies.ts +++ b/server/private/routers/policy/listResourcePolicies.ts @@ -1,7 +1,7 @@ /* * This file is part of a proprietary work. * - * Copyright (c) 2025 Fossorial, Inc. + * Copyright (c) 2025-2026 Fossorial, Inc. * All rights reserved. * * This file is licensed under the Fossorial Commercial License. diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index d36e49e87..b56469167 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -25,7 +25,10 @@ import { import { registry } from "@server/openApi"; import { OpenAPITags } from "@server/openApi"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; -import { validateAndConstructDomain, checkWildcardDomainConflict } from "@server/lib/domainUtils"; +import { + validateAndConstructDomain, + checkWildcardDomainConflict +} from "@server/lib/domainUtils"; import { build } from "@server/build"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; @@ -304,11 +307,30 @@ async function updateHttpResource( const updateData = parsedBody.data; + const isLicensed = await isLicensedOrSubscribed( + resource.orgId, + tierMatrix.wildcardSubdomain + ); + if (updateData.resourcePolicyId != null) { + if (!isLicensed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Resource policies are not supported on your current plan. Please upgrade to access this feature." + ) + ); + } + const [existingPolicy] = await db .select() .from(resourcePolicies) - .where(eq(resourcePolicies.resourcePolicyId, updateData.resourcePolicyId)) + .where( + eq( + resourcePolicies.resourcePolicyId, + updateData.resourcePolicyId + ) + ) .limit(1); if (!existingPolicy) { @@ -346,10 +368,6 @@ async function updateHttpResource( // Wildcard subdomains are a paid feature if (updateData.subdomain && updateData.subdomain.includes("*")) { - const isLicensed = await isLicensedOrSubscribed( - resource.orgId, - tierMatrix.wildcardSubdomain - ); if (!isLicensed) { return next( createHttpError( @@ -494,10 +512,6 @@ async function updateHttpResource( headers = null; } - const isLicensed = await isLicensedOrSubscribed( - resource.orgId, - tierMatrix.maintencePage - ); if (!isLicensed) { updateData.maintenanceModeEnabled = undefined; updateData.maintenanceModeType = undefined; @@ -560,7 +574,12 @@ async function updateRawResource( const [existingPolicy] = await db .select() .from(resourcePolicies) - .where(eq(resourcePolicies.resourcePolicyId, updateData.resourcePolicyId)) + .where( + eq( + resourcePolicies.resourcePolicyId, + updateData.resourcePolicyId + ) + ) .limit(1); if (!existingPolicy) { diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index 4b1e9f516..4d725ae4b 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -41,7 +41,7 @@ import { ResourcePolicyProvider } from "@app/providers/ResourcePolicyProvider"; import { zodResolver } from "@hookform/resolvers/zod"; import { CaretSortIcon } from "@radix-ui/react-icons"; import { build } from "@server/build"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { ArrowRightIcon, CheckIcon, ShieldAlertIcon } from "lucide-react"; import { useTranslations } from "next-intl"; @@ -70,6 +70,7 @@ export default function ResourceAuthenticationPage() { const queryClient = useQueryClient(); const { env } = useEnvContext(); + const { isPaidUser } = usePaidStatus(); const api = createApiClient({ env }); const router = useRouter(); @@ -188,111 +189,118 @@ export default function ResourceAuthenticationPage() { return ( <> - {build !== "oss" && ( - - - - {t("resourcePolicySelectTitle")} - - - {t("resourcePolicySelectDescription")} - - - - { - form.setValue("type", value); - }} - cols={2} - /> - {selectedResourceType === "shared" && ( - - - - - - - + + + {t("resourcePolicySelectTitle")} + + + {t("resourcePolicySelectDescription")} + + + + { + form.setValue("type", value); + }} + cols={2} + /> + {selectedResourceType === "shared" && ( + + + + + + + - - {policiesList.map( - (policy) => ( - - setSelectedPolicy( - { - id: policy.resourcePolicyId, - name: policy.name - } - ) - } - > - - {policy.name} - - ) - )} - - - - - - )} - - - - - - )} + value={ + resourcePolicysearchQuery + } + onValueChange={ + setResourcePolicySearchQuery + } + /> + + + {t( + "resourcePolicyNotFound" + )} + + + {policiesList.map( + (policy) => ( + + setSelectedPolicy( + { + id: policy.resourcePolicyId, + name: policy.name + } + ) + } + > + + { + policy.name + } + + ) + )} + + + + + + )} + + + + + + )} {selectedResourceType === "inline" ? ( diff --git a/src/components/ResourcePoliciesTable.tsx b/src/components/ResourcePoliciesTable.tsx index bfdc49724..3039c821c 100644 --- a/src/components/ResourcePoliciesTable.tsx +++ b/src/components/ResourcePoliciesTable.tsx @@ -29,6 +29,8 @@ import { DropdownMenuTrigger } from "./ui/dropdown-menu"; import ConfirmDeleteDialog from "./ConfirmDeleteDialog"; +import { PaidFeaturesAlert } from "./PaidFeaturesAlert"; +import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix"; type ResourcePolicyRow = ListResourcePoliciesResponse["policies"][number]; @@ -253,6 +255,9 @@ export function ResourcePoliciesTable({ return ( <> + {selectedResourcePolicy && ( ; } - return ( -
- - {/* Name */} - - - - {t("resourcePolicyName")} - - - {t("resourcePolicyNameDescription")} - - - - - ( - - {t("name")} - - - - - - )} - /> - - - + const policyTiers = tierMatrix[TierFeature.ResourcePolicies]; + const isDisabled = !isPaidUser(policyTiers); - - - - - + return ( + <> + + +
+ + {/* Name */} + + + + {t("resourcePolicyName")} + + + {t("resourcePolicyNameDescription")} + + + + + ( + + + {t("name")} + + + + + + + )} + /> + + + + + + + + + +
- + + ); } From 5922bfb1a0914d13bf2d3039ca8e778c4fb40fc3 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 May 2026 16:01:40 -0700 Subject: [PATCH 095/771] Fix API endpoint action issues --- server/auth/actions.ts | 33 +++++++++++-------- .../middlewares/verifyResourcePolicyAccess.ts | 2 +- .../verifyUserCanSetUserOrgRoles.ts | 2 +- server/routers/external.ts | 3 +- server/routers/user/getOrgUser.ts | 7 ++-- 5 files changed, 24 insertions(+), 23 deletions(-) diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 23a5d73e1..cbe65b8f6 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -5,6 +5,7 @@ import { and, eq, inArray } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; +import logger from "@server/logger"; export enum ActionsEnum { createOrgUser = "createOrgUser", @@ -199,6 +200,23 @@ export async function checkUserActionPermission( } } + // If no direct permission, check role-based permission (any of user's roles) + const roleActionPermission = await db + .select() + .from(roleActions) + .where( + and( + eq(roleActions.actionId, actionId), + inArray(roleActions.roleId, userOrgRoleIds), + eq(roleActions.orgId, req.userOrgId!) + ) + ) + .limit(1); + + if (roleActionPermission.length > 0) { + return true; + } + // Check if the user has direct permission for the action in the current org const userActionPermission = await db .select() @@ -216,20 +234,7 @@ export async function checkUserActionPermission( return true; } - // If no direct permission, check role-based permission (any of user's roles) - const roleActionPermission = await db - .select() - .from(roleActions) - .where( - and( - eq(roleActions.actionId, actionId), - inArray(roleActions.roleId, userOrgRoleIds), - eq(roleActions.orgId, req.userOrgId!) - ) - ) - .limit(1); - - return roleActionPermission.length > 0; + return false; } catch (error) { console.error("Error checking user action permission:", error); throw createHttpError( diff --git a/server/middlewares/verifyResourcePolicyAccess.ts b/server/middlewares/verifyResourcePolicyAccess.ts index 142468d15..30fe48e8c 100644 --- a/server/middlewares/verifyResourcePolicyAccess.ts +++ b/server/middlewares/verifyResourcePolicyAccess.ts @@ -111,7 +111,7 @@ export async function verifyResourcePolicyAccess( req.userOrgRoleIds = await getUserOrgRoleIds( req.userOrg.userId, - orgId! + policy.orgId ); req.userOrgId = policy.orgId; diff --git a/server/middlewares/verifyUserCanSetUserOrgRoles.ts b/server/middlewares/verifyUserCanSetUserOrgRoles.ts index 1a7554ab3..3b8687b96 100644 --- a/server/middlewares/verifyUserCanSetUserOrgRoles.ts +++ b/server/middlewares/verifyUserCanSetUserOrgRoles.ts @@ -38,7 +38,7 @@ export function verifyUserCanSetUserOrgRoles() { return next( createHttpError( HttpCode.FORBIDDEN, - "User does not have permission perform this action" + "User does not have permission to set user organization roles" ) ); } catch (error) { diff --git a/server/routers/external.ts b/server/routers/external.ts index 31c8fccfb..d9cb0e291 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -105,7 +105,6 @@ authenticated.put( site.createSite ); - authenticated.get( "/org/:orgId/sites", verifyOrgAccess, @@ -727,7 +726,7 @@ authenticated.put( "/resource-policy/:resourcePolicyId/access-control", verifyResourcePolicyAccess, verifyUserHasAction(ActionsEnum.setResourcePolicyUsers), - verifyUserHasAction(ActionsEnum.setResourcePolicyRoles), + logActionAudit(ActionsEnum.setResourcePolicyUsers), policy.setResourcePolicyAccessControl ); diff --git a/server/routers/user/getOrgUser.ts b/server/routers/user/getOrgUser.ts index c415e186c..552b55f8a 100644 --- a/server/routers/user/getOrgUser.ts +++ b/server/routers/user/getOrgUser.ts @@ -47,10 +47,7 @@ export async function queryUser(orgId: string, userId: string) { .from(userOrgRoles) .leftJoin(roles, eq(userOrgRoles.roleId, roles.roleId)) .where( - and( - eq(userOrgRoles.userId, userId), - eq(userOrgRoles.orgId, orgId) - ) + and(eq(userOrgRoles.userId, userId), eq(userOrgRoles.orgId, orgId)) ); const isAdmin = roleRows.some((r) => r.isAdmin); @@ -146,7 +143,7 @@ export async function getOrgUser( return next( createHttpError( HttpCode.FORBIDDEN, - "User does not have permission perform this action" + "User does not have permission to get organization user details" ) ); } From 7b05c02508ff83cff4c0002c022819cb89e1fa15 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 May 2026 16:19:04 -0700 Subject: [PATCH 096/771] Adjust translation --- messages/en-US.json | 3 ++- .../settings/resources/proxy/[niceId]/authentication/page.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 4bf8e8806..f3b0decab 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -833,11 +833,12 @@ "resourcePolicyAuthMethodsEmpty": "No authentication method", "resourcePolicyOtpEmpty": "No one time password", "resourcePolicyReadOnly": "This policy is Read only", - "resourcePolicyReadOnlyDescription": "This resource policy is shared accross multiple resources, you cannot edit it in this page.", + "resourcePolicyReadOnlyDescription": "This resource policy is shared accross multiple resources, you cannot edit it on this page.", "resourcePolicyTypeSave": "Save Resource type", "resourcePolicySelect": "Select resource policy", "resourcePolicySelectError": "Select a resource policy", "resourcePolicyNotFound": "Policy not found", + "resourcePolicySearch": "Search policies", "resourcePolicyRulesEmpty": "No authentication rules", "resourceAuthMethodsDescriptions": "Allow access to the resource via additional auth methods", "resourceAuthSettingsSave": "Saved successfully", diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index 4d725ae4b..3194e7343 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -233,7 +233,7 @@ export default function ResourceAuthenticationPage() { Date: Mon, 4 May 2026 17:30:10 -0700 Subject: [PATCH 097/771] Adjust verify session queries to use policies --- server/db/queries/verifySessionQueries.ts | 125 +++++++++++++---- server/private/routers/hybrid.ts | 156 +++++++++++++++++----- 2 files changed, 220 insertions(+), 61 deletions(-) diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts index 46b45b1a0..17844e13c 100644 --- a/server/db/queries/verifySessionQueries.ts +++ b/server/db/queries/verifySessionQueries.ts @@ -17,10 +17,13 @@ import { resourceHeaderAuth, ResourceHeaderAuth, resourceRules, + resourcePolicyRules, resources, roleResources, + rolePolicies, sessions, userResources, + userPolicies, users, ResourceHeaderAuthExtendedCompatibility, resourceHeaderAuthExtendedCompatibility @@ -154,58 +157,126 @@ export async function getRoleName(roleId: number): Promise { } /** - * Check if role has access to resource + * Check if role has access to resource (direct or via resource policy) */ export async function getRoleResourceAccess( resourceId: number, roleIds: number[] ) { - const roleResourceAccess = await db - .select() - .from(roleResources) - .where( - and( - eq(roleResources.resourceId, resourceId), - inArray(roleResources.roleId, roleIds) + const [direct, viaPolicies] = await Promise.all([ + db + .select() + .from(roleResources) + .where( + and( + eq(roleResources.resourceId, resourceId), + inArray(roleResources.roleId, roleIds) + ) + ), + db + .select({ + roleId: rolePolicies.roleId, + resourcePolicyId: rolePolicies.resourcePolicyId + }) + .from(rolePolicies) + .innerJoin( + resources, + eq(resources.resourcePolicyId, rolePolicies.resourcePolicyId) ) - ); + .where( + and( + eq(resources.resourceId, resourceId), + inArray(rolePolicies.roleId, roleIds) + ) + ) + ]); - return roleResourceAccess.length > 0 ? roleResourceAccess : null; + const combined = [...direct, ...viaPolicies]; + return combined.length > 0 ? combined : null; } /** - * Check if user has direct access to resource + * Check if user has access to resource (direct or via resource policy) */ export async function getUserResourceAccess( userId: string, resourceId: number ) { - const userResourceAccess = await db - .select() - .from(userResources) - .where( - and( - eq(userResources.userId, userId), - eq(userResources.resourceId, resourceId) + const [direct, viaPolicies] = await Promise.all([ + db + .select() + .from(userResources) + .where( + and( + eq(userResources.userId, userId), + eq(userResources.resourceId, resourceId) + ) ) - ) - .limit(1); + .limit(1), + db + .select({ + userId: userPolicies.userId, + resourcePolicyId: userPolicies.resourcePolicyId + }) + .from(userPolicies) + .innerJoin( + resources, + eq(resources.resourcePolicyId, userPolicies.resourcePolicyId) + ) + .where( + and( + eq(resources.resourceId, resourceId), + eq(userPolicies.userId, userId) + ) + ) + .limit(1) + ]); - return userResourceAccess.length > 0 ? userResourceAccess[0] : null; + return direct[0] ?? viaPolicies[0] ?? null; } /** - * Get resource rules for a given resource + * Get resource rules for a given resource (direct and via resource policy) */ export async function getResourceRules( resourceId: number ): Promise { - const rules = await db - .select() - .from(resourceRules) - .where(eq(resourceRules.resourceId, resourceId)); + const [directRules, policyRules] = await Promise.all([ + db + .select() + .from(resourceRules) + .where(eq(resourceRules.resourceId, resourceId)), + db + .select({ + ruleId: resourcePolicyRules.ruleId, + resourceId: sql`${resourceId}`, + enabled: resourcePolicyRules.enabled, + priority: resourcePolicyRules.priority, + action: resourcePolicyRules.action, + match: resourcePolicyRules.match, + value: resourcePolicyRules.value + }) + .from(resourcePolicyRules) + .innerJoin( + resources, + eq( + resources.resourcePolicyId, + resourcePolicyRules.resourcePolicyId + ) + ) + .where(eq(resources.resourceId, resourceId)) + ]); - return rules; + const maxDirectPriority = directRules.reduce( + (max, r) => Math.max(max, r.priority), + 0 + ); + const offsetPolicyRules = policyRules.map((r) => ({ + ...r, + priority: maxDirectPriority + r.priority + })); + + return [...directRules, ...offsetPolicyRules] as ResourceRule[]; } /** diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index 98e7ff671..508952341 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -45,8 +45,11 @@ import { users, userOrgs, roleResources, + rolePolicies, userResources, + userPolicies, resourceRules, + resourcePolicyRules, userOrgRoles, roles } from "@server/db"; @@ -430,7 +433,10 @@ hybridRouter.get( ); // Decrypt and save key file - const decryptedKey = decrypt(cert.keyFile!, config.getRawConfig().server.secret!); + const decryptedKey = decrypt( + cert.keyFile!, + config.getRawConfig().server.secret! + ); // Return only the certificate data without org information return { @@ -531,7 +537,10 @@ hybridRouter.get( wildcardCandidates.length > 0 ? and( eq(resources.wildcard, true), - inArray(resources.fullDomain, wildcardCandidates) + inArray( + resources.fullDomain, + wildcardCandidates + ) ) : sql`false` ) @@ -545,10 +554,10 @@ hybridRouter.get( if ( result && - await checkExitNodeOrg( + (await checkExitNodeOrg( remoteExitNode.exitNodeId, result.resources.orgId - ) + )) ) { // If the exit node is not allowed for the org, return an error return next( @@ -1132,22 +1141,43 @@ hybridRouter.get( ); } - const roleResourceAccess = await db - .select() - .from(roleResources) - .where( - and( - eq(roleResources.resourceId, resourceId), - eq(roleResources.roleId, roleId) + const [direct, viaPolicies] = await Promise.all([ + db + .select() + .from(roleResources) + .where( + and( + eq(roleResources.resourceId, resourceId), + eq(roleResources.roleId, roleId) + ) ) - ) - .limit(1); + .limit(1), + db + .select({ + roleId: rolePolicies.roleId, + resourcePolicyId: rolePolicies.resourcePolicyId + }) + .from(rolePolicies) + .innerJoin( + resources, + eq( + resources.resourcePolicyId, + rolePolicies.resourcePolicyId + ) + ) + .where( + and( + eq(resources.resourceId, resourceId), + eq(rolePolicies.roleId, roleId) + ) + ) + .limit(1) + ]); - const result = - roleResourceAccess.length > 0 ? roleResourceAccess[0] : null; + const result = direct[0] ?? viaPolicies[0] ?? null; return response(res, { - data: result, + data: result as any, success: true, error: false, message: result @@ -1222,21 +1252,44 @@ hybridRouter.get( ); } - const roleResourceAccess = await db - .select({ - resourceId: roleResources.resourceId, - roleId: roleResources.roleId - }) - .from(roleResources) - .where( - and( - eq(roleResources.resourceId, resourceId), - inArray(roleResources.roleId, roleIds) - ) - ); + const [direct, viaPolicies] = await Promise.all([ + db + .select({ + resourceId: roleResources.resourceId, + roleId: roleResources.roleId + }) + .from(roleResources) + .where( + and( + eq(roleResources.resourceId, resourceId), + inArray(roleResources.roleId, roleIds) + ) + ), + roleIds.length > 0 + ? db + .select({ + resourceId: sql`${resourceId}`, + roleId: rolePolicies.roleId + }) + .from(rolePolicies) + .innerJoin( + resources, + eq( + resources.resourcePolicyId, + rolePolicies.resourcePolicyId + ) + ) + .where( + and( + eq(resources.resourceId, resourceId), + inArray(rolePolicies.roleId, roleIds) + ) + ) + : Promise.resolve([]) + ]); - const result = - roleResourceAccess.length > 0 ? roleResourceAccess : null; + const combined = [...direct, ...viaPolicies]; + const result = combined.length > 0 ? combined : null; return response<{ resourceId: number; roleId: number }[] | null>( res, @@ -1397,10 +1450,45 @@ hybridRouter.get( ); } - const rules = await db - .select() - .from(resourceRules) - .where(eq(resourceRules.resourceId, resourceId)); + const [directRules, policyRules] = await Promise.all([ + db + .select() + .from(resourceRules) + .where(eq(resourceRules.resourceId, resourceId)), + db + .select({ + ruleId: resourcePolicyRules.ruleId, + resourceId: sql`${resourceId}`, + enabled: resourcePolicyRules.enabled, + priority: resourcePolicyRules.priority, + action: resourcePolicyRules.action, + match: resourcePolicyRules.match, + value: resourcePolicyRules.value + }) + .from(resourcePolicyRules) + .innerJoin( + resources, + eq( + resources.resourcePolicyId, + resourcePolicyRules.resourcePolicyId + ) + ) + .where(eq(resources.resourceId, resourceId)) + ]); + + const maxDirectPriority = directRules.reduce( + (max, r) => Math.max(max, r.priority), + 0 + ); + const offsetPolicyRules = policyRules.map((r) => ({ + ...r, + priority: maxDirectPriority + r.priority + })); + + const rules = [ + ...directRules, + ...offsetPolicyRules + ] as (typeof resourceRules.$inferSelect)[]; // backward compatibility: COUNTRY -> GEOIP // TODO: remove this after a few versions once all exit nodes are updated From 7ccceeea0dad2873469a7451b8ca1adb7af2508a Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 May 2026 17:43:02 -0700 Subject: [PATCH 098/771] Ignore extra sqlite files --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3e03b1112..b2221fde6 100644 --- a/.gitignore +++ b/.gitignore @@ -17,9 +17,9 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts *.db -*.sqlite +*.sqlite* !Dockerfile.sqlite -*.sqlite3 +*.sqlite3* *.log .machinelogs*.json *-audit.json From fc2c13a686f6b5a3a846c9a73ff265703dbc047c Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 May 2026 20:42:02 -0700 Subject: [PATCH 099/771] Add policies to blueprints --- server/lib/blueprints/proxyResources.ts | 1199 +++++++++++++++++------ server/lib/blueprints/types.ts | 25 +- 2 files changed, 909 insertions(+), 315 deletions(-) diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index 34b352a42..cb32c476d 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -16,7 +16,15 @@ import { Transaction, userOrgs, userResources, - users + users, + resourcePolicies, + resourcePolicyPassword, + resourcePolicyPincode, + resourcePolicyHeaderAuth, + resourcePolicyRules, + resourcePolicyWhiteList, + rolePolicies, + userPolicies } from "@server/db"; import { resources, targets, sites } from "@server/db"; import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm"; @@ -30,6 +38,7 @@ import logger from "@server/logger"; import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; import { pickPort } from "@server/routers/target/helpers"; import { resourcePassword } from "@server/db"; +import { getUniqueResourcePolicyName } from "@server/db/names"; import { hashPassword } from "@server/auth/password"; import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; import { isValidRegionId } from "@server/db/regions"; @@ -242,163 +251,257 @@ export async function updateProxyResources( resourceData.maintenance = undefined; } - [resource] = await trx - .update(resources) - .set({ - name: resourceData.name || "Unnamed Resource", - protocol: protocol || "tcp", - http: http, - proxyPort: http ? null : resourceData["proxy-port"], - fullDomain: http ? resourceData["full-domain"] : null, - subdomain: domain ? domain.subdomain : null, - domainId: domain ? domain.domainId : null, - wildcard: domain ? domain.wildcard : false, - enabled: resourceEnabled, - sso: resourceData.auth?.["sso-enabled"] || false, - skipToIdpId: - resourceData.auth?.["auto-login-idp"] || null, - ssl: resourceSsl, - setHostHeader: resourceData["host-header"] || null, - tlsServerName: resourceData["tls-server-name"] || null, - emailWhitelistEnabled: resourceData.auth?.[ - "whitelist-users" - ] - ? resourceData.auth["whitelist-users"].length > 0 - : false, - headers: headers || null, - applyRules: - resourceData.rules && resourceData.rules.length > 0, - maintenanceModeEnabled: - resourceData.maintenance?.enabled, - maintenanceModeType: resourceData.maintenance?.type, - maintenanceTitle: resourceData.maintenance?.title, - maintenanceMessage: resourceData.maintenance?.message, - maintenanceEstimatedTime: - resourceData.maintenance?.["estimated-time"] - }) - .where( - eq(resources.resourceId, existingResource.resourceId) - ) - .returning(); + // Look up the admin role (needed for inline policy creation) + const [adminRole] = await trx + .select() + .from(roles) + .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) + .limit(1); - await trx - .delete(resourcePassword) - .where( - eq( - resourcePassword.resourceId, - existingResource.resourceId - ) - ); - if (resourceData.auth?.password) { - const passwordHash = await hashPassword( - resourceData.auth.password - ); - - await trx.insert(resourcePassword).values({ - resourceId: existingResource.resourceId, - passwordHash - }); + if (!adminRole) { + throw new Error(`Admin role not found`); } - await trx - .delete(resourcePincode) - .where( - eq( - resourcePincode.resourceId, - existingResource.resourceId + if (resourceData.policy) { + // SHARED POLICY MODE: look up shared policy by niceId + const [sharedPolicy] = await trx + .select() + .from(resourcePolicies) + .where( + and( + eq( + resourcePolicies.niceId, + resourceData.policy + ), + eq(resourcePolicies.orgId, orgId) + ) ) - ); - if (resourceData.auth?.pincode) { - const pincodeHash = await hashPassword( - resourceData.auth.pincode.toString() - ); + .limit(1); - await trx.insert(resourcePincode).values({ - resourceId: existingResource.resourceId, - pincodeHash, - digitLength: 6 - }); - } - - await trx - .delete(resourceHeaderAuth) - .where( - eq( - resourceHeaderAuth.resourceId, - existingResource.resourceId - ) - ); - - await trx - .delete(resourceHeaderAuthExtendedCompatibility) - .where( - eq( - resourceHeaderAuthExtendedCompatibility.resourceId, - existingResource.resourceId - ) - ); - - if (resourceData.auth?.["basic-auth"]) { - const headerAuthUser = - resourceData.auth?.["basic-auth"]?.user; - const headerAuthPassword = - resourceData.auth?.["basic-auth"]?.password; - const headerAuthExtendedCompatibility = - resourceData.auth?.["basic-auth"] - ?.extendedCompatibility; - if ( - headerAuthUser && - headerAuthPassword && - headerAuthExtendedCompatibility !== null - ) { - const headerAuthHash = await hashPassword( - Buffer.from( - `${headerAuthUser}:${headerAuthPassword}` - ).toString("base64") + if (!sharedPolicy) { + throw new Error( + `Shared policy not found: ${resourceData.policy} in org ${orgId}` ); - await Promise.all([ - trx.insert(resourceHeaderAuth).values({ - resourceId: existingResource.resourceId, - headerAuthHash - }), - trx - .insert(resourceHeaderAuthExtendedCompatibility) - .values({ - resourceId: existingResource.resourceId, - extendedCompatibilityIsActivated: - headerAuthExtendedCompatibility - }) - ]); } - } - if (resourceData.auth?.["sso-roles"]) { - const ssoRoles = resourceData.auth?.["sso-roles"]; - await syncRoleResources( - existingResource.resourceId, - ssoRoles, + [resource] = await trx + .update(resources) + .set({ + name: resourceData.name || "Unnamed Resource", + protocol: protocol || "tcp", + http: http, + proxyPort: http ? null : resourceData["proxy-port"], + fullDomain: http + ? resourceData["full-domain"] + : null, + subdomain: domain ? domain.subdomain : null, + domainId: domain ? domain.domainId : null, + wildcard: domain ? domain.wildcard : false, + enabled: resourceEnabled, + sso: resourceData.auth?.["sso-enabled"] || false, + skipToIdpId: + resourceData.auth?.["auto-login-idp"] || null, + ssl: resourceSsl, + setHostHeader: resourceData["host-header"] || null, + tlsServerName: + resourceData["tls-server-name"] || null, + emailWhitelistEnabled: resourceData.auth?.[ + "whitelist-users" + ] + ? resourceData.auth["whitelist-users"].length > + 0 + : false, + headers: headers || null, + applyRules: + resourceData.rules && + resourceData.rules.length > 0, + maintenanceModeEnabled: + resourceData.maintenance?.enabled, + maintenanceModeType: resourceData.maintenance?.type, + maintenanceTitle: resourceData.maintenance?.title, + maintenanceMessage: + resourceData.maintenance?.message, + maintenanceEstimatedTime: + resourceData.maintenance?.["estimated-time"], + resourcePolicyId: sharedPolicy.resourcePolicyId + }) + .where( + eq( + resources.resourceId, + existingResource.resourceId + ) + ) + .returning(); + + // Update OLD resource-level auth tables + await trx + .delete(resourcePassword) + .where( + eq( + resourcePassword.resourceId, + existingResource.resourceId + ) + ); + if (resourceData.auth?.password) { + const passwordHash = await hashPassword( + resourceData.auth.password + ); + await trx.insert(resourcePassword).values({ + resourceId: existingResource.resourceId, + passwordHash + }); + } + + await trx + .delete(resourcePincode) + .where( + eq( + resourcePincode.resourceId, + existingResource.resourceId + ) + ); + if (resourceData.auth?.pincode) { + const pincodeHash = await hashPassword( + resourceData.auth.pincode.toString() + ); + await trx.insert(resourcePincode).values({ + resourceId: existingResource.resourceId, + pincodeHash, + digitLength: 6 + }); + } + + await trx + .delete(resourceHeaderAuth) + .where( + eq( + resourceHeaderAuth.resourceId, + existingResource.resourceId + ) + ); + await trx + .delete(resourceHeaderAuthExtendedCompatibility) + .where( + eq( + resourceHeaderAuthExtendedCompatibility.resourceId, + existingResource.resourceId + ) + ); + if (resourceData.auth?.["basic-auth"]) { + const headerAuthUser = + resourceData.auth["basic-auth"]?.user; + const headerAuthPassword = + resourceData.auth["basic-auth"]?.password; + const headerAuthExtendedCompatibility = + resourceData.auth["basic-auth"] + ?.extendedCompatibility; + if ( + headerAuthUser && + headerAuthPassword && + headerAuthExtendedCompatibility !== null + ) { + const headerAuthHash = await hashPassword( + Buffer.from( + `${headerAuthUser}:${headerAuthPassword}` + ).toString("base64") + ); + await Promise.all([ + trx.insert(resourceHeaderAuth).values({ + resourceId: existingResource.resourceId, + headerAuthHash + }), + trx + .insert( + resourceHeaderAuthExtendedCompatibility + ) + .values({ + resourceId: existingResource.resourceId, + extendedCompatibilityIsActivated: + headerAuthExtendedCompatibility + }) + ]); + } + } + + if (resourceData.auth?.["sso-roles"]) { + await syncRoleResources( + existingResource.resourceId, + resourceData.auth["sso-roles"], + orgId, + trx + ); + } + + if (resourceData.auth?.["sso-users"]) { + await syncUserResources( + existingResource.resourceId, + resourceData.auth["sso-users"], + orgId, + trx + ); + } + + if (resourceData.auth?.["whitelist-users"]) { + await syncWhitelistUsers( + existingResource.resourceId, + resourceData.auth["whitelist-users"], + orgId, + trx + ); + } + } else { + // INLINE POLICY MODE: ensure inline policy exists + const inlinePolicyId = await ensureInlinePolicy( + existingResource.defaultResourcePolicyId, orgId, + resourceNiceId, + adminRole.roleId, trx ); - } - if (resourceData.auth?.["sso-users"]) { - const ssoUsers = resourceData.auth?.["sso-users"]; - await syncUserResources( - existingResource.resourceId, - ssoUsers, - orgId, - trx - ); - } + [resource] = await trx + .update(resources) + .set({ + name: resourceData.name || "Unnamed Resource", + protocol: protocol || "tcp", + http: http, + proxyPort: http ? null : resourceData["proxy-port"], + fullDomain: http + ? resourceData["full-domain"] + : null, + subdomain: domain ? domain.subdomain : null, + domainId: domain ? domain.domainId : null, + wildcard: domain ? domain.wildcard : false, + enabled: resourceEnabled, + ssl: resourceSsl, + setHostHeader: resourceData["host-header"] || null, + tlsServerName: + resourceData["tls-server-name"] || null, + headers: headers || null, + maintenanceModeEnabled: + resourceData.maintenance?.enabled, + maintenanceModeType: resourceData.maintenance?.type, + maintenanceTitle: resourceData.maintenance?.title, + maintenanceMessage: + resourceData.maintenance?.message, + maintenanceEstimatedTime: + resourceData.maintenance?.["estimated-time"], + resourcePolicyId: null, + defaultResourcePolicyId: inlinePolicyId + }) + .where( + eq( + resources.resourceId, + existingResource.resourceId + ) + ) + .returning(); - if (resourceData.auth?.["whitelist-users"]) { - const whitelistUsers = - resourceData.auth?.["whitelist-users"]; - await syncWhitelistUsers( - existingResource.resourceId, - whitelistUsers, + // Update inline policy auth fields and policy-level tables + await syncInlinePolicyAuth( + inlinePolicyId, orgId, + resourceData, trx ); } @@ -618,69 +721,88 @@ export async function updateProxyResources( } } - const existingRules = await trx - .select() - .from(resourceRules) - .where( - eq(resourceRules.resourceId, existingResource.resourceId) - ) - .orderBy(resourceRules.priority); + if (resourceData.policy) { + // SHARED POLICY MODE: sync rules into old resourceRules table + const existingRules = await trx + .select() + .from(resourceRules) + .where( + eq( + resourceRules.resourceId, + existingResource.resourceId + ) + ) + .orderBy(resourceRules.priority); - // Sync rules - for (const [index, rule] of resourceData.rules?.entries() || []) { - const intendedPriority = rule.priority ?? index + 1; - const existingRule = existingRules[index]; - if (existingRule) { - if ( - existingRule.action !== getRuleAction(rule.action) || - existingRule.match !== rule.match.toUpperCase() || - existingRule.value !== - getRuleValue( - rule.match.toUpperCase(), - rule.value - ) || - existingRule.priority !== intendedPriority - ) { - validateRule(rule); - await trx - .update(resourceRules) - .set({ - action: getRuleAction(rule.action), - match: rule.match.toUpperCase(), - value: getRuleValue( + // Sync rules + for (const [index, rule] of resourceData.rules?.entries() || + []) { + const intendedPriority = rule.priority ?? index + 1; + const existingRule = existingRules[index]; + if (existingRule) { + if ( + existingRule.action !== + getRuleAction(rule.action) || + existingRule.match !== rule.match.toUpperCase() || + existingRule.value !== + getRuleValue( rule.match.toUpperCase(), rule.value - ), - priority: intendedPriority - }) - .where( - eq(resourceRules.ruleId, existingRule.ruleId) - ); + ) || + existingRule.priority !== intendedPriority + ) { + validateRule(rule); + await trx + .update(resourceRules) + .set({ + action: getRuleAction(rule.action), + match: rule.match.toUpperCase(), + value: getRuleValue( + rule.match.toUpperCase(), + rule.value + ), + priority: intendedPriority + }) + .where( + eq( + resourceRules.ruleId, + existingRule.ruleId + ) + ); + } + } else { + validateRule(rule); + await trx.insert(resourceRules).values({ + resourceId: existingResource.resourceId, + action: getRuleAction(rule.action), + match: rule.match.toUpperCase(), + value: getRuleValue( + rule.match.toUpperCase(), + rule.value + ), + priority: intendedPriority + }); } - } else { - validateRule(rule); - await trx.insert(resourceRules).values({ - resourceId: existingResource.resourceId, - action: getRuleAction(rule.action), - match: rule.match.toUpperCase(), - value: getRuleValue( - rule.match.toUpperCase(), - rule.value - ), - priority: intendedPriority - }); } - } - if (existingRules.length > (resourceData.rules?.length || 0)) { - const rulesToDelete = existingRules.slice( - resourceData.rules?.length || 0 - ); - for (const rule of rulesToDelete) { - await trx - .delete(resourceRules) - .where(eq(resourceRules.ruleId, rule.ruleId)); + if (existingRules.length > (resourceData.rules?.length || 0)) { + const rulesToDelete = existingRules.slice( + resourceData.rules?.length || 0 + ); + for (const rule of rulesToDelete) { + await trx + .delete(resourceRules) + .where(eq(resourceRules.ruleId, rule.ruleId)); + } } + } else { + // INLINE POLICY MODE: sync rules into policy-level table + const inlinePolicyId = resource!.defaultResourcePolicyId!; + await syncInlinePolicyRules( + inlinePolicyId, + resourceData.rules || [], + trx + ); } logger.debug(`Updated resource ${existingResource.resourceId}`); @@ -704,6 +826,58 @@ export async function updateProxyResources( resourceData.maintenance = undefined; } + // Look up admin role (needed for inline policy and roleResources) + const [adminRole] = await trx + .select() + .from(roles) + .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) + .limit(1); + + if (!adminRole) { + throw new Error(`Admin role not found`); + } + + // Always create an inline policy for the resource + const policyNiceId = await getUniqueResourcePolicyName(orgId); + const [inlinePolicy] = await trx + .insert(resourcePolicies) + .values({ + niceId: policyNiceId, + orgId, + name: `default policy for ${resourceNiceId}`, + sso: true, + scope: "resource" + }) + .returning(); + + // Make the inline policy visible to the admin role + await trx.insert(rolePolicies).values({ + roleId: adminRole.roleId, + resourcePolicyId: inlinePolicy.resourcePolicyId + }); + + // Determine the active shared policy (if provided) + let sharedPolicyId: number | null = null; + if (resourceData.policy) { + const [sharedPolicy] = await trx + .select() + .from(resourcePolicies) + .where( + and( + eq(resourcePolicies.niceId, resourceData.policy), + eq(resourcePolicies.orgId, orgId) + ) + ) + .limit(1); + + if (!sharedPolicy) { + throw new Error( + `Shared policy not found: ${resourceData.policy} in org ${orgId}` + ); + } + sharedPolicyId = sharedPolicy.resourcePolicyId; + } + // Create new resource const [newResource] = await trx .insert(resources) @@ -719,123 +893,156 @@ export async function updateProxyResources( domainId: domain ? domain.domainId : null, wildcard: domain ? domain.wildcard : false, enabled: resourceEnabled, - sso: resourceData.auth?.["sso-enabled"] || false, - skipToIdpId: resourceData.auth?.["auto-login-idp"] || null, setHostHeader: resourceData["host-header"] || null, tlsServerName: resourceData["tls-server-name"] || null, ssl: resourceSsl, headers: headers || null, - applyRules: - resourceData.rules && resourceData.rules.length > 0, maintenanceModeEnabled: resourceData.maintenance?.enabled, maintenanceModeType: resourceData.maintenance?.type, maintenanceTitle: resourceData.maintenance?.title, maintenanceMessage: resourceData.maintenance?.message, maintenanceEstimatedTime: - resourceData.maintenance?.["estimated-time"] + resourceData.maintenance?.["estimated-time"], + defaultResourcePolicyId: inlinePolicy.resourcePolicyId, + resourcePolicyId: sharedPolicyId, + // Only set these resource-level fields when using a shared policy + ...(sharedPolicyId + ? { + sso: resourceData.auth?.["sso-enabled"] || false, + skipToIdpId: + resourceData.auth?.["auto-login-idp"] || null, + emailWhitelistEnabled: resourceData.auth?.[ + "whitelist-users" + ] + ? resourceData.auth["whitelist-users"] + .length > 0 + : false, + applyRules: + resourceData.rules && + resourceData.rules.length > 0 + } + : {}) }) .returning(); - if (resourceData.auth?.password) { - const passwordHash = await hashPassword( - resourceData.auth.password - ); - - await trx.insert(resourcePassword).values({ - resourceId: newResource.resourceId, - passwordHash - }); - } - - if (resourceData.auth?.pincode) { - const pincodeHash = await hashPassword( - resourceData.auth.pincode.toString() - ); - - await trx.insert(resourcePincode).values({ - resourceId: newResource.resourceId, - pincodeHash, - digitLength: 6 - }); - } - - if (resourceData.auth?.["basic-auth"]) { - const headerAuthUser = resourceData.auth?.["basic-auth"]?.user; - const headerAuthPassword = - resourceData.auth?.["basic-auth"]?.password; - const headerAuthExtendedCompatibility = - resourceData.auth?.["basic-auth"]?.extendedCompatibility; - - if ( - headerAuthUser && - headerAuthPassword && - headerAuthExtendedCompatibility !== null - ) { - const headerAuthHash = await hashPassword( - Buffer.from( - `${headerAuthUser}:${headerAuthPassword}` - ).toString("base64") - ); - - await Promise.all([ - trx.insert(resourceHeaderAuth).values({ - resourceId: newResource.resourceId, - headerAuthHash - }), - trx - .insert(resourceHeaderAuthExtendedCompatibility) - .values({ - resourceId: newResource.resourceId, - extendedCompatibilityIsActivated: - headerAuthExtendedCompatibility - }) - ]); - } - } - resource = newResource; - const [adminRole] = await trx - .select() - .from(roles) - .where(and(eq(roles.isAdmin, true), eq(roles.orgId, orgId))) - .limit(1); - - if (!adminRole) { - throw new Error(`Admin role not found`); - } - await trx.insert(roleResources).values({ roleId: adminRole.roleId, resourceId: newResource.resourceId }); - if (resourceData.auth?.["sso-roles"]) { - const ssoRoles = resourceData.auth?.["sso-roles"]; - await syncRoleResources( - newResource.resourceId, - ssoRoles, + if (sharedPolicyId) { + // SHARED POLICY MODE: update OLD resource-level auth tables + if (resourceData.auth?.password) { + const passwordHash = await hashPassword( + resourceData.auth.password + ); + await trx.insert(resourcePassword).values({ + resourceId: newResource.resourceId, + passwordHash + }); + } + + if (resourceData.auth?.pincode) { + const pincodeHash = await hashPassword( + resourceData.auth.pincode.toString() + ); + await trx.insert(resourcePincode).values({ + resourceId: newResource.resourceId, + pincodeHash, + digitLength: 6 + }); + } + + if (resourceData.auth?.["basic-auth"]) { + const headerAuthUser = + resourceData.auth["basic-auth"]?.user; + const headerAuthPassword = + resourceData.auth["basic-auth"]?.password; + const headerAuthExtendedCompatibility = + resourceData.auth["basic-auth"]?.extendedCompatibility; + if ( + headerAuthUser && + headerAuthPassword && + headerAuthExtendedCompatibility !== null + ) { + const headerAuthHash = await hashPassword( + Buffer.from( + `${headerAuthUser}:${headerAuthPassword}` + ).toString("base64") + ); + await Promise.all([ + trx.insert(resourceHeaderAuth).values({ + resourceId: newResource.resourceId, + headerAuthHash + }), + trx + .insert(resourceHeaderAuthExtendedCompatibility) + .values({ + resourceId: newResource.resourceId, + extendedCompatibilityIsActivated: + headerAuthExtendedCompatibility + }) + ]); + } + } + + if (resourceData.auth?.["sso-roles"]) { + await syncRoleResources( + newResource.resourceId, + resourceData.auth["sso-roles"], + orgId, + trx + ); + } + + if (resourceData.auth?.["sso-users"]) { + await syncUserResources( + newResource.resourceId, + resourceData.auth["sso-users"], + orgId, + trx + ); + } + + if (resourceData.auth?.["whitelist-users"]) { + await syncWhitelistUsers( + newResource.resourceId, + resourceData.auth["whitelist-users"], + orgId, + trx + ); + } + + // Rules into OLD resourceRules table + for (const [index, rule] of resourceData.rules?.entries() || + []) { + validateRule(rule); + await trx.insert(resourceRules).values({ + resourceId: newResource.resourceId, + action: getRuleAction(rule.action), + match: rule.match.toUpperCase(), + value: getRuleValue( + rule.match.toUpperCase(), + rule.value + ), + priority: rule.priority ?? index + 1 + }); + } + } else { + // INLINE POLICY MODE: update the inline policy auth fields + await syncInlinePolicyAuth( + inlinePolicy.resourcePolicyId, orgId, + resourceData, trx ); - } - if (resourceData.auth?.["sso-users"]) { - const ssoUsers = resourceData.auth?.["sso-users"]; - await syncUserResources( - newResource.resourceId, - ssoUsers, - orgId, - trx - ); - } - - if (resourceData.auth?.["whitelist-users"]) { - const whitelistUsers = resourceData.auth?.["whitelist-users"]; - await syncWhitelistUsers( - newResource.resourceId, - whitelistUsers, - orgId, + // Rules into policy-level table + await syncInlinePolicyRules( + inlinePolicy.resourcePolicyId, + resourceData.rules || [], trx ); } @@ -849,17 +1056,6 @@ export async function updateProxyResources( await createTarget(newResource.resourceId, targetData); } - for (const [index, rule] of resourceData.rules?.entries() || []) { - validateRule(rule); - await trx.insert(resourceRules).values({ - resourceId: newResource.resourceId, - action: getRuleAction(rule.action), - match: rule.match.toUpperCase(), - value: getRuleValue(rule.match.toUpperCase(), rule.value), - priority: rule.priority ?? index + 1 - }); - } - logger.debug(`Created resource ${newResource.resourceId}`); } @@ -1097,6 +1293,399 @@ async function syncWhitelistUsers( } } +/** + * Creates an inline resourcePolicy if one doesn't exist yet, and returns its ID. + * Makes the policy visible to the admin role via rolePolicies. + */ +async function ensureInlinePolicy( + existingPolicyId: number | null | undefined, + orgId: string, + resourceNiceId: string, + adminRoleId: number, + trx: Transaction +): Promise { + if (existingPolicyId) { + return existingPolicyId; + } + + const policyNiceId = await getUniqueResourcePolicyName(orgId); + const [newPolicy] = await trx + .insert(resourcePolicies) + .values({ + niceId: policyNiceId, + orgId, + name: `default policy for ${resourceNiceId}`, + sso: true, + scope: "resource" + }) + .returning(); + + await trx.insert(rolePolicies).values({ + roleId: adminRoleId, + resourcePolicyId: newPolicy.resourcePolicyId + }); + + return newPolicy.resourcePolicyId; +} + +/** + * Updates the inline policy's auth-related fields and policy-level tables + * (used when no shared policy is specified in the blueprint). + */ +async function syncInlinePolicyAuth( + policyId: number, + orgId: string, + resourceData: any, + trx: Transaction +) { + // Update policy-level SSO/whitelist/applyRules fields + await trx + .update(resourcePolicies) + .set({ + sso: resourceData.auth?.["sso-enabled"] ?? false, + idpId: resourceData.auth?.["auto-login-idp"] || null, + emailWhitelistEnabled: resourceData.auth?.["whitelist-users"] + ? resourceData.auth["whitelist-users"].length > 0 + : false, + applyRules: !!(resourceData.rules && resourceData.rules.length > 0) + }) + .where(eq(resourcePolicies.resourcePolicyId, policyId)); + + // Password + await trx + .delete(resourcePolicyPassword) + .where(eq(resourcePolicyPassword.resourcePolicyId, policyId)); + if (resourceData.auth?.password) { + const passwordHash = await hashPassword(resourceData.auth.password); + await trx.insert(resourcePolicyPassword).values({ + resourcePolicyId: policyId, + passwordHash + }); + } + + // Pincode + await trx + .delete(resourcePolicyPincode) + .where(eq(resourcePolicyPincode.resourcePolicyId, policyId)); + if (resourceData.auth?.pincode) { + const pincodeHash = await hashPassword( + resourceData.auth.pincode.toString() + ); + await trx.insert(resourcePolicyPincode).values({ + resourcePolicyId: policyId, + pincodeHash, + digitLength: 6 + }); + } + + // Header auth + await trx + .delete(resourcePolicyHeaderAuth) + .where(eq(resourcePolicyHeaderAuth.resourcePolicyId, policyId)); + if (resourceData.auth?.["basic-auth"]) { + const headerAuthUser = resourceData.auth["basic-auth"]?.user; + const headerAuthPassword = resourceData.auth["basic-auth"]?.password; + const headerAuthExtendedCompatibility = + resourceData.auth["basic-auth"]?.extendedCompatibility; + if ( + headerAuthUser && + headerAuthPassword && + headerAuthExtendedCompatibility !== null + ) { + const headerAuthHash = await hashPassword( + Buffer.from(`${headerAuthUser}:${headerAuthPassword}`).toString( + "base64" + ) + ); + await trx.insert(resourcePolicyHeaderAuth).values({ + resourcePolicyId: policyId, + headerAuthHash, + extendedCompatibility: headerAuthExtendedCompatibility + }); + } + } + + // SSO roles → rolePolicies + if (resourceData.auth?.["sso-roles"] !== undefined) { + await syncRolePolicies( + policyId, + resourceData.auth["sso-roles"], + orgId, + trx + ); + } + + // SSO users → userPolicies + if (resourceData.auth?.["sso-users"] !== undefined) { + await syncUserPolicies( + policyId, + resourceData.auth["sso-users"], + orgId, + trx + ); + } + + // Whitelist → resourcePolicyWhiteList + if (resourceData.auth?.["whitelist-users"] !== undefined) { + await syncWhitelistPolicyUsers( + policyId, + resourceData.auth["whitelist-users"], + orgId, + trx + ); + } +} + +/** + * Syncs rules into the resourcePolicyRules table (inline policy mode). + */ +async function syncInlinePolicyRules( + policyId: number, + rules: any[], + trx: Transaction +) { + const existingRules = await trx + .select() + .from(resourcePolicyRules) + .where(eq(resourcePolicyRules.resourcePolicyId, policyId)) + .orderBy(resourcePolicyRules.priority); + + for (const [index, rule] of rules.entries()) { + const intendedPriority = rule.priority ?? index + 1; + const existingRule = existingRules[index]; + if (existingRule) { + if ( + existingRule.action !== getRuleAction(rule.action) || + existingRule.match !== rule.match.toUpperCase() || + existingRule.value !== + getRuleValue(rule.match.toUpperCase(), rule.value) || + existingRule.priority !== intendedPriority + ) { + validateRule(rule); + await trx + .update(resourcePolicyRules) + .set({ + action: getRuleAction(rule.action) as + | "ACCEPT" + | "DROP" + | "PASS", + match: rule.match.toUpperCase() as + | "CIDR" + | "IP" + | "PATH", + value: getRuleValue( + rule.match.toUpperCase(), + rule.value + ), + priority: intendedPriority + }) + .where(eq(resourcePolicyRules.ruleId, existingRule.ruleId)); + } + } else { + validateRule(rule); + await trx.insert(resourcePolicyRules).values({ + resourcePolicyId: policyId, + action: getRuleAction(rule.action) as + | "ACCEPT" + | "DROP" + | "PASS", + match: rule.match.toUpperCase() as "CIDR" | "IP" | "PATH", + value: getRuleValue(rule.match.toUpperCase(), rule.value), + priority: intendedPriority + }); + } + } + + if (existingRules.length > rules.length) { + const rulesToDelete = existingRules.slice(rules.length); + for (const rule of rulesToDelete) { + await trx + .delete(resourcePolicyRules) + .where(eq(resourcePolicyRules.ruleId, rule.ruleId)); + } + } +} + +/** + * Syncs SSO roles to the rolePolicies table (inline policy mode). + */ +async function syncRolePolicies( + policyId: number, + ssoRoles: string[], + orgId: string, + trx: Transaction +) { + const existingRolePoliciesList = await trx + .select() + .from(rolePolicies) + .where(eq(rolePolicies.resourcePolicyId, policyId)); + + for (const roleName of ssoRoles) { + const [role] = await trx + .select() + .from(roles) + .where(and(eq(roles.name, roleName), eq(roles.orgId, orgId))) + .limit(1); + + if (!role) { + throw new Error(`Role not found: ${roleName} in org ${orgId}`); + } + + if (role.isAdmin) { + continue; + } + + const existingRolePolicy = existingRolePoliciesList.find( + (rp) => rp.roleId === role.roleId + ); + + if (!existingRolePolicy) { + await trx.insert(rolePolicies).values({ + roleId: role.roleId, + resourcePolicyId: policyId + }); + } + } + + for (const existingRolePolicy of existingRolePoliciesList) { + const [role] = await trx + .select() + .from(roles) + .where(eq(roles.roleId, existingRolePolicy.roleId)) + .limit(1); + + if (role?.isAdmin) { + continue; + } + + if (role && !ssoRoles.includes(role.name)) { + await trx + .delete(rolePolicies) + .where( + and( + eq(rolePolicies.roleId, existingRolePolicy.roleId), + eq(rolePolicies.resourcePolicyId, policyId) + ) + ); + } + } +} + +/** + * Syncs SSO users to the userPolicies table (inline policy mode). + */ +async function syncUserPolicies( + policyId: number, + ssoUsers: string[], + orgId: string, + trx: Transaction +) { + const existingUserPoliciesList = await trx + .select() + .from(userPolicies) + .where(eq(userPolicies.resourcePolicyId, policyId)); + + for (const username of ssoUsers) { + 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.user.userId + ); + + if (!existingUserPolicy) { + await trx.insert(userPolicies).values({ + userId: user.user.userId, + resourcePolicyId: policyId + }); + } + } + + for (const existingUserPolicy of existingUserPoliciesList) { + const [user] = await trx + .select() + .from(users) + .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) + .where( + and( + eq(users.userId, existingUserPolicy.userId), + eq(userOrgs.orgId, orgId) + ) + ) + .limit(1); + + if ( + user && + user.user.username && + !ssoUsers.includes(user.user.username) + ) { + await trx + .delete(userPolicies) + .where( + and( + eq(userPolicies.userId, existingUserPolicy.userId), + eq(userPolicies.resourcePolicyId, policyId) + ) + ); + } + } +} + +/** + * Syncs whitelist emails to the resourcePolicyWhiteList table (inline policy mode). + */ +async function syncWhitelistPolicyUsers( + policyId: number, + whitelistUsers: string[], + orgId: string, + trx: Transaction +) { + const existingWhitelist = await trx + .select() + .from(resourcePolicyWhiteList) + .where(eq(resourcePolicyWhiteList.resourcePolicyId, policyId)); + + for (const email of whitelistUsers) { + const existingEntry = existingWhitelist.find((w) => w.email === email); + + if (!existingEntry) { + await trx.insert(resourcePolicyWhiteList).values({ + email, + resourcePolicyId: policyId + }); + } + } + + for (const existingEntry of existingWhitelist) { + if (!whitelistUsers.includes(existingEntry.email)) { + await trx + .delete(resourcePolicyWhiteList) + .where( + and( + eq( + resourcePolicyWhiteList.whitelistId, + existingEntry.whitelistId + ), + eq(resourcePolicyWhiteList.resourcePolicyId, policyId) + ) + ); + } + } +} + function checkIfHealthcheckChanged( existing: TargetHealthCheck | undefined, incoming: TargetHealthCheck | undefined diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index 13f4caa8f..7a8cfcfb3 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -162,9 +162,10 @@ export const HeaderSchema = z.object({ }); // Schema for individual resource -export const ResourceSchema = z +export const PublicResourceSchema = z .object({ name: z.string().optional(), + policy: z.string().optional(), protocol: z.enum(["http", "tcp", "udp"]).optional(), ssl: z.boolean().optional(), scheme: z.enum(["http", "https"]).optional(), @@ -340,7 +341,8 @@ export const ResourceSchema = z if (parts.includes("*", 1)) return false; // no further wildcards if (parts.length < 3) return false; // need at least *.label.tld - const labelRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/; + const labelRegex = + /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$|^[a-zA-Z0-9]$/; return parts.slice(1).every((label) => labelRegex.test(label)); }, { @@ -354,7 +356,7 @@ export function isTargetsOnlyResource(resource: any): boolean { return Object.keys(resource).length === 1 && resource.targets; } -export const ClientResourceSchema = z +export const PrivateResourceSchema = z .object({ name: z.string().min(1).max(255), mode: z.enum(["host", "cidr", "http"]), @@ -435,19 +437,19 @@ export const ClientResourceSchema = z export const ConfigSchema = z .object({ "proxy-resources": z - .record(z.string(), ResourceSchema) + .record(z.string(), PublicResourceSchema) .optional() .prefault({}), "public-resources": z - .record(z.string(), ResourceSchema) + .record(z.string(), PublicResourceSchema) .optional() .prefault({}), "client-resources": z - .record(z.string(), ClientResourceSchema) + .record(z.string(), PrivateResourceSchema) .optional() .prefault({}), "private-resources": z - .record(z.string(), ClientResourceSchema) + .record(z.string(), PrivateResourceSchema) .optional() .prefault({}), sites: z.record(z.string(), SiteSchema).optional().prefault({}) @@ -472,10 +474,13 @@ export const ConfigSchema = z } return data as { - "proxy-resources": Record>; + "proxy-resources": Record< + string, + z.infer + >; "client-resources": Record< string, - z.infer + z.infer >; sites: Record>; }; @@ -614,5 +619,5 @@ export const ConfigSchema = z // Type inference from the schema export type Site = z.infer; export type Target = z.infer; -export type Resource = z.infer; +export type Resource = z.infer; export type Config = z.infer; From b33a6e6face563f8e09f6c670756cf351f10a166 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 4 May 2026 20:54:43 -0700 Subject: [PATCH 100/771] Wipe the old tables if you are using inline --- server/lib/blueprints/proxyResources.ts | 55 +++++++ server/routers/resource/updateResource.ts | 178 +++++++++++++++------- 2 files changed, 180 insertions(+), 53 deletions(-) diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index cb32c476d..51f9be55e 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -497,6 +497,50 @@ export async function updateProxyResources( ) .returning(); + // Clear the old resource-level auth tables (not used in inline policy mode) + await Promise.all([ + trx + .delete(resourcePassword) + .where( + eq( + resourcePassword.resourceId, + existingResource.resourceId + ) + ), + trx + .delete(resourcePincode) + .where( + eq( + resourcePincode.resourceId, + existingResource.resourceId + ) + ), + trx + .delete(resourceHeaderAuth) + .where( + eq( + resourceHeaderAuth.resourceId, + existingResource.resourceId + ) + ), + trx + .delete(resourceHeaderAuthExtendedCompatibility) + .where( + eq( + resourceHeaderAuthExtendedCompatibility.resourceId, + existingResource.resourceId + ) + ), + trx + .delete(resourceWhitelist) + .where( + eq( + resourceWhitelist.resourceId, + existingResource.resourceId + ) + ) + ]); + // Update inline policy auth fields and policy-level tables await syncInlinePolicyAuth( inlinePolicyId, @@ -798,6 +842,17 @@ export async function updateProxyResources( } else { // INLINE POLICY MODE: sync rules into policy-level table const inlinePolicyId = resource!.defaultResourcePolicyId!; + + // Clear the old resource-level rules table + await trx + .delete(resourceRules) + .where( + eq( + resourceRules.resourceId, + existingResource.resourceId + ) + ); + await syncInlinePolicyRules( inlinePolicyId, resourceData.rules || [], diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index b56469167..71e5f8e6e 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -1,6 +1,16 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, domainNamespaces, loginPage } from "@server/db"; +import { + db, + domainNamespaces, + loginPage, + resourceHeaderAuth, + resourceHeaderAuthExtendedCompatibility, + resourcePassword, + resourcePincode, + resourceRules, + resourceWhitelist +} from "@server/db"; import { domains, Org, @@ -569,60 +579,122 @@ async function updateRawResource( } const updateData = parsedBody.data; + let updatedResource: Resource | null = null; - if (updateData.resourcePolicyId != null) { - const [existingPolicy] = await db - .select() - .from(resourcePolicies) - .where( - eq( - resourcePolicies.resourcePolicyId, - updateData.resourcePolicyId - ) - ) - .limit(1); - - if (!existingPolicy) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource policy with ID ${updateData.resourcePolicyId} not found` - ) - ); - } - } - - if (updateData.niceId) { - const [existingResource] = await db - .select() - .from(resources) - .where( - and( - eq(resources.niceId, updateData.niceId), - eq(resources.orgId, resource.orgId) - ) - ); - - if ( - existingResource && - existingResource.resourceId !== resource.resourceId - ) { - return next( - createHttpError( - HttpCode.CONFLICT, - `A resource with niceId "${updateData.niceId}" already exists` - ) - ); - } - } - - const updatedResource = await db - .update(resources) - .set(updateData) + const [existingResource] = await db + .select() + .from(resources) .where(eq(resources.resourceId, resource.resourceId)) - .returning(); + .limit(1); - if (updatedResource.length === 0) { + await db.transaction(async (trx) => { + if (updateData.resourcePolicyId != null) { + const [existingPolicy] = await trx + .select() + .from(resourcePolicies) + .where( + eq( + resourcePolicies.resourcePolicyId, + updateData.resourcePolicyId + ) + ) + .limit(1); + + if (!existingPolicy) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Resource policy with ID ${updateData.resourcePolicyId} not found` + ) + ); + } + } else { + // we are in an inline policy and we need to clear out the old tables + await Promise.all([ + trx + .delete(resourcePassword) + .where( + eq( + resourcePassword.resourceId, + existingResource.resourceId + ) + ), + trx + .delete(resourcePincode) + .where( + eq( + resourcePincode.resourceId, + existingResource.resourceId + ) + ), + trx + .delete(resourceHeaderAuth) + .where( + eq( + resourceHeaderAuth.resourceId, + existingResource.resourceId + ) + ), + trx + .delete(resourceHeaderAuthExtendedCompatibility) + .where( + eq( + resourceHeaderAuthExtendedCompatibility.resourceId, + existingResource.resourceId + ) + ), + trx + .delete(resourceWhitelist) + .where( + eq( + resourceWhitelist.resourceId, + existingResource.resourceId + ) + ), + + trx + .delete(resourceRules) + .where( + eq( + resourceRules.resourceId, + existingResource.resourceId + ) + ) + ]); + } + + if (updateData.niceId) { + const [existingResourceConflict] = await trx + .select() + .from(resources) + .where( + and( + eq(resources.niceId, updateData.niceId), + eq(resources.orgId, resource.orgId) + ) + ); + + if ( + existingResourceConflict && + existingResourceConflict.resourceId !== resource.resourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + `A resource with niceId "${updateData.niceId}" already exists` + ) + ); + } + } + + [updatedResource] = await trx + .update(resources) + .set(updateData) + .where(eq(resources.resourceId, resource.resourceId)) + .returning(); + }); + + if (!updatedResource) { return next( createHttpError( HttpCode.NOT_FOUND, @@ -632,7 +704,7 @@ async function updateRawResource( } return response(res, { - data: updatedResource[0], + data: updatedResource, success: true, error: false, message: "Non-http Resource updated successfully", From 3253d60900dcd86dce4529cf4dc1204f483e088e Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 5 May 2026 20:53:16 +0200 Subject: [PATCH 101/771] =?UTF-8?q?=F0=9F=9A=A7=20Add=20CRUD=20endpoints?= =?UTF-8?q?=20and=20tables=20for=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/auth/actions.ts | 3 + server/db/pg/schema/schema.ts | 85 ++++++++-- server/private/routers/external.ts | 25 +++ .../private/routers/labels/createOrgLabel.ts | 144 ++++++++++++++++ server/private/routers/labels/index.ts | 16 ++ .../private/routers/labels/listOrgLabels.ts | 155 ++++++++++++++++++ .../private/routers/labels/updateOrgLabel.ts | 109 ++++++++++++ server/routers/labels/types.ts | 10 ++ 8 files changed, 531 insertions(+), 16 deletions(-) create mode 100644 server/private/routers/labels/createOrgLabel.ts create mode 100644 server/private/routers/labels/index.ts create mode 100644 server/private/routers/labels/listOrgLabels.ts create mode 100644 server/private/routers/labels/updateOrgLabel.ts create mode 100644 server/routers/labels/types.ts diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 9ba1b5bce..f6bfa2549 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -148,6 +148,9 @@ export enum ActionsEnum { updateAlertRule = "updateAlertRule", deleteAlertRule = "deleteAlertRule", listAlertRules = "listAlertRules", + listOrgLabels = "listOrgLabels", + createOrgLabel = "createOrgLabel", + updateOrgLabel = "updateOrgLabel", getAlertRule = "getAlertRule", createHealthCheck = "createHealthCheck", updateHealthCheck = "updateHealthCheck", diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 7fbcef621..0a4066db3 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -162,6 +162,45 @@ export const resources = pgTable("resources", { wildcard: boolean("wildcard").notNull().default(false) }); +export const labels = pgTable("labels", { + labelId: serial("labelId").primaryKey(), + name: varchar("name").notNull(), + color: varchar("color").notNull(), + orgId: varchar("orgId") + .references(() => orgs.orgId, { + onDelete: "cascade" + }) + .notNull() +}); + +export const siteLabels = pgTable("siteLabels", { + siteLabelId: serial("siteLabelId").primaryKey(), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), + labelId: integer("labelId") + .references(() => labels.labelId, { + onDelete: "cascade" + }) + .notNull() +}); + +export const resourceLabels = pgTable("resourceLabels", { + resourceLabelId: serial("resourceLabelId").primaryKey(), + resourceId: integer("resourceId") + .references(() => resources.resourceId, { + onDelete: "cascade" + }) + .notNull(), + labelId: integer("labelId") + .references(() => labels.labelId, { + onDelete: "cascade" + }) + .notNull() +}); + export const targets = pgTable("targets", { targetId: serial("targetId").primaryKey(), resourceId: integer("resourceId") @@ -196,9 +235,11 @@ export const targetHealthCheck = pgTable("targetHealthCheck", { onDelete: "cascade" }) .notNull(), - siteId: integer("siteId").references(() => sites.siteId, { - onDelete: "cascade" - }).notNull(), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), name: varchar("name"), hcEnabled: boolean("hcEnabled").notNull().default(false), hcPath: varchar("hcPath"), @@ -1097,19 +1138,30 @@ export const roundTripMessageTracker = pgTable("roundTripMessageTracker", { complete: boolean("complete").notNull().default(false) }); -export const statusHistory = pgTable("statusHistory", { - id: serial("id").primaryKey(), - entityType: varchar("entityType").notNull(), - entityId: integer("entityId").notNull(), - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - status: varchar("status").notNull(), - timestamp: integer("timestamp").notNull(), -}, (table) => [ - index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp), - index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp), -]); +export const statusHistory = pgTable( + "statusHistory", + { + id: serial("id").primaryKey(), + entityType: varchar("entityType").notNull(), + entityId: integer("entityId").notNull(), + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + status: varchar("status").notNull(), + timestamp: integer("timestamp").notNull() + }, + (table) => [ + index("idx_statusHistory_entity").on( + table.entityType, + table.entityId, + table.timestamp + ), + index("idx_statusHistory_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); export type Org = InferSelectModel; export type User = InferSelectModel; @@ -1179,3 +1231,4 @@ export type RoundTripMessageTracker = InferSelectModel< >; export type Network = InferSelectModel; export type StatusHistory = InferSelectModel; +export type Label = InferSelectModel; diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index a2667daa1..1b8c1ef4c 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -31,6 +31,7 @@ import * as siteProvisioning from "#private/routers/siteProvisioning"; import * as eventStreamingDestination from "#private/routers/eventStreamingDestination"; import * as alertRule from "#private/routers/alertRule"; import * as healthChecks from "#private/routers/healthChecks"; +import * as labels from "#private/routers/labels"; import { verifyOrgAccess, @@ -732,6 +733,30 @@ authenticated.get( alertRule.getAlertRule ); +authenticated.get( + "/org/:orgId/labels", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.listOrgLabels), + labels.listOrgLabels +); + +authenticated.post( + "/org/:orgId/labels", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.createOrgLabel), + labels.createOrgLabel +); + +authenticated.patch( + "/org/:orgId/label/:labelId", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.updateOrgLabel), + labels.updateOrgLabel +); + authenticated.get( "/org/:orgId/health-checks", verifyValidLicense, diff --git a/server/private/routers/labels/createOrgLabel.ts b/server/private/routers/labels/createOrgLabel.ts new file mode 100644 index 000000000..1439018d2 --- /dev/null +++ b/server/private/routers/labels/createOrgLabel.ts @@ -0,0 +1,144 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ +import { + db, + labels, + resourceLabels, + resources, + siteLabels, + sites, + type Label +} from "@server/db"; +import response from "@server/lib/response"; +import logger from "@server/logger"; +import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; +import HttpCode from "@server/types/HttpCode"; +import { eq } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; + +const paramsSchema = z.strictObject({ + orgId: z.string().nonempty() +}); + +const bodySchema = z.strictObject({ + name: z.string().nonempty(), + color: z + .string() + .regex(/^#?([0-9a-f]{6}|[0-9a-f]{3})$/i) + .nonempty(), + siteId: z.number().int().optional(), + resourceId: z.number().int().optional() +}); + +export async function createOrgLabel( + req: Request, + res: Response, + next: NextFunction +) { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { name, color, siteId, resourceId } = parsedBody.data; + + if (siteId) { + const siteCount = await db.$count(sites, eq(sites.siteId, siteId)); + + if (siteCount === 0) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Site with Id ${siteId} doesn't exist.` + ) + ); + } + } + + if (resourceId) { + const resourceCount = await db.$count( + sites, + eq(resources.resourceId, resourceId) + ); + + if (resourceCount === 0) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Resource with Id ${resourceId} doesn't exist.` + ) + ); + } + } + + const label = await db.transaction(async (tx) => { + const [label] = await tx + .insert(labels) + .values({ + name, + color, + orgId + }) + .returning(); + + if (siteId) { + await tx.insert(siteLabels).values({ + siteId, + labelId: label.labelId + }); + } + + if (resourceId) { + await tx.insert(resourceLabels).values({ + resourceId, + labelId: label.labelId + }); + } + return label; + }); + + return response(res, { + data: { label }, + success: true, + error: false, + message: "Org Label created successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/labels/index.ts b/server/private/routers/labels/index.ts new file mode 100644 index 000000000..4c7e5f432 --- /dev/null +++ b/server/private/routers/labels/index.ts @@ -0,0 +1,16 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +export * from "./listOrgLabels"; +export * from "./createOrgLabel"; +export * from "./updateOrgLabel"; diff --git a/server/private/routers/labels/listOrgLabels.ts b/server/private/routers/labels/listOrgLabels.ts new file mode 100644 index 000000000..dc2b50017 --- /dev/null +++ b/server/private/routers/labels/listOrgLabels.ts @@ -0,0 +1,155 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { db, labels } from "@server/db"; +import response from "@server/lib/response"; +import logger from "@server/logger"; +import type { ListOrgLabelsResponse } from "@server/routers/labels/types"; +import HttpCode from "@server/types/HttpCode"; +import { and, asc, eq, like, sql } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; + +const paramsSchema = z.strictObject({ + orgId: z.string().nonempty() +}); + +const listLabelsSchema = z.object({ + pageSize: z.coerce + .number() // for prettier formatting + .int() + .positive() + .optional() + .catch(20) + .default(20) + .openapi({ + type: "integer", + default: 20, + description: "Number of items per page" + }), + page: z.coerce + .number() // for prettier formatting + .int() + .min(0) + .optional() + .catch(1) + .default(1) + .openapi({ + type: "integer", + default: 1, + description: "Page number to retrieve" + }), + query: z.string().optional() +}); + +function queryLabelsBase() { + return db + .select({ + labelId: labels.labelId, + name: labels.name, + color: labels.color + }) + .from(labels); +} + +export async function listOrgLabels( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedQuery = listLabelsSchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error) + ) + ); + } + + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error) + ) + ); + } + const { orgId } = parsedParams.data; + + if (req.user && orgId && orgId !== req.userOrgId) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "User does not have access to this organization" + ) + ); + } + + const { pageSize, page, query } = parsedQuery.data; + + const conditions = [and(eq(labels.orgId, orgId))]; + + if (query) { + conditions.push( + like( + sql`LOWER(${labels.name})`, + "%" + query.toLowerCase() + "%" + ) + ); + } + + const baseQuery = queryLabelsBase().where(and(...conditions)); + + // we need to add `as` so that drizzle filters the result as a subquery + const countQuery = db.$count( + queryLabelsBase() + .where(and(...conditions)) + .as("filtered_labels") + ); + + const labelListQuery = baseQuery + .limit(pageSize) + .offset(pageSize * (page - 1)) + .orderBy(asc(labels.name)); + + const [totalCount, rows] = await Promise.all([ + countQuery, + labelListQuery + ]); + + return response(res, { + data: { + labels: rows, + pagination: { + total: totalCount, + pageSize, + page + } + }, + success: true, + error: false, + message: "Labels retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/labels/updateOrgLabel.ts b/server/private/routers/labels/updateOrgLabel.ts new file mode 100644 index 000000000..d6478678a --- /dev/null +++ b/server/private/routers/labels/updateOrgLabel.ts @@ -0,0 +1,109 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { + db, + labels, + resourceLabels, + resources, + siteLabels, + sites, + type Label +} from "@server/db"; +import response from "@server/lib/response"; +import logger from "@server/logger"; +import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; +import HttpCode from "@server/types/HttpCode"; +import { and, eq } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; + +const paramsSchema = z.strictObject({ + orgId: z.string().nonempty(), + labelId: z.string().transform(Number).pipe(z.int().positive()) +}); + +const updateLabelBodySchema = z.strictObject({ + name: z.string().min(1).max(255).optional(), + color: z + .string() + .regex(/^#?([0-9a-f]{6}|[0-9a-f]{3})$/i) + .nonempty() +}); + +export async function updateOrgLabel( + req: Request, + res: Response, + next: NextFunction +) { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId, labelId } = parsedParams.data; + + const parsedBody = updateLabelBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const [existing] = await db + .select() + .from(labels) + .where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId))); + + if (!existing) { + return next(createHttpError(HttpCode.NOT_FOUND, "Label not found")); + } + + const { name, color } = parsedBody.data; + + const [label] = await db + .update(labels) + .set({ + name, + color + }) + .where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId))) + .returning(); + + return response(res, { + data: { + label + }, + success: true, + error: false, + message: "Label updated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/labels/types.ts b/server/routers/labels/types.ts new file mode 100644 index 000000000..ad182ad11 --- /dev/null +++ b/server/routers/labels/types.ts @@ -0,0 +1,10 @@ +import type { Label } from "@server/db"; +import type { PaginatedResponse } from "@server/types/Pagination"; + +export type ListOrgLabelsResponse = PaginatedResponse<{ + labels: Omit[]; +}>; + +export type CreateOrEditLabelResponse = { + label: Label; +}; From 09baf2f32ee23836f47e1e5b4341c9ec391ef1eb Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 5 May 2026 21:08:22 +0200 Subject: [PATCH 102/771] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20add=20sqlite=20?= =?UTF-8?q?table=20for=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/sqlite/schema/schema.ts | 87 +++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 16 deletions(-) diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 423190420..3695e29a0 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -183,6 +183,47 @@ export const resources = sqliteTable("resources", { wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false) }); +export const labels = sqliteTable("labels", { + labelId: integer("labelId").primaryKey({ autoIncrement: true }), + name: text("name").notNull(), + color: text("color").notNull(), + orgId: text("orgId") + .references(() => orgs.orgId, { + onDelete: "cascade" + }) + .notNull() +}); + +export const siteLabels = sqliteTable("siteLabels", { + siteLabelId: integer("siteLabelId").primaryKey({ autoIncrement: true }), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), + labelId: integer("labelId") + .references(() => labels.labelId, { + onDelete: "cascade" + }) + .notNull() +}); + +export const resourceLabels = sqliteTable("resourceLabels", { + resourceLabelId: integer("resourceLabelId").primaryKey({ + autoIncrement: true + }), + resourceId: integer("resourceId") + .references(() => resources.resourceId, { + onDelete: "cascade" + }) + .notNull(), + labelId: integer("labelId") + .references(() => labels.labelId, { + onDelete: "cascade" + }) + .notNull() +}); + export const targets = sqliteTable("targets", { targetId: integer("targetId").primaryKey({ autoIncrement: true }), resourceId: integer("resourceId") @@ -219,9 +260,11 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", { onDelete: "cascade" }) .notNull(), - siteId: integer("siteId").references(() => sites.siteId, { - onDelete: "cascade" - }).notNull(), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), name: text("name"), hcEnabled: integer("hcEnabled", { mode: "boolean" }) .notNull() @@ -1196,19 +1239,30 @@ export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", { complete: integer("complete", { mode: "boolean" }).notNull().default(false) }); -export const statusHistory = sqliteTable("statusHistory", { - id: integer("id").primaryKey({ autoIncrement: true }), - entityType: text("entityType").notNull(), // "site" | "healthCheck" - entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId - orgId: text("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks - timestamp: integer("timestamp").notNull(), // unix epoch seconds -}, (table) => [ - index("idx_statusHistory_entity").on(table.entityType, table.entityId, table.timestamp), - index("idx_statusHistory_org_timestamp").on(table.orgId, table.timestamp), -]); +export const statusHistory = sqliteTable( + "statusHistory", + { + id: integer("id").primaryKey({ autoIncrement: true }), + entityType: text("entityType").notNull(), // "site" | "healthCheck" + entityId: integer("entityId").notNull(), // siteId or targetHealthCheckId + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + status: text("status").notNull(), // "online"/"offline" for sites; "healthy"/"unhealthy"/"unknown" for healthChecks + timestamp: integer("timestamp").notNull() // unix epoch seconds + }, + (table) => [ + index("idx_statusHistory_entity").on( + table.entityType, + table.entityId, + table.timestamp + ), + index("idx_statusHistory_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); export type Org = InferSelectModel; export type User = InferSelectModel; @@ -1278,3 +1332,4 @@ export type RoundTripMessageTracker = InferSelectModel< typeof roundTripMessageTracker >; export type StatusHistory = InferSelectModel; +export type Label = InferSelectModel; From 0d04cc365f0215eef36fe2ed42d45d86c7fa25e6 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 5 May 2026 21:35:10 +0200 Subject: [PATCH 103/771] =?UTF-8?q?=E2=9C=A8=20attach=20label=20to=20item?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/auth/actions.ts | 1 + server/private/routers/external.ts | 8 + .../routers/labels/attachLabelToItem.ts | 155 ++++++++++++++++++ .../private/routers/labels/createOrgLabel.ts | 17 +- server/private/routers/labels/index.ts | 1 + .../private/routers/labels/updateOrgLabel.ts | 10 +- 6 files changed, 177 insertions(+), 15 deletions(-) create mode 100644 server/private/routers/labels/attachLabelToItem.ts diff --git a/server/auth/actions.ts b/server/auth/actions.ts index f6bfa2549..5e2f58287 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -151,6 +151,7 @@ export enum ActionsEnum { listOrgLabels = "listOrgLabels", createOrgLabel = "createOrgLabel", updateOrgLabel = "updateOrgLabel", + attachLabelToItem = "attachLabelToItem", getAlertRule = "getAlertRule", createHealthCheck = "createHealthCheck", updateHealthCheck = "updateHealthCheck", diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 1b8c1ef4c..7941cd1fe 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -757,6 +757,14 @@ authenticated.patch( labels.updateOrgLabel ); +authenticated.put( + "/org/:orgId/label/:labelId/attach", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.attachLabelToItem), + labels.attachLabelToItem +); + authenticated.get( "/org/:orgId/health-checks", verifyValidLicense, diff --git a/server/private/routers/labels/attachLabelToItem.ts b/server/private/routers/labels/attachLabelToItem.ts new file mode 100644 index 000000000..4655c3f3b --- /dev/null +++ b/server/private/routers/labels/attachLabelToItem.ts @@ -0,0 +1,155 @@ +/* + * This file is part of a proprietary work. + * + * Copyright (c) 2025-2026 Fossorial, Inc. + * All rights reserved. + * + * This file is licensed under the Fossorial Commercial License. + * You may not use this file except in compliance with the License. + * Unauthorized use, copying, modification, or distribution is strictly prohibited. + * + * This file is not licensed under the AGPLv3. + */ + +import { + db, + labels, + resourceLabels, + resources, + siteLabels, + sites +} from "@server/db"; +import response from "@server/lib/response"; +import logger from "@server/logger"; +import HttpCode from "@server/types/HttpCode"; +import { and, eq } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; + +const paramsSchema = z.strictObject({ + orgId: z.string().nonempty(), + labelId: z.string().transform(Number).pipe(z.int().positive()) +}); + +const attachLabelBodySchema = z.strictObject({ + siteId: z.number().int().optional(), + resourceId: z.number().int().optional() +}); + +export async function attachLabelToItem( + req: Request, + res: Response, + next: NextFunction +) { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId, labelId } = parsedParams.data; + + const parsedBody = attachLabelBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { siteId, resourceId } = parsedBody.data; + + if (!siteId && !resourceId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "At least one of `siteId` or `resourceId` should be provided." + ) + ); + } + + const [existing] = await db + .select() + .from(labels) + .where(and(eq(labels.labelId, labelId), eq(labels.orgId, orgId))); + + if (!existing) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Label with Id ${labelId} not found` + ) + ); + } + + if (siteId) { + const siteCount = await db.$count( + sites, + and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)) + ); + + if (siteCount === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Site with Id ${siteId} doesn't exist.` + ) + ); + } + + await db + .insert(siteLabels) + .values({ + labelId, + siteId + }) + .returning(); + } + + if (resourceId) { + const resourceCount = await db.$count( + resources, + and( + eq(resources.resourceId, resourceId), + eq(resources.orgId, orgId) + ) + ); + + if (resourceCount === 0) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Resource with Id ${resourceId} doesn't exist.` + ) + ); + } + + await db.insert(resourceLabels).values({ + labelId, + resourceId + }); + } + + return response(res, { + data: {}, + success: true, + error: false, + message: "Site Label object created successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/private/routers/labels/createOrgLabel.ts b/server/private/routers/labels/createOrgLabel.ts index 1439018d2..074a96207 100644 --- a/server/private/routers/labels/createOrgLabel.ts +++ b/server/private/routers/labels/createOrgLabel.ts @@ -16,14 +16,13 @@ import { resourceLabels, resources, siteLabels, - sites, - type Label + sites } from "@server/db"; import response from "@server/lib/response"; import logger from "@server/logger"; import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; import HttpCode from "@server/types/HttpCode"; -import { eq } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; @@ -74,7 +73,10 @@ export async function createOrgLabel( const { name, color, siteId, resourceId } = parsedBody.data; if (siteId) { - const siteCount = await db.$count(sites, eq(sites.siteId, siteId)); + const siteCount = await db.$count( + sites, + and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)) + ); if (siteCount === 0) { return next( @@ -88,8 +90,11 @@ export async function createOrgLabel( if (resourceId) { const resourceCount = await db.$count( - sites, - eq(resources.resourceId, resourceId) + resources, + and( + eq(resources.resourceId, resourceId), + eq(resources.orgId, orgId) + ) ); if (resourceCount === 0) { diff --git a/server/private/routers/labels/index.ts b/server/private/routers/labels/index.ts index 4c7e5f432..cc15ebffe 100644 --- a/server/private/routers/labels/index.ts +++ b/server/private/routers/labels/index.ts @@ -14,3 +14,4 @@ export * from "./listOrgLabels"; export * from "./createOrgLabel"; export * from "./updateOrgLabel"; +export * from "./attachLabelToItem"; diff --git a/server/private/routers/labels/updateOrgLabel.ts b/server/private/routers/labels/updateOrgLabel.ts index d6478678a..eb5f5177a 100644 --- a/server/private/routers/labels/updateOrgLabel.ts +++ b/server/private/routers/labels/updateOrgLabel.ts @@ -11,15 +11,7 @@ * This file is not licensed under the AGPLv3. */ -import { - db, - labels, - resourceLabels, - resources, - siteLabels, - sites, - type Label -} from "@server/db"; +import { db, labels } from "@server/db"; import response from "@server/lib/response"; import logger from "@server/logger"; import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; From a8f4d2b7d18ab4f8eacb215f6d4a9f9f83733920 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 5 May 2026 20:53:36 -0700 Subject: [PATCH 104/771] Add new user and role selectors for pagination --- .../proxy/[niceId]/authentication/page.tsx | 909 +----------------- .../resource-policy/EditPolicyForm.tsx | 37 +- .../EditPolicyUserRolesSectionForm.tsx | 98 +- src/components/users-selector.tsx | 5 +- 4 files changed, 42 insertions(+), 1007 deletions(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index 94e1ed6ff..3194e7343 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -2,16 +2,12 @@ import ActionBanner from "@app/components/ActionBanner"; import { EditPolicyForm } from "@app/components/resource-policy/EditPolicyForm"; -import { RolesSelector } from "@app/components/roles-selector"; -import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm"; -import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm"; import { SettingsContainer, SettingsSection, SettingsSectionBody, SettingsSectionDescription, SettingsSectionFooter, - SettingsSectionForm, SettingsSectionHeader, SettingsSectionTitle } from "@app/components/Settings"; @@ -19,9 +15,6 @@ import { StrategySelect, type StrategyOption } from "@app/components/StrategySelect"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { Tag, TagInput } from "@app/components/tags/tag-input"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { Button } from "@app/components/ui/button"; import { Command, @@ -31,30 +24,11 @@ import { CommandItem, CommandList } from "@app/components/ui/command"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@app/components/ui/form"; -import { InfoPopup } from "@app/components/ui/info-popup"; import { Popover, PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@app/components/ui/select"; -import { UsersSelector } from "@app/components/users-selector"; -import type { ResourceContextType } from "@app/contexts/resourceContext"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; @@ -62,36 +36,18 @@ import { useResourceContext } from "@app/hooks/useResourceContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { cn } from "@app/lib/cn"; -import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { orgQueries, resourceQueries } from "@app/lib/queries"; import { ResourcePolicyProvider } from "@app/providers/ResourcePolicyProvider"; import { zodResolver } from "@hookform/resolvers/zod"; import { CaretSortIcon } from "@radix-ui/react-icons"; import { build } from "@server/build"; import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix"; -import { UserType } from "@server/types/UserTypes"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import SetResourcePasswordForm from "components/SetResourcePasswordForm"; -import { - ArrowRightIcon, - Binary, - Bot, - CheckIcon, - InfoIcon, - Key, - ShieldAlertIcon -} from "lucide-react"; +import { ArrowRightIcon, CheckIcon, ShieldAlertIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { - useActionState, - useEffect, - useMemo, - useRef, - useState, - useTransition -} from "react"; +import { useEffect, useState, useTransition } from "react"; import { useForm, useWatch } from "react-hook-form"; import { z } from "zod"; @@ -108,34 +64,9 @@ const resourceTypeSchema = z type ResourcePolicyType = StrategyOption<"inline" | "shared">; -const UsersRolesFormSchema = z.object({ - roles: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ), - users: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ) -}); - -const whitelistSchema = z.object({ - emails: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ) -}); - export default function ResourceAuthenticationPage() { const { org } = useOrgContext(); - const { resource, updateResource, authInfo, updateAuthInfo } = - useResourceContext(); + const { resource, updateResource } = useResourceContext(); const queryClient = useQueryClient(); const { env } = useEnvContext(); @@ -151,42 +82,6 @@ export default function ResourceAuthenticationPage() { }) ); - const { data: resourceRoles = [], isLoading: isLoadingResourceRoles } = - useQuery( - resourceQueries.resourceRoles({ - resourceId: resource.resourceId - }) - ); - const { data: resourceUsers = [], isLoading: isLoadingResourceUsers } = - useQuery( - resourceQueries.resourceUsers({ - resourceId: resource.resourceId - }) - ); - - const { data: whitelist = [], isLoading: isLoadingWhiteList } = useQuery( - resourceQueries.resourceWhitelist({ - resourceId: resource.resourceId - }) - ); - - const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery( - orgQueries.roles({ - orgId: org.org.orgId - }) - ); - const { data: orgUsers = [], isLoading: isLoadingOrgUsers } = useQuery( - orgQueries.users({ - orgId: org.org.orgId - }) - ); - const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery({ - ...orgQueries.identityProviders({ - orgId: org.org.orgId, - useOrgOnlyIdp: env.app.identityProviderMode === "org" - }) - }); - const form = useForm({ resolver: zodResolver(resourceTypeSchema), defaultValues: { @@ -231,58 +126,6 @@ export default function ResourceAuthenticationPage() { } ]; - const allIdps = useMemo(() => { - if (build === "saas") { - if (isPaidUser(tierMatrix.orgOidc)) { - return orgIdps.map((idp) => ({ - id: idp.idpId, - text: idp.name - })); - } - } else { - return orgIdps.map((idp) => ({ - id: idp.idpId, - text: idp.name - })); - } - return []; - }, [orgIdps]); - - const [ssoEnabled, setSsoEnabled] = useState(resource.sso ?? false); - - useEffect(() => { - setSsoEnabled(resource.sso ?? false); - }, [resource.sso]); - - const [selectedIdpId, setSelectedIdpId] = useState( - resource.skipToIdpId || null - ); - - const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] = - useState(false); - const [loadingRemoveResourcePincode, setLoadingRemoveResourcePincode] = - useState(false); - const [ - loadingRemoveResourceHeaderAuth, - setLoadingRemoveResourceHeaderAuth - ] = useState(false); - - const [isSetPasswordOpen, setIsSetPasswordOpen] = useState(false); - const [isSetPincodeOpen, setIsSetPincodeOpen] = useState(false); - const [isSetHeaderAuthOpen, setIsSetHeaderAuthOpen] = useState(false); - - const usersRolesForm = useForm({ - resolver: zodResolver(UsersRolesFormSchema), - defaultValues: { roles: [], users: [] } - }); - - const whitelistForm = useForm({ - resolver: zodResolver(whitelistSchema), - defaultValues: { emails: [] } - }); - - const hasInitializedRef = useRef(false); - useEffect(() => { if (!isLoadingPolicies && policies?.sharedPolicy) { setSelectedPolicy({ @@ -292,55 +135,7 @@ export default function ResourceAuthenticationPage() { } }, [isLoadingPolicies, policies?.sharedPolicy]); - const pageLoading = - isLoadingPolicies || - !policies || - isLoadingOrgRoles || - isLoadingOrgUsers || - isLoadingResourceRoles || - isLoadingResourceUsers || - isLoadingWhiteList || - isLoadingOrgIdps; - - useEffect(() => { - if (pageLoading || hasInitializedRef.current) return; - - usersRolesForm.setValue( - "roles", - resourceRoles - .map((i) => ({ - id: i.roleId.toString(), - text: i.name - })) - .filter((role) => role.text !== "Admin") - ); - usersRolesForm.setValue( - "users", - resourceUsers.map((i) => ({ - id: i.userId.toString(), - text: `${getUserDisplayName({ - email: i.email, - username: i.username - })}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` - })) - ); - - whitelistForm.setValue( - "emails", - whitelist.map((w) => ({ - id: w.email, - text: w.email - })) - ); - hasInitializedRef.current = true; - }, [pageLoading, resourceRoles, resourceUsers, whitelist, orgIdps]); - - const [, submitUserRolesForm, loadingSaveUsersRoles] = useActionState( - onSubmitUsersRoles, - null - ); - - const [isUpdatingResource, startTransitionPolicy] = useTransition(); + const [isUpdatingResource, startTransition] = useTransition(); async function handleSaveResourcePolicyType() { try { @@ -381,206 +176,18 @@ export default function ResourceAuthenticationPage() { } } - async function onSubmitUsersRoles() { - const isValid = usersRolesForm.trigger(); - if (!isValid) return; - - const data = usersRolesForm.getValues(); - - try { - const jobs = [ - api.post(`/resource/${resource.resourceId}/roles`, { - roleIds: data.roles.map((i) => parseInt(i.id)) - }), - api.post(`/resource/${resource.resourceId}/users`, { - userIds: data.users.map((i) => i.id) - }), - api.post(`/resource/${resource.resourceId}`, { - sso: ssoEnabled, - skipToIdpId: selectedIdpId - }) - ]; - - await Promise.all(jobs); - - updateResource({ - sso: ssoEnabled, - skipToIdpId: selectedIdpId - }); - - updateAuthInfo({ - sso: ssoEnabled - }); - - toast({ - title: t("resourceAuthSettingsSave"), - description: t("resourceAuthSettingsSaveDescription") - }); - await queryClient.invalidateQueries( - resourceQueries.resourceUsers({ - resourceId: resource.resourceId - }) - ); - await queryClient.invalidateQueries( - resourceQueries.resourceRoles({ - resourceId: resource.resourceId - }) - ); - - router.refresh(); - } catch (e) { - console.error(e); - toast({ - variant: "destructive", - title: t("resourceErrorUsersRolesSave"), - description: formatAxiosError( - e, - t("resourceErrorUsersRolesSaveDescription") - ) - }); - } - } - - function removeResourcePassword() { - setLoadingRemoveResourcePassword(true); - - api.post(`/resource/${resource.resourceId}/password`, { - password: null - }) - .then(() => { - toast({ - title: t("resourcePasswordRemove"), - description: t("resourcePasswordRemoveDescription") - }); - - updateAuthInfo({ - password: false - }); - router.refresh(); - }) - .catch((e) => { - toast({ - variant: "destructive", - title: t("resourceErrorPasswordRemove"), - description: formatAxiosError( - e, - t("resourceErrorPasswordRemoveDescription") - ) - }); - }) - .finally(() => setLoadingRemoveResourcePassword(false)); - } - - function removeResourcePincode() { - setLoadingRemoveResourcePincode(true); - - api.post(`/resource/${resource.resourceId}/pincode`, { - pincode: null - }) - .then(() => { - toast({ - title: t("resourcePincodeRemove"), - description: t("resourcePincodeRemoveDescription") - }); - - updateAuthInfo({ - pincode: false - }); - router.refresh(); - }) - .catch((e) => { - toast({ - variant: "destructive", - title: t("resourceErrorPincodeRemove"), - description: formatAxiosError( - e, - t("resourceErrorPincodeRemoveDescription") - ) - }); - }) - .finally(() => setLoadingRemoveResourcePincode(false)); - } - - function removeResourceHeaderAuth() { - setLoadingRemoveResourceHeaderAuth(true); - - api.post(`/resource/${resource.resourceId}/header-auth`, { - user: null, - password: null, - extendedCompatibility: null - }) - .then(() => { - toast({ - title: t("resourceHeaderAuthRemove"), - description: t("resourceHeaderAuthRemoveDescription") - }); - - updateAuthInfo({ - headerAuth: false - }); - router.refresh(); - }) - .catch((e) => { - toast({ - variant: "destructive", - title: t("resourceErrorHeaderAuthRemove"), - description: formatAxiosError( - e, - t("resourceErrorHeaderAuthRemoveDescription") - ) - }); - }) - .finally(() => setLoadingRemoveResourceHeaderAuth(false)); - } + const pageLoading = isLoadingPolicies || !policies; if (pageLoading) { return <>; } + console.log({ + shared: policies.sharedPolicy + }); + return ( <> - {isSetPasswordOpen && ( - { - setIsSetPasswordOpen(false); - updateAuthInfo({ - password: true - }); - }} - /> - )} - - {isSetPincodeOpen && ( - { - setIsSetPincodeOpen(false); - updateAuthInfo({ - pincode: true - }); - }} - /> - )} - - {isSetHeaderAuthOpen && ( - { - setIsSetHeaderAuthOpen(false); - updateAuthInfo({ - headerAuth: true - }); - }} - /> - )} - {build !== "oss" && isPaidUser(tierMatrix[TierFeature.ResourcePolicies]) && ( @@ -683,7 +290,7 @@ export default function ResourceAuthenticationPage() { - - - - - - - {t("resourceAuthMethods")} - - - {t("resourceAuthMethodsDescriptions")} - - - - - {/* Password Protection */} -
-
- - - {t("resourcePasswordProtection", { - status: authInfo.password - ? t("enabled") - : t("disabled") - })} - -
- -
- - {/* PIN Code Protection */} -
-
- - - {t("resourcePincodeProtection", { - status: authInfo.pincode - ? t("enabled") - : t("disabled") - })} - -
- -
- - {/* Header Authentication Protection */} -
-
- - - {authInfo.headerAuth - ? t( - "resourceHeaderAuthProtectionEnabled" - ) - : t( - "resourceHeaderAuthProtectionDisabled" - )} - -
- -
-
-
-
- - - {selectedResourceType === "inline" ? ( @@ -1020,216 +344,3 @@ export default function ResourceAuthenticationPage() { ); } - -type OneTimePasswordFormSectionProps = Pick< - ResourceContextType, - "resource" | "updateResource" -> & { - whitelist: Array<{ email: string }>; - isLoadingWhiteList: boolean; -}; - -function OneTimePasswordFormSection({ - resource, - updateResource, - whitelist, - isLoadingWhiteList -}: OneTimePasswordFormSectionProps) { - const { env } = useEnvContext(); - const [whitelistEnabled, setWhitelistEnabled] = useState( - resource.emailWhitelistEnabled ?? false - ); - - useEffect(() => { - setWhitelistEnabled(resource.emailWhitelistEnabled); - }, [resource.emailWhitelistEnabled]); - - const queryClient = useQueryClient(); - - const [loadingSaveWhitelist, startTransition] = useTransition(); - const whitelistForm = useForm({ - resolver: zodResolver(whitelistSchema), - defaultValues: { emails: [] } - }); - const api = createApiClient({ env }); - const router = useRouter(); - const t = useTranslations(); - - const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< - number | null - >(null); - - useEffect(() => { - if (isLoadingWhiteList) return; - - whitelistForm.setValue( - "emails", - whitelist.map((w) => ({ - id: w.email, - text: w.email - })) - ); - }, [isLoadingWhiteList, whitelist, whitelistForm]); - - async function saveWhitelist() { - try { - await api.post(`/resource/${resource.resourceId}`, { - emailWhitelistEnabled: whitelistEnabled - }); - - if (whitelistEnabled) { - await api.post(`/resource/${resource.resourceId}/whitelist`, { - emails: whitelistForm.getValues().emails.map((i) => i.text) - }); - } - - updateResource({ - emailWhitelistEnabled: whitelistEnabled - }); - - toast({ - title: t("resourceWhitelistSave"), - description: t("resourceWhitelistSaveDescription") - }); - router.refresh(); - await queryClient.invalidateQueries( - resourceQueries.resourceWhitelist({ - resourceId: resource.resourceId - }) - ); - } catch (e) { - console.error(e); - toast({ - variant: "destructive", - title: t("resourceErrorWhitelistSave"), - description: formatAxiosError( - e, - t("resourceErrorWhitelistSaveDescription") - ) - }); - } - } - - return ( - - - - {t("otpEmailTitle")} - - - {t("otpEmailTitleDescription")} - - - - - {!env.email.emailEnabled && ( - - - - {t("otpEmailSmtpRequired")} - - - {t("otpEmailSmtpRequiredDescription")} - - - )} - - - {whitelistEnabled && env.email.emailEnabled && ( -
- - ( - - - - - - {/* @ts-ignore */} - { - return z - .email() - .or( - z - .string() - .regex( - /^\*@[\w.-]+\.[a-zA-Z]{2,}$/, - { - message: - t( - "otpEmailErrorInvalid" - ) - } - ) - ) - .safeParse(tag) - .success; - }} - setActiveTagIndex={ - setActiveEmailTagIndex - } - placeholder={t( - "otpEmailEnter" - )} - tags={ - whitelistForm.getValues() - .emails - } - setTags={(newRoles) => { - whitelistForm.setValue( - "emails", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - allowDuplicates={false} - sortTags={true} - /> - - - {t("otpEmailEnterDescription")} - - - )} - /> - - - )} -
-
- - - -
- ); -} diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index d0594752f..b26453c6e 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -6,11 +6,9 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; -import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { orgQueries } from "@app/lib/queries"; import { build } from "@server/build"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; -import { UserType } from "@server/types/UserTypes"; import { useQuery } from "@tanstack/react-query"; import { useTranslations } from "next-intl"; @@ -51,12 +49,6 @@ export function EditPolicyForm({ env.server.maxmind_asn_path && env.server.maxmind_asn_path.length > 0 ); - const { data: orgRoles = [], isLoading: isLoadingOrgRoles } = useQuery( - orgQueries.roles({ orgId: org.org.orgId }) - ); - const { data: orgUsers = [], isLoading: isLoadingOrgUsers } = useQuery( - orgQueries.users({ orgId: org.org.orgId }) - ); const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery( orgQueries.identityProviders({ orgId: org.org.orgId, @@ -64,26 +56,6 @@ export function EditPolicyForm({ }) ); - const allRoles = useMemo( - () => - orgRoles - .map((role) => ({ - id: role.roleId.toString(), - text: role.name - })) - .filter((role) => role.text !== "Admin"), - [orgRoles] - ); - - const allUsers = useMemo( - () => - orgUsers.map((user) => ({ - id: user.id.toString(), - text: `${getUserDisplayName({ email: user.email, username: user.username })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` - })), - [orgUsers] - ); - const allIdps = useMemo(() => { if (build === "saas") { if (isPaidUser(tierMatrix.orgOidc)) { @@ -98,17 +70,18 @@ export function EditPolicyForm({ return []; }, [orgIdps, isPaidUser]); - if (isLoadingOrgRoles || isLoadingOrgUsers || isLoadingOrgIdps) { + if (isLoadingOrgIdps) { return <>; } return ( - {!hidePolicyNameForm && } + {!hidePolicyNameForm && ( + + )} diff --git a/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx index 29fe486be..f535021e2 100644 --- a/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx @@ -23,8 +23,9 @@ import type { AxiosResponse } from "axios"; import { useRouter } from "next/navigation"; import { createPolicySchema } from "."; +import { RolesSelector } from "@app/components/roles-selector"; +import { UsersSelector } from "@app/components/users-selector"; import { SwitchInput } from "@app/components/SwitchInput"; -import { Tag, TagInput } from "@app/components/tags/tag-input"; import { Button } from "@app/components/ui/button"; import { Form, @@ -50,15 +51,13 @@ import { useForm, useWatch } from "react-hook-form"; // ─── PolicyUsersRolesSection ────────────────────────────────────────────────── type PolicyUsersRolesSectionProps = { - allRoles: { id: string; text: string }[]; - allUsers: { id: string; text: string }[]; + orgId: string; allIdps: { id: number; text: string }[]; readonly?: boolean; }; export function EditPolicyUsersRolesSectionForm({ - allRoles, - allUsers, + orgId, allIdps, readonly }: PolicyUsersRolesSectionProps) { @@ -98,12 +97,6 @@ export function EditPolicyUsersRolesSectionForm({ control: form.control, name: "skipToIdpId" }); - const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< - number | null - >(null); - const [activeUsersTagIndex, setActiveUsersTagIndex] = useState< - number | null - >(null); const [, formAction, isSubmitting] = useActionState(onSubmit, null); @@ -190,43 +183,21 @@ export function EditPolicyUsersRolesSectionForm({ {t("roles")} - { + onSelectRoles={( + roles + ) => form.setValue( "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true + roles + ) } - autocompleteOptions={ - allRoles - } - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} disabled={readonly} + restrictAdminRole /> @@ -247,42 +218,19 @@ export function EditPolicyUsersRolesSectionForm({ {t("users")} - { + onSelectUsers={( + users + ) => form.setValue( "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true + users + ) } - autocompleteOptions={ - allUsers - } - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} disabled={readonly} /> diff --git a/src/components/users-selector.tsx b/src/components/users-selector.tsx index 5cbe90e89..b63e3d4bb 100644 --- a/src/components/users-selector.tsx +++ b/src/components/users-selector.tsx @@ -18,12 +18,14 @@ export type UsersSelectorProps = { orgId: string; selectedUsers?: SelectedUser[]; onSelectUsers: (users: SelectedUser[]) => void; + disabled?: boolean; }; export function UsersSelector({ orgId, selectedUsers = [], - onSelectUsers + onSelectUsers, + disabled }: UsersSelectorProps) { const t = useTranslations(); const [userSearchQuery, setUserSearchQuery] = useState(""); @@ -58,6 +60,7 @@ export function UsersSelector({ options={usersShown} value={selectedUsers} onChange={onSelectUsers} + disabled={disabled} /> ); } From 54c1dd3bae034edcfaf7b46c5949ff542fe13f01 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 5 May 2026 21:05:42 -0700 Subject: [PATCH 105/771] Make path the default --- src/components/resource-policy/CreatePolicyRulesSectionForm.tsx | 2 +- src/components/resource-policy/EditPolicyRulesSectionForm.tsx | 2 +- src/components/resource-policy/ResourcePolicySubForms.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx b/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx index b3285b284..4550d5bfb 100644 --- a/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx +++ b/src/components/resource-policy/CreatePolicyRulesSectionForm.tsx @@ -147,7 +147,7 @@ export function CreatePolicyRulesSectionForm({ resolver: zodResolver(addRuleSchema), defaultValues: { action: "ACCEPT" as const, - match: "IP", + match: "PATH", value: "" } }); diff --git a/src/components/resource-policy/EditPolicyRulesSectionForm.tsx b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx index 48e0ef605..f00a7c6a5 100644 --- a/src/components/resource-policy/EditPolicyRulesSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx @@ -151,7 +151,7 @@ export function EditPolicyRulesSectionForm({ resolver: zodResolver(addRuleSchema), defaultValues: { action: "ACCEPT" as const, - match: "IP", + match: "PATH", value: "" } }); diff --git a/src/components/resource-policy/ResourcePolicySubForms.tsx b/src/components/resource-policy/ResourcePolicySubForms.tsx index 37a83b3fe..1b46c79f4 100644 --- a/src/components/resource-policy/ResourcePolicySubForms.tsx +++ b/src/components/resource-policy/ResourcePolicySubForms.tsx @@ -972,7 +972,7 @@ export function PolicyRulesSection({ resolver: zodResolver(addRuleSchema), defaultValues: { action: "ACCEPT" as const, - match: "IP", + match: "PATH", value: "" } }); From c4b3656fad8a0e75a86fb84b17c80adbcf886e12 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 6 May 2026 10:09:05 -0700 Subject: [PATCH 106/771] Update UI to support additions on the resource --- .../proxy/[niceId]/authentication/page.tsx | 5 +- .../multi-select/multi-select-content.tsx | 63 +-- .../multi-select/multi-select-tag-input.tsx | 87 +++-- .../resource-policy/EditPolicyForm.tsx | 6 +- .../EditPolicyRulesSectionForm.tsx | 267 ++++++++++--- .../EditPolicyUserRolesSectionForm.tsx | 369 ++++++++++++++---- src/components/roles-selector.tsx | 5 +- src/components/users-selector.tsx | 5 +- src/lib/queries.ts | 12 + 9 files changed, 621 insertions(+), 198 deletions(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index 3194e7343..4a5aac070 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -336,7 +336,10 @@ export default function ResourceAuthenticationPage() { } /> - +
) )} diff --git a/src/components/multi-select/multi-select-content.tsx b/src/components/multi-select/multi-select-content.tsx index 9f49b41ca..e1b263240 100644 --- a/src/components/multi-select/multi-select-content.tsx +++ b/src/components/multi-select/multi-select-content.tsx @@ -23,6 +23,7 @@ export type MultiSelectTagsProps = { onSearch: (query: string) => void; ref?: Ref; disabled?: boolean; + lockedIds?: Set; }; export function MultiSelectContent({ @@ -32,7 +33,8 @@ export function MultiSelectContent({ value, options, onSearch, - onChange + onChange, + lockedIds }: MultiSelectTagsProps) { const t = useTranslations(); const selectedValues = new Set(value.map((v) => v.id)); @@ -48,33 +50,38 @@ export function MultiSelectContent({ {emptyPlaceholder ?? t("noResults")} - {options.map((option) => ( - { - let newValues = []; - if (selectedValues.has(option.id)) { - newValues = value.filter( - (v) => v.id !== option.id - ); - } else { - newValues = [...value, option]; - } - onChange(newValues); - }} - > - - {`${option.text}`} - - ))} + {options.map((option) => { + const isLocked = lockedIds?.has(option.id); + return ( + { + if (isLocked) return; + let newValues = []; + if (selectedValues.has(option.id)) { + newValues = value.filter( + (v) => v.id !== option.id + ); + } else { + newValues = [...value, option]; + } + onChange(newValues); + }} + > + + {`${option.text}`} + + ); + })}
diff --git a/src/components/multi-select/multi-select-tag-input.tsx b/src/components/multi-select/multi-select-tag-input.tsx index 5634f7481..30bd4a913 100644 --- a/src/components/multi-select/multi-select-tag-input.tsx +++ b/src/components/multi-select/multi-select-tag-input.tsx @@ -5,7 +5,7 @@ import { PopoverTrigger } from "@app/components/ui/popover"; import { cn } from "@app/lib/cn"; -import { ChevronDownIcon, XIcon } from "lucide-react"; +import { ChevronDownIcon, LockIcon, XIcon } from "lucide-react"; import { type MultiSelectTagsProps, type TagValue, @@ -16,10 +16,12 @@ export interface MultiSelectInputProps< T extends TagValue > extends MultiSelectTagsProps { buttonText?: string; + lockedIds?: Set; } export function MultiSelectTagInput({ buttonText, + lockedIds, ...props }: MultiSelectInputProps) { const selectedValues = new Set(props.value.map((v) => v.id)); @@ -52,46 +54,63 @@ export function MultiSelectTagInput({ "overflow-x-auto" )} > - {props.value.map((option) => ( - e.stopPropagation()} - > - {option.text} - - - ))} + {option.text} + {isLocked ? ( + + + + ) : ( + + )} +
+ ); + })} {buttonText}
- + ); diff --git a/src/components/resource-policy/EditPolicyForm.tsx b/src/components/resource-policy/EditPolicyForm.tsx index b26453c6e..043ab1852 100644 --- a/src/components/resource-policy/EditPolicyForm.tsx +++ b/src/components/resource-policy/EditPolicyForm.tsx @@ -27,11 +27,13 @@ import { EditPolicyRulesSectionForm } from "./EditPolicyRulesSectionForm"; export type EditPolicyFormProps = { hidePolicyNameForm?: boolean; readonly?: boolean; + resourceId?: number; }; export function EditPolicyForm({ hidePolicyNameForm, - readonly + readonly, + resourceId }: EditPolicyFormProps) { const { org } = useOrgContext(); const t = useTranslations(); @@ -84,6 +86,7 @@ export function EditPolicyForm({ orgId={org.org.orgId} allIdps={allIdps} readonly={readonly} + resourceId={resourceId} /> @@ -97,6 +100,7 @@ export function EditPolicyForm({ isMaxmindAvailable={isMaxmindAvailable} isMaxmindAsnAvailable={isMaxmindASNAvailable} readonly={readonly} + resourceId={resourceId} /> ); diff --git a/src/components/resource-policy/EditPolicyRulesSectionForm.tsx b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx index f00a7c6a5..dc7bec7b3 100644 --- a/src/components/resource-policy/EditPolicyRulesSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyRulesSectionForm.tsx @@ -75,13 +75,28 @@ import { getSortedRowModel, useReactTable } from "@tanstack/react-table"; -import { ArrowUpDown, Check, ChevronsUpDown, Plus } from "lucide-react"; +import { + ArrowUpDown, + Check, + ChevronsUpDown, + LockIcon, + Plus +} from "lucide-react"; -import { useCallback, useMemo, useState, useTransition } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + useTransition +} from "react"; import { UseFormReturn, useForm, useWatch } from "react-hook-form"; import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { resourceQueries } from "@app/lib/queries"; +import { useQuery } from "@tanstack/react-query"; import type { AxiosResponse } from "axios"; import { useRouter } from "next/navigation"; @@ -103,18 +118,21 @@ type LocalRule = { enabled: boolean; new?: boolean; updated?: boolean; + fromPolicy?: boolean; }; type PolicyRulesSectionProps = { isMaxmindAvailable: boolean; isMaxmindAsnAvailable: boolean; readonly?: boolean; + resourceId?: number; }; export function EditPolicyRulesSectionForm({ isMaxmindAvailable, isMaxmindAsnAvailable, - readonly + readonly, + resourceId }: PolicyRulesSectionProps) { const t = useTranslations(); @@ -122,6 +140,18 @@ export function EditPolicyRulesSectionForm({ const api = createApiClient(useEnvContext()); const router = useRouter(); + const isResourceOverlay = resourceId !== undefined; + + // ── Fetch resource-specific rules when in overlay mode ─────────────────── + const { data: resourceRulesData } = useQuery({ + ...resourceQueries.resourceRules({ resourceId: resourceId! }), + enabled: isResourceOverlay + }); + + const deletedResourceRuleIdsRef = useRef>(new Set()); + const [resourceRulesInitialized, setResourceRulesInitialized] = + useState(false); + const form = useForm({ resolver: zodResolver( createPolicySchema.pick({ @@ -140,8 +170,42 @@ export function EditPolicyRulesSectionForm({ name: "applyRules" }); - const [rules, setRules] = useState(policy.rules); - const [isExpanded, setIsExpanded] = useState(rulesEnabled); + const [rules, setRules] = useState( + policy.rules.map((r) => ({ ...r, fromPolicy: !isResourceOverlay })) + ); + const [isExpanded, setIsExpanded] = useState( + rulesEnabled || isResourceOverlay + ); + + // Initialize resource-specific rules once fetched + useEffect(() => { + if (!isResourceOverlay || resourceRulesInitialized) return; + if (!resourceRulesData) return; + + const policyRuleIds = new Set(policy.rules.map((r) => r.ruleId)); + const resourceSpecific: LocalRule[] = resourceRulesData + .filter((r) => !policyRuleIds.has(r.ruleId)) + .map((r) => ({ + ruleId: r.ruleId, + action: r.action as "ACCEPT" | "DROP" | "PASS", + match: r.match, + value: r.value, + priority: r.priority, + enabled: r.enabled, + fromPolicy: false + })); + + setRules([ + ...policy.rules.map((r) => ({ ...r, fromPolicy: true })), + ...resourceSpecific + ]); + setResourceRulesInitialized(true); + }, [ + isResourceOverlay, + resourceRulesData, + resourceRulesInitialized, + policy.rules + ]); const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false); @@ -275,11 +339,17 @@ export function EditPolicyRulesSectionForm({ const removeRule = useCallback( function removeRule(ruleId: number) { + const rule = rules.find((r) => r.ruleId === ruleId); + if (!rule || rule.fromPolicy) return; // cannot remove policy rules + // Track deletion for resource overlay mode (only for existing DB rules) + if (isResourceOverlay && !rule.new) { + deletedResourceRuleIdsRef.current.add(ruleId); + } const updatedRules = rules.filter((rule) => rule.ruleId !== ruleId); setRules(updatedRules); syncFormRules(updatedRules); }, - [rules, syncFormRules] + [rules, syncFormRules, isResourceOverlay] ); const updateRule = useCallback( @@ -328,35 +398,45 @@ export function EditPolicyRulesSectionForm({ ), - cell: ({ row }) => ( - e.currentTarget.focus()} - onBlur={(e) => { - const parsed = z.coerce - .number() - .int() - .optional() - .safeParse(e.target.value); - if (!parsed.success) { - toast({ - variant: "destructive", - title: t("rulesErrorInvalidPriority"), - description: t( - "rulesErrorInvalidPriorityDescription" - ) + cell: ({ row }) => { + const isLocked = row.original.fromPolicy; + if (isLocked) { + return ( + + — + + ); + } + return ( + e.currentTarget.focus()} + onBlur={(e) => { + const parsed = z.coerce + .number() + .int() + .optional() + .safeParse(e.target.value); + if (!parsed.success) { + toast({ + variant: "destructive", + title: t("rulesErrorInvalidPriority"), + description: t( + "rulesErrorInvalidPriorityDescription" + ) + }); + return; + } + updateRule(row.original.ruleId, { + priority: parsed.data }); - return; - } - updateRule(row.original.ruleId, { - priority: parsed.data - }); - }} - /> - ) + }} + /> + ); + } }, { accessorKey: "action", @@ -364,7 +444,7 @@ export function EditPolicyRulesSectionForm({ cell: ({ row }) => ( @@ -444,7 +524,9 @@ export function EditPolicyRulesSectionForm({ + {row.original.fromPolicy ? ( + + ) : ( + + )}
) } @@ -651,11 +745,15 @@ export function EditPolicyRulesSectionForm({ async function saveRules() { if (readonly) return; + if (isResourceOverlay) { + await saveResourceOverlayRules(); + return; + } + const isValid = form.trigger(); if (!isValid) return; const payload = form.getValues(); - console.log({ payload }); try { const res = await api @@ -689,6 +787,57 @@ export function EditPolicyRulesSectionForm({ } } + async function saveResourceOverlayRules() { + try { + const newRules = rules.filter((r) => !r.fromPolicy && r.new); + const updatedRules = rules.filter( + (r) => !r.fromPolicy && !r.new && r.updated + ); + const deletedIds = [...deletedResourceRuleIdsRef.current]; + + await Promise.all([ + ...newRules.map((r) => + api.put(`/resource/${resourceId}/rule`, { + action: r.action, + match: r.match, + value: r.value, + priority: r.priority, + enabled: r.enabled + }) + ), + ...updatedRules.map((r) => + api.post(`/resource/${resourceId}/rule/${r.ruleId}`, { + action: r.action, + match: r.match, + value: r.value, + priority: r.priority, + enabled: r.enabled + }) + ), + ...deletedIds.map((id) => + api.delete(`/resource/${resourceId}/rule/${id}`) + ) + ]); + + deletedResourceRuleIdsRef.current = new Set(); + + toast({ + title: t("success"), + description: t("policyUpdatedSuccess") + }); + router.refresh(); + } catch (e) { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: formatAxiosError( + e, + t("policyErrorUpdateDescription") + ) + }); + } + } + if (!isExpanded) { return ( @@ -740,7 +889,7 @@ export function EditPolicyRulesSectionForm({ onCheckedChange={(val) => { form.setValue("applyRules", val); }} - disabled={readonly} + disabled={readonly || isResourceOverlay} />
@@ -763,7 +912,8 @@ export function EditPolicyRulesSectionForm({ value={field.value} disabled={ readonly || - !rulesEnabled + (!isResourceOverlay && + !rulesEnabled) } onValueChange={ field.onChange @@ -802,7 +952,8 @@ export function EditPolicyRulesSectionForm({ value={field.value} disabled={ readonly || - !rulesEnabled + (!isResourceOverlay && + !rulesEnabled) } onValueChange={ field.onChange @@ -872,7 +1023,8 @@ export function EditPolicyRulesSectionForm({ role="combobox" disabled={ readonly || - !rulesEnabled + (!isResourceOverlay && + !rulesEnabled) } aria-expanded={ openAddRuleCountrySelect @@ -965,7 +1117,8 @@ export function EditPolicyRulesSectionForm({ role="combobox" disabled={ readonly || - !rulesEnabled + (!isResourceOverlay && + !rulesEnabled) } aria-expanded={ openAddRuleAsnSelect @@ -1083,7 +1236,8 @@ export function EditPolicyRulesSectionForm({ {...field} disabled={ readonly || - !rulesEnabled + (!isResourceOverlay && + !rulesEnabled) } /> )} @@ -1095,7 +1249,10 @@ export function EditPolicyRulesSectionForm({ diff --git a/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx index f535021e2..d55a40a94 100644 --- a/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx +++ b/src/components/resource-policy/EditPolicyUserRolesSectionForm.tsx @@ -45,7 +45,9 @@ import { } from "@app/components/ui/select"; import { useResourcePolicyContext } from "@app/providers/ResourcePolicyProvider"; -import { useActionState, useState } from "react"; +import { resourceQueries } from "@app/lib/queries"; +import { useQuery } from "@tanstack/react-query"; +import { useActionState, useEffect, useMemo, useRef, useState } from "react"; import { useForm, useWatch } from "react-hook-form"; // ─── PolicyUsersRolesSection ────────────────────────────────────────────────── @@ -54,12 +56,14 @@ type PolicyUsersRolesSectionProps = { orgId: string; allIdps: { id: number; text: string }[]; readonly?: boolean; + resourceId?: number; }; export function EditPolicyUsersRolesSectionForm({ orgId, allIdps, - readonly + readonly, + resourceId }: PolicyUsersRolesSectionProps) { const t = useTranslations(); @@ -69,6 +73,105 @@ export function EditPolicyUsersRolesSectionForm({ const api = createApiClient(useEnvContext()); + // ── Resource overlay: fetch resource-specific roles & users ────────────── + const isResourceOverlay = resourceId !== undefined; + + const { data: resourceRolesData } = useQuery({ + ...resourceQueries.resourceRoles({ resourceId: resourceId! }), + enabled: isResourceOverlay + }); + + const { data: resourceUsersData } = useQuery({ + ...resourceQueries.resourceUsers({ resourceId: resourceId! }), + enabled: isResourceOverlay + }); + + // IDs from the policy (locked — cannot be removed) + const policyRoleLockedIds = useMemo( + () => new Set(policy.roles.map((r) => r.roleId.toString())), + [policy.roles] + ); + const policyUserLockedIds = useMemo( + () => new Set(policy.users.map((u) => u.userId)), + [policy.users] + ); + + // Policy entries mapped to selector format + const policyRoleItems = useMemo( + () => + policy.roles.map((r) => ({ + id: r.roleId.toString(), + text: r.name + })), + [policy.roles] + ); + const policyUserItems = useMemo( + () => + policy.users.map((u) => ({ + id: u.userId, + text: `${getUserDisplayName({ email: u.email, username: u.username })}${u.type !== UserType.Internal ? ` (${u.idpName})` : ""}` + })), + [policy.users] + ); + + // Track the initial resource-specific roles/users for diffing on save + const initialResourceRoleIdsRef = useRef>(new Set()); + const initialResourceUserIdsRef = useRef>(new Set()); + + // Combined selected roles/users (policy + resource-specific) + const [combinedRoles, setCombinedRoles] = useState(policyRoleItems); + const [combinedUsers, setCombinedUsers] = useState(policyUserItems); + const [resourceRolesInitialized, setResourceRolesInitialized] = + useState(false); + const [resourceUsersInitialized, setResourceUsersInitialized] = + useState(false); + + useEffect(() => { + if (!isResourceOverlay || resourceRolesInitialized) return; + if (!resourceRolesData) return; + + const resourceSpecific = resourceRolesData + .filter((r) => !policyRoleLockedIds.has(r.roleId.toString())) + .map((r) => ({ id: r.roleId.toString(), text: r.name })); + + initialResourceRoleIdsRef.current = new Set( + resourceSpecific.map((r) => r.id) + ); + setCombinedRoles([...policyRoleItems, ...resourceSpecific]); + setResourceRolesInitialized(true); + }, [ + isResourceOverlay, + resourceRolesData, + resourceRolesInitialized, + policyRoleItems, + policyRoleLockedIds + ]); + + useEffect(() => { + if (!isResourceOverlay || resourceUsersInitialized) return; + if (!resourceUsersData) return; + + const resourceSpecific = resourceUsersData + .filter((u) => !policyUserLockedIds.has(u.userId)) + .map((u) => ({ + id: u.userId, + text: `${getUserDisplayName({ email: u.email ?? undefined, username: u.username ?? undefined })}${u.type !== UserType.Internal ? ` (${u.idpName})` : ""}` + })); + + initialResourceUserIdsRef.current = new Set( + resourceSpecific.map((u) => u.id) + ); + setCombinedUsers([...policyUserItems, ...resourceSpecific]); + setResourceUsersInitialized(true); + }, [ + isResourceOverlay, + resourceUsersData, + resourceUsersInitialized, + policyUserItems, + policyUserLockedIds + ]); + + // ── Standard policy form (non-overlay) ────────────────────────────────── const form = useForm({ resolver: zodResolver( createPolicySchema.pick({ @@ -81,14 +184,8 @@ export function EditPolicyUsersRolesSectionForm({ defaultValues: { sso: policy.sso, skipToIdpId: policy.idpId, - roles: policy.roles.map((role) => ({ - id: role.roleId.toString(), - text: role.name - })), - users: policy.users.map((user) => ({ - id: user.userId, - text: `${getUserDisplayName({ email: user.email, username: user.username })}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` - })) + roles: policyRoleItems, + users: policyUserItems } }); @@ -99,12 +196,17 @@ export function EditPolicyUsersRolesSectionForm({ }); const [, formAction, isSubmitting] = useActionState(onSubmit, null); + const [isSavingOverlay, setIsSavingOverlay] = useState(false); async function onSubmit() { if (readonly) return; - const isValid = await form.trigger(); + if (isResourceOverlay) { + await saveResourceOverlay(); + return; + } + const isValid = await form.trigger(); if (!isValid) return; const payload = form.getValues(); @@ -147,6 +249,87 @@ export function EditPolicyUsersRolesSectionForm({ } } + async function saveResourceOverlay() { + setIsSavingOverlay(true); + try { + // Compute which roles/users are resource-specific (non-locked) + const currentResourceRoleIds = new Set( + combinedRoles + .filter((r) => !policyRoleLockedIds.has(r.id)) + .map((r) => r.id) + ); + const currentResourceUserIds = new Set( + combinedUsers + .filter((u) => !policyUserLockedIds.has(u.id)) + .map((u) => u.id) + ); + + const initialRoleIds = initialResourceRoleIdsRef.current; + const initialUserIds = initialResourceUserIdsRef.current; + + const addedRoleIds = [...currentResourceRoleIds].filter( + (id) => !initialRoleIds.has(id) + ); + const removedRoleIds = [...initialRoleIds].filter( + (id) => !currentResourceRoleIds.has(id) + ); + const addedUserIds = [...currentResourceUserIds].filter( + (id) => !initialUserIds.has(id) + ); + const removedUserIds = [...initialUserIds].filter( + (id) => !currentResourceUserIds.has(id) + ); + + await Promise.all([ + ...addedRoleIds.map((id) => + api.post(`/resource/${resourceId}/roles/add`, { + roleId: Number(id) + }) + ), + ...removedRoleIds.map((id) => + api.post(`/resource/${resourceId}/roles/remove`, { + roleId: Number(id) + }) + ), + ...addedUserIds.map((id) => + api.post(`/resource/${resourceId}/users/add`, { + userId: id + }) + ), + ...removedUserIds.map((id) => + api.post(`/resource/${resourceId}/users/remove`, { + userId: id + }) + ) + ]); + + // Update refs to reflect new state + initialResourceRoleIdsRef.current = currentResourceRoleIds; + initialResourceUserIdsRef.current = currentResourceUserIds; + + toast({ + title: t("success"), + description: t("policyUpdatedSuccess") + }); + router.refresh(); + } catch (e) { + toast({ + variant: "destructive", + title: t("policyErrorUpdate"), + description: formatAxiosError( + e, + t("policyErrorUpdateDescription") + ) + }); + } finally { + setIsSavingOverlay(false); + } + } + + const isLoading = + isResourceOverlay && + (!resourceRolesInitialized || !resourceUsersInitialized); + return (
@@ -166,78 +349,105 @@ export function EditPolicyUsersRolesSectionForm({ label={t("ssoUse")} defaultChecked={ssoEnabled} onCheckedChange={(val) => { - console.log(`form.setValue("sso", ${val})`); form.setValue("sso", val); }} - disabled={readonly} + disabled={readonly || isResourceOverlay} /> {ssoEnabled && ( <> - ( - - - {t("roles")} - - - - form.setValue( - "roles", + + {t("roles")} + + {isResourceOverlay ? ( + + ) : ( + ( + - - - - {t( - "resourceRoleDescription" + ) => + form.setValue( + "roles", + roles + ) + } + disabled={readonly} + restrictAdminRole + /> )} - - - )} - /> - ( - - - {t("users")} - - - - form.setValue( - "users", + /> + )} + + + + {t("resourceRoleDescription")} + + + + + {t("users")} + + {isResourceOverlay ? ( + + ) : ( + ( + - - - - )} - /> + ) => + form.setValue( + "users", + users + ) + } + disabled={readonly} + /> + )} + /> + )} + + + )} @@ -247,7 +457,7 @@ export function EditPolicyUsersRolesSectionForm({ {t("defaultIdentityProvider")} + + + + + + ) : ( t("labelsNotFound") )} From 2fd519e102d8fba6d7414db910ef4baed0ddcc2f Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 8 May 2026 22:31:36 +0200 Subject: [PATCH 114/771] =?UTF-8?q?=E2=9C=A8=20add=20and=20toggle=20site?= =?UTF-8?q?=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/private/routers/external.ts | 2 +- server/routers/site/listSites.ts | 2 +- src/app/[orgId]/settings/sites/page.tsx | 1 + src/components/SitesTable.tsx | 187 +++++++++++++++--------- src/components/labels-selector.tsx | 68 +++++---- 5 files changed, 156 insertions(+), 104 deletions(-) diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 5b146da18..5e20f6db6 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -765,7 +765,7 @@ authenticated.put( labels.attachLabelToItem ); -authenticated.delete( +authenticated.put( "/org/:orgId/label/:labelId/detach", verifyValidLicense, verifyOrgAccess, diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 8c35a2521..ac1942574 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -392,7 +392,7 @@ export async function listSites( .select({ labelId: labels.labelId, name: labels.name, - color: labels.name, + color: labels.color, siteId: siteLabels.siteId }) .from(labels) diff --git a/src/app/[orgId]/settings/sites/page.tsx b/src/app/[orgId]/settings/sites/page.tsx index 631baee41..6542959a3 100644 --- a/src/app/[orgId]/settings/sites/page.tsx +++ b/src/app/[orgId]/settings/sites/page.tsx @@ -60,6 +60,7 @@ export default async function SitesPage(props: SitesPageProps) { return { name: site.name, id: site.siteId, + labels: site.labels, nice: site.niceId.toString(), address: site.address?.split("/")[0], mbIn: formatSize(site.megabytesIn || 0, site.type), diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index a50bc8b20..57e9ea8a9 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -3,6 +3,16 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import UptimeMiniBar from "@app/components/UptimeMiniBar"; +import { + Credenza, + CredenzaBody, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import SiteResourcesOverview from "@app/components/SiteResourcesOverview"; import { Badge } from "@app/components/ui/badge"; import { Button } from "@app/components/ui/button"; import { @@ -14,9 +24,9 @@ import { import { InfoPopup } from "@app/components/ui/info-popup"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useNavigationContext } from "@app/hooks/useNavigationContext"; -import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; import { build } from "@server/build"; import { type PaginationState } from "@tanstack/react-table"; import { @@ -27,32 +37,30 @@ import { ChevronDown, ChevronsUpDownIcon, MoreHorizontal, - PlusIcon + PlusIcon, + XIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; -import { useState, useTransition, useEffect } from "react"; +import { + startTransition, + useEffect, + useOptimistic, + useState, + useTransition +} from "react"; import { useDebouncedCallback } from "use-debounce"; import z from "zod"; import { ColumnFilterButton } from "./ColumnFilterButton"; -import SiteResourcesOverview from "@app/components/SiteResourcesOverview"; -import { - Credenza, - CredenzaBody, - CredenzaContent, - CredenzaDescription, - CredenzaFooter, - CredenzaHeader, - CredenzaTitle -} from "@app/components/Credenza"; import { ControlledDataTable, type ExtendedColumnDef } from "./ui/controlled-data-table"; -import { Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip"; + +import { LabelsSelector, type SelectedLabel } from "./labels-selector"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; -import { LabelsSelector } from "./labels-selector"; +import { cn } from "@app/lib/cn"; export type SiteRow = { id: number; @@ -463,36 +471,7 @@ export default function SitesTable({ accessorKey: "labels", header: () => {t("labels")}, cell: ({ row }) => { - const labels = row.original.labels ?? []; - return ( -
- - - - - - {}} - /> - - -
- ); + return ; } }, { @@ -653,12 +632,6 @@ export default function SitesTable({ string={selectedSite.name} title={t("siteDelete")} /> - - {/* */} )} @@ -696,36 +669,104 @@ export default function SitesTable({ ); } -type SiteLabelsDialogProps = { +type SiteLabelCellProps = { site: SiteRow; - isOpen: boolean; - setIsOpen: (open: boolean) => void; + orgId: string; }; -function SiteLabelsDialog({ site, isOpen, setIsOpen }: SiteLabelsDialogProps) { +function SiteLabelCell({ site, orgId }: SiteLabelCellProps) { const t = useTranslations(); + + const api = createApiClient(useEnvContext()); + + const router = useRouter(); + + const labels = site.labels ?? []; + const [optimisticLabels, setOptimisticLabels] = useOptimistic(labels); + + function toggleSiteLabel( + label: SelectedLabel, + action: "attach" | "detach" + ) { + startTransition(async () => { + try { + if (action === "attach") { + setOptimisticLabels([...optimisticLabels, label]); + + await api.put( + `/org/${orgId}/label/${label.labelId}/attach`, + { siteId: site.id } + ); + } else { + setOptimisticLabels( + optimisticLabels.filter( + (lb) => lb.labelId !== label.labelId + ) + ); + await api.put( + `/org/${orgId}/label/${label.labelId}/detach`, + { siteId: site.id } + ); + } + } catch (e) { + toast({ + title: t("error"), + description: formatAxiosError(e, t("errorOccurred")), + variant: "destructive" + }); + } finally { + router.refresh(); + } + }); + } + return ( - - - - {t("siteLabelsTab")} - - {t("siteLabelsDescription")} - - - - <> - - +
+ {optimisticLabels.map((label) => ( + + ))} + + - - - + + + + + +
); } diff --git a/src/components/labels-selector.tsx b/src/components/labels-selector.tsx index ec8d7f270..64a80b26a 100644 --- a/src/components/labels-selector.tsx +++ b/src/components/labels-selector.tsx @@ -21,12 +21,13 @@ import { SelectTrigger, SelectValue } from "./ui/select"; -import { createApiClient } from "@app/lib/api"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; import type { AxiosResponse } from "axios"; +import { toast } from "@app/hooks/useToast"; -type SelectedLabel = { +export type SelectedLabel = { name: string; color: string; labelId: number; @@ -35,8 +36,7 @@ type SelectedLabel = { export type LabelsSelectorProps = { orgId: string; selectedLabels: SelectedLabel[]; - onSelectionChange: (sites: SelectedLabel[]) => void; - onCreateLabel: (newlabel: SelectedLabel) => Promise; + toggleLabel: (newlabel: SelectedLabel, action: "detach" | "attach") => void; }; const LABEL_COLORS = { @@ -52,8 +52,7 @@ const LABEL_COLORS = { export function LabelsSelector({ orgId, selectedLabels, - onSelectionChange, - onCreateLabel + toggleLabel }: LabelsSelectorProps) { const t = useTranslations(); const [labelSearchQuery, setlabelsSearchQuery] = useState(""); @@ -94,17 +93,28 @@ export function LabelsSelector({ async function createLabel(_: any, formData: FormData) { const name = formData.get("name")?.toString(); const color = formData.get("color")?.toString(); - const res = await api.post>( - `/org/${orgId}/labels`, - { name, color } - ); + try { + const res = await api.post< + AxiosResponse + >(`/org/${orgId}/labels`, { name, color }); - const { label } = res.data.data; - await onCreateLabel({ - labelId: label.labelId, - name: label.name, - color: label.color - }); + const { label } = res.data.data; + + toggleLabel( + { + labelId: label.labelId, + name: label.name, + color: label.color + }, + "attach" + ); + } catch (e) { + toast({ + title: t("error"), + description: formatAxiosError(e, t("errorOccurred")), + variant: "destructive" + }); + } setlabelsSearchQuery(""); } @@ -185,18 +195,18 @@ export function LabelsSelector({ key={label.labelId} value={`${label.labelId}`} onSelect={() => { - if (selectedIds.has(label.labelId)) { - onSelectionChange( - selectedLabels.filter( - (l) => l.labelId !== label.labelId - ) - ); - } else { - onSelectionChange([ - ...selectedLabels, - label - ]); - } + toggleLabel( + label, + selectedIds.has(label.labelId) + ? "detach" + : "attach" + ); + // } else { + // onSelectionChange([ + // ...selectedLabels, + // label + // ]); + // } }} >
Date: Mon, 11 May 2026 16:57:53 +0200 Subject: [PATCH 115/771] =?UTF-8?q?=F0=9F=90=9B=20handle=20idempotency=20w?= =?UTF-8?q?hen=20adding/removing=20labels=20from=20sites/resources?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 60 +++++++++++-------- .../routers/labels/attachLabelToItem.ts | 15 +++-- 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 0a4066db3..a797e3ddc 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -173,33 +173,41 @@ export const labels = pgTable("labels", { .notNull() }); -export const siteLabels = pgTable("siteLabels", { - siteLabelId: serial("siteLabelId").primaryKey(), - siteId: integer("siteId") - .references(() => sites.siteId, { - onDelete: "cascade" - }) - .notNull(), - labelId: integer("labelId") - .references(() => labels.labelId, { - onDelete: "cascade" - }) - .notNull() -}); +export const siteLabels = pgTable( + "siteLabels", + { + siteLabelId: serial("siteLabelId").primaryKey(), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), + labelId: integer("labelId") + .references(() => labels.labelId, { + onDelete: "cascade" + }) + .notNull() + }, + (t) => [unique("site_label_uniq").on(t.siteId, t.labelId)] +); -export const resourceLabels = pgTable("resourceLabels", { - resourceLabelId: serial("resourceLabelId").primaryKey(), - resourceId: integer("resourceId") - .references(() => resources.resourceId, { - onDelete: "cascade" - }) - .notNull(), - labelId: integer("labelId") - .references(() => labels.labelId, { - onDelete: "cascade" - }) - .notNull() -}); +export const resourceLabels = pgTable( + "resourceLabels", + { + resourceLabelId: serial("resourceLabelId").primaryKey(), + resourceId: integer("resourceId") + .references(() => resources.resourceId, { + onDelete: "cascade" + }) + .notNull(), + labelId: integer("labelId") + .references(() => labels.labelId, { + onDelete: "cascade" + }) + .notNull() + }, + (t) => [unique("resource_label_uniq").on(t.resourceId, t.labelId)] +); export const targets = pgTable("targets", { targetId: serial("targetId").primaryKey(), diff --git a/server/private/routers/labels/attachLabelToItem.ts b/server/private/routers/labels/attachLabelToItem.ts index 392332776..79ea360de 100644 --- a/server/private/routers/labels/attachLabelToItem.ts +++ b/server/private/routers/labels/attachLabelToItem.ts @@ -106,13 +106,14 @@ export async function attachLabelToItem( ); } + // idempotent, calling this endpoint multiple times should attach the label only once await db .insert(siteLabels) .values({ labelId, siteId }) - .returning(); + .onConflictDoNothing(); } if (resourceId) { @@ -133,10 +134,14 @@ export async function attachLabelToItem( ); } - await db.insert(resourceLabels).values({ - labelId, - resourceId - }); + // idempotent, calling this endpoint multiple times should attach the label only once + await db + .insert(resourceLabels) + .values({ + labelId, + resourceId + }) + .onConflictDoNothing(); } return response(res, { From c44c02b8ba1e2f9f1764ba871beb720b0b439150 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 11 May 2026 17:04:44 +0200 Subject: [PATCH 116/771] =?UTF-8?q?=F0=9F=92=84=20make=20site=20labels=20c?= =?UTF-8?q?olumn=20design=20nicer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SitesTable.tsx | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index 57e9ea8a9..f75184a00 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -469,7 +469,11 @@ export default function SitesTable({ // The label feature should be added to the tiers { accessorKey: "labels", - header: () => {t("labels")}, + header: () => ( + + {t("labels")} + + ), cell: ({ row }) => { return ; } @@ -679,6 +683,8 @@ function SiteLabelCell({ site, orgId }: SiteLabelCellProps) { const api = createApiClient(useEnvContext()); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const router = useRouter(); const labels = site.labels ?? []; @@ -722,7 +728,7 @@ function SiteLabelCell({ site, orgId }: SiteLabelCellProps) { return (
- {optimisticLabels.map((label) => ( + {optimisticLabels.slice(0, 3).map((label) => ( ))} - + {optimisticLabels.length > 3 && ( + + )} +
From 8a0c2031d4af408ea8bfa19fa44d0569cb645710 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 11 May 2026 18:02:59 +0200 Subject: [PATCH 119/771] =?UTF-8?q?=E2=9C=A8=20search=20list=20by=20labels?= =?UTF-8?q?=20too?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/site/listSites.ts | 100 +++++++++++++++++++------------ 1 file changed, 62 insertions(+), 38 deletions(-) diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index ac1942574..829379412 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -190,9 +190,9 @@ const listSitesSchema = z.object({ }) }); -function querySitesBase() { - return db - .select({ +function querySitesBase(isLabelFeatureEnabled: boolean) { + let query = db + .selectDistinct({ siteId: sites.siteId, niceId: sites.niceId, name: sites.name, @@ -231,6 +231,14 @@ function querySitesBase() { remoteExitNodes, eq(remoteExitNodes.exitNodeId, sites.exitNodeId) ); + + if (isLabelFeatureEnabled) { + query = query + .leftJoin(siteLabels, eq(siteLabels.siteId, sites.siteId)) + .leftJoin(labels, eq(labels.labelId, siteLabels.labelId)); + } + + return query; } type SiteRowBase = Awaited>[0]; @@ -314,6 +322,11 @@ export async function listSites( .where(eq(sites.orgId, orgId)); } + const isLabelFeatureEnabled = await isLicensedOrSubscribed( + orgId, + tierMatrix.labels + ); + const { pageSize, page, query, sort_by, order, online, status } = parsedQuery.data; @@ -325,31 +338,43 @@ export async function listSites( eq(sites.orgId, orgId) ) ]; - if (query) { - conditions.push( - or( - like( - sql`LOWER(${sites.name})`, - "%" + query.toLowerCase() + "%" - ), - like( - sql`LOWER(${sites.niceId})`, - "%" + query.toLowerCase() + "%" - ) - ) - ); - } + if (typeof online !== "undefined") { conditions.push(eq(sites.online, online)); } if (typeof status !== "undefined") { conditions.push(eq(sites.status, status)); } - const baseQuery = querySitesBase().where(and(...conditions)); + if (query) { + const queryList = [ + like( + sql`LOWER(${sites.name})`, + "%" + query.toLowerCase() + "%" + ), + like( + sql`LOWER(${sites.niceId})`, + "%" + query.toLowerCase() + "%" + ) + ]; + + if (isLabelFeatureEnabled) { + queryList.push( + like( + sql`LOWER(${labels.name})`, + "%" + query.toLowerCase() + "%" + ) + ); + } + conditions.push(or(...queryList)); + } + + const baseQuery = querySitesBase(isLabelFeatureEnabled).where( + and(...conditions) + ); // we need to add `as` so that drizzle filters the result as a subquery const countQuery = db.$count( - querySitesBase() + querySitesBase(isLabelFeatureEnabled) .where(and(...conditions)) .as("filtered_sites") ); @@ -382,25 +407,24 @@ export async function listSites( siteId: number; }> = []; - // The label feature should be added in the tiers - // if (await isLicensedOrSubscribed(orgId, tierMatrix.fullRbac)) { - // } - labelsForSites = - siteIds.length === 0 - ? [] - : await db - .select({ - labelId: labels.labelId, - name: labels.name, - color: labels.color, - siteId: siteLabels.siteId - }) - .from(labels) - .innerJoin( - siteLabels, - eq(siteLabels.labelId, labels.labelId) - ) - .where(inArray(siteLabels.siteId, siteIds)); + if (isLabelFeatureEnabled) { + labelsForSites = + siteIds.length === 0 + ? [] + : await db + .select({ + labelId: labels.labelId, + name: labels.name, + color: labels.color, + siteId: siteLabels.siteId + }) + .from(labels) + .innerJoin( + siteLabels, + eq(siteLabels.labelId, labels.labelId) + ) + .where(inArray(siteLabels.siteId, siteIds)); + } const sitesWithUpdates: SiteWithUpdateAvailable[] = rows.map((site) => { const siteWithUpdate: SiteWithUpdateAvailable = { ...site }; From 21f72639b69325afab8c6440f771d02f7ad14cd0 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 11 May 2026 18:13:19 +0200 Subject: [PATCH 120/771] =?UTF-8?q?=F0=9F=9A=A7=20make=20labels=20column?= =?UTF-8?q?=20paid,=20and=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SitesTable.tsx | 109 +++++++++++++++------------------- 1 file changed, 49 insertions(+), 60 deletions(-) diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index f75184a00..2dd793841 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -37,8 +37,7 @@ import { ChevronDown, ChevronsUpDownIcon, MoreHorizontal, - PlusIcon, - XIcon + PlusIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; @@ -58,9 +57,11 @@ import { type ExtendedColumnDef } from "./ui/controlled-data-table"; +import { cn } from "@app/lib/cn"; import { LabelsSelector, type SelectedLabel } from "./labels-selector"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; -import { cn } from "@app/lib/cn"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; export type SiteRow = { id: number; @@ -110,10 +111,12 @@ export default function SitesTable({ const [selectedSite, setSelectedSite] = useState(null); const [resourcesDialogSite, setResourcesDialogSite] = useState(null); - const [isLabelsDialogOpen, setIsLabelsDialogOpen] = useState(false); const [isRefreshing, startTransition] = useTransition(); const [isNavigatingToAddPage, startNavigation] = useTransition(); + const { isPaidUser } = usePaidStatus(); + const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels); + const api = createApiClient(useEnvContext()); const t = useTranslations(); @@ -466,18 +469,26 @@ export default function SitesTable({ ); } }, - // The label feature should be added to the tiers - { - accessorKey: "labels", - header: () => ( - - {t("labels")} - - ), - cell: ({ row }) => { - return ; - } - }, + ...(isLabelFeatureEnabled + ? [ + { + accessorKey: "labels", + header: () => ( + + {t("labels")} + + ), + cell: ({ row }: { row: { original: SiteRow } }) => { + return ( + + ); + } + } + ] + : []), { id: "actions", enableHiding: false, @@ -518,24 +529,6 @@ export default function SitesTable({ {t("sitesTableViewPrivateResources")} - { - setSelectedSite(siteRow); - setIsLabelsDialogOpen(true); - }} - > - {t("addLabels")} - - { - setSelectedSite(siteRow); - setIsDeleteModalOpen(true); - }} - > - - {t("delete")} - - {selectedSite && ( - <> - { - setIsDeleteModalOpen(val); - setSelectedSite(null); - }} - dialog={ -
-

{t("siteQuestionRemove")}

-

{t("siteMessageRemove")}

-
- } - buttonText={t("siteConfirmDelete")} - onConfirm={async () => - startTransition(() => deleteSite(selectedSite!.id)) - } - string={selectedSite.name} - title={t("siteDelete")} - /> - + { + setIsDeleteModalOpen(val); + setSelectedSite(null); + }} + dialog={ +
+

{t("siteQuestionRemove")}

+

{t("siteMessageRemove")}

+
+ } + buttonText={t("siteConfirmDelete")} + onConfirm={async () => + startTransition(() => deleteSite(selectedSite!.id)) + } + string={selectedSite.name} + title={t("siteDelete")} + /> )} toggleSiteLabel(label, "detach")} + onClick={() => setIsPopoverOpen(true)} className={cn( "inline-flex gap-1 items-center", "rounded-full text-sm cursor-pointer", - "px-1.5 py-0 h-auto" + "pl-1.5 pr-2 py-0 h-auto" )} >
- + {label.name} - - ))} {optimisticLabels.length > 3 && ( From 6e066d38b096236ebcab0a0631730fa6df147fe8 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 11 May 2026 18:17:29 +0200 Subject: [PATCH 121/771] =?UTF-8?q?=F0=9F=9A=9A=20Make=20label=20badge=20i?= =?UTF-8?q?ts=20own=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SitesTable.tsx | 27 +++++------------------ src/components/label-badge.tsx | 40 ++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 src/components/label-badge.tsx diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index 2dd793841..b8064c933 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -57,11 +57,12 @@ import { type ExtendedColumnDef } from "./ui/controlled-data-table"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { cn } from "@app/lib/cn"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; +import { LabelBadge } from "./label-badge"; import { LabelsSelector, type SelectedLabel } from "./labels-selector"; import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; -import { usePaidStatus } from "@app/hooks/usePaidStatus"; -import { tierMatrix } from "@server/lib/billing/tierMatrix"; export type SiteRow = { id: number; @@ -720,27 +721,11 @@ function SiteLabelCell({ site, orgId }: SiteLabelCellProps) { return (
{optimisticLabels.slice(0, 3).map((label) => ( - + {...label} + /> ))} {optimisticLabels.length > 3 && ( + ); +} From 14e1a119d3c313158a19d65981e96fc01cce8565 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 11 May 2026 18:24:47 +0200 Subject: [PATCH 122/771] =?UTF-8?q?=F0=9F=9A=A7=20WIP:=20showing=20labels?= =?UTF-8?q?=20in=20proxy=20resources=20table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/resource/listResources.ts | 36 +-- src/components/ProxyResourcesTable.tsx | 320 +++++++++++++---------- 2 files changed, 195 insertions(+), 161 deletions(-) diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 16a82e400..ab2e41204 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -325,24 +325,6 @@ export async function listResources( ) ]; - if (query) { - conditions.push( - or( - like( - sql`LOWER(${resources.name})`, - "%" + query.toLowerCase() + "%" - ), - like( - sql`LOWER(${resources.niceId})`, - "%" + query.toLowerCase() + "%" - ), - like( - sql`LOWER(${resources.fullDomain})`, - "%" + query.toLowerCase() + "%" - ) - ) - ); - } if (typeof enabled !== "undefined") { conditions.push(eq(resources.enabled, enabled)); } @@ -386,6 +368,24 @@ export async function listResources( .where(and(eq(sites.orgId, orgId), eq(sites.siteId, siteId))); conditions.push(inArray(resources.resourceId, resourcesWithSite)); } + if (query) { + conditions.push( + or( + like( + sql`LOWER(${resources.name})`, + "%" + query.toLowerCase() + "%" + ), + like( + sql`LOWER(${resources.niceId})`, + "%" + query.toLowerCase() + "%" + ), + like( + sql`LOWER(${resources.fullDomain})`, + "%" + query.toLowerCase() + "%" + ) + ) + ); + } const baseQuery = queryResourcesBase().where(and(...conditions)); diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index 98ddd8eb7..21a770a68 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -2,10 +2,12 @@ import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import CopyToClipboard from "@app/components/CopyToClipboard"; +import { ResourceAccessCertIndicator } from "@app/components/ResourceAccessCertIndicator"; import { ResourceSitesStatusCell, type ResourceSiteRow } from "@app/components/ResourceSitesStatusCell"; +import { Selectedsite, SitesSelector } from "@app/components/site-selector"; import { Badge } from "@app/components/ui/badge"; import { Button } from "@app/components/ui/button"; import { ExtendedColumnDef } from "@app/components/ui/data-table"; @@ -24,12 +26,14 @@ import { import { Switch } from "@app/components/ui/switch"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useNavigationContext } from "@app/hooks/useNavigationContext"; -import { Selectedsite, SitesSelector } from "@app/components/site-selector"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import { cn } from "@app/lib/cn"; import { dataTableFilterPopoverContentClassName } from "@app/lib/dataTableFilterPopover"; import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; -import { toast } from "@app/hooks/useToast"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { build } from "@server/build"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { UpdateResourceResponse } from "@server/routers/resource"; import type { PaginationState } from "@tanstack/react-table"; import { AxiosResponse } from "axios"; @@ -64,8 +68,6 @@ import z from "zod"; import { ColumnFilterButton } from "./ColumnFilterButton"; import { ControlledDataTable } from "./ui/controlled-data-table"; import UptimeMiniBar from "./UptimeMiniBar"; -import { ResourceAccessCertIndicator } from "@app/components/ResourceAccessCertIndicator"; -import { build } from "@server/build"; export type TargetHealth = { targetId: number; @@ -97,31 +99,13 @@ export type ResourceRow = { health?: "healthy" | "degraded" | "unhealthy" | "unknown"; sites: ResourceSiteRow[]; wildcard?: boolean; + labels?: Array<{ + labelId: number; + name: string; + color: string; + }>; }; -function StatusIcon({ - status, - className = "" -}: { - status: string | undefined | null; - className?: string; -}) { - const iconClass = `h-4 w-4 ${className}`; - - switch (status) { - case "healthy": - return ; - case "degraded": - return ; - case "unhealthy": - return ; - case "unknown": - return ; - default: - return null; - } -} - type ProxyResourcesTableProps = { resources: ResourceRow[]; orgId: string; @@ -153,6 +137,9 @@ export default function ProxyResourcesTable({ const [selectedResource, setSelectedResource] = useState(); + const { isPaidUser } = usePaidStatus(); + const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels); + const [isRefreshing, startTransition] = useTransition(); const [isNavigatingToAddPage, startNavigation] = useTransition(); const [siteFilterOpen, setSiteFilterOpen] = useState(false); @@ -233,120 +220,6 @@ export default function ProxyResourcesTable({ } } - function TargetStatusCell({ - targets, - healthStatus - }: { - targets?: TargetHealth[]; - healthStatus?: string; - }) { - const overallStatus = healthStatus; - - if (!targets || targets.length === 0) { - return ( -
- - - {t("resourcesTableNoTargets")} - -
- ); - } - - const monitoredTargets = targets.filter( - (t) => t.enabled && t.healthStatus && t.healthStatus !== "unknown" - ); - const unknownTargets = targets.filter( - (t) => !t.enabled || !t.healthStatus || t.healthStatus === "unknown" - ); - - return ( - - - - - - {monitoredTargets.length > 0 && ( - <> - {monitoredTargets.map((target) => ( - -
- - {target.siteName - ? `${target.siteName} (${target.ip}:${target.port})` - : `${target.ip}:${target.port}`} -
- - {target.healthStatus} - -
- ))} - - )} - {unknownTargets.length > 0 && ( - <> - {unknownTargets.map((target) => ( - -
- - {target.siteName - ? `${target.siteName} (${target.ip}:${target.port})` - : `${target.ip}:${target.port}`} -
- - {!target.enabled - ? t("disabled") - : t("resourcesTableNotMonitored")} - -
- ))} - - )} -
-
- ); - } - const proxyColumns: ExtendedColumnDef[] = [ { accessorKey: "name", @@ -653,6 +526,28 @@ export default function ProxyResourcesTable({ /> ) }, + ...(isLabelFeatureEnabled + ? [ + { + id: "labels", + accessorKey: "labels", + header: () => ( + + {t("labels")} + + ), + cell: ({ row }: { row: { original: ResourceRow } }) => { + return ( + // + <> + ); + } + } + ] + : []), { id: "actions", enableHiding: false, @@ -800,7 +695,11 @@ export default function ProxyResourcesTable({ isRefreshing={isRefreshing || isFiltering} isNavigatingToAddPage={isNavigatingToAddPage} enableColumnVisibility - columnVisibility={{ niceId: false, protocol: false }} + columnVisibility={{ + niceId: false, + protocol: false, + labels: false + }} stickyLeftColumn="name" stickyRightColumn="actions" /> @@ -808,6 +707,118 @@ export default function ProxyResourcesTable({ ); } +function TargetStatusCell({ + targets, + healthStatus +}: { + targets?: TargetHealth[]; + healthStatus?: string; +}) { + const overallStatus = healthStatus; + const t = useTranslations(); + + if (!targets || targets.length === 0) { + return ( +
+ + {t("resourcesTableNoTargets")} +
+ ); + } + + const monitoredTargets = targets.filter( + (t) => t.enabled && t.healthStatus && t.healthStatus !== "unknown" + ); + const unknownTargets = targets.filter( + (t) => !t.enabled || !t.healthStatus || t.healthStatus === "unknown" + ); + + return ( + + + + + + {monitoredTargets.length > 0 && ( + <> + {monitoredTargets.map((target) => ( + +
+ + {target.siteName + ? `${target.siteName} (${target.ip}:${target.port})` + : `${target.ip}:${target.port}`} +
+ + {target.healthStatus} + +
+ ))} + + )} + {unknownTargets.length > 0 && ( + <> + {unknownTargets.map((target) => ( + +
+ + {target.siteName + ? `${target.siteName} (${target.ip}:${target.port})` + : `${target.ip}:${target.port}`} +
+ + {!target.enabled + ? t("disabled") + : t("resourcesTableNotMonitored")} + +
+ ))} + + )} +
+
+ ); +} + type ResourceEnabledFormProps = { resource: ResourceRow; onToggleResourceEnabled: ( @@ -847,3 +858,26 @@ function ResourceEnabledForm({ ); } + +function StatusIcon({ + status, + className = "" +}: { + status: string | undefined | null; + className?: string; +}) { + const iconClass = `h-4 w-4 ${className}`; + + switch (status) { + case "healthy": + return ; + case "degraded": + return ; + case "unhealthy": + return ; + case "unknown": + return ; + default: + return null; + } +} From a0759a79a1616c8b5959e03a14c8f543882cd291 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 11 May 2026 18:28:40 +0200 Subject: [PATCH 123/771] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20add=20unique=20?= =?UTF-8?q?indexes=20to=20site=20&=20resource=20labels=20in=20sqlite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/sqlite/schema/schema.ts | 64 +++++++++++++++++-------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 3695e29a0..924581120 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -194,35 +194,43 @@ export const labels = sqliteTable("labels", { .notNull() }); -export const siteLabels = sqliteTable("siteLabels", { - siteLabelId: integer("siteLabelId").primaryKey({ autoIncrement: true }), - siteId: integer("siteId") - .references(() => sites.siteId, { - onDelete: "cascade" - }) - .notNull(), - labelId: integer("labelId") - .references(() => labels.labelId, { - onDelete: "cascade" - }) - .notNull() -}); +export const siteLabels = sqliteTable( + "siteLabels", + { + siteLabelId: integer("siteLabelId").primaryKey({ autoIncrement: true }), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), + labelId: integer("labelId") + .references(() => labels.labelId, { + onDelete: "cascade" + }) + .notNull() + }, + (t) => [unique("site_label_uniq").on(t.siteId, t.labelId)] +); -export const resourceLabels = sqliteTable("resourceLabels", { - resourceLabelId: integer("resourceLabelId").primaryKey({ - autoIncrement: true - }), - resourceId: integer("resourceId") - .references(() => resources.resourceId, { - onDelete: "cascade" - }) - .notNull(), - labelId: integer("labelId") - .references(() => labels.labelId, { - onDelete: "cascade" - }) - .notNull() -}); +export const resourceLabels = sqliteTable( + "resourceLabels", + { + resourceLabelId: integer("resourceLabelId").primaryKey({ + autoIncrement: true + }), + resourceId: integer("resourceId") + .references(() => resources.resourceId, { + onDelete: "cascade" + }) + .notNull(), + labelId: integer("labelId") + .references(() => labels.labelId, { + onDelete: "cascade" + }) + .notNull() + }, + (t) => [unique("resource_label_uniq").on(t.resourceId, t.labelId)] +); export const targets = sqliteTable("targets", { targetId: integer("targetId").primaryKey({ autoIncrement: true }), From 549e1ead1dd3a6e86e3346fab1bcb110b319ecc5 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 11 May 2026 18:30:23 +0200 Subject: [PATCH 124/771] =?UTF-8?q?=E2=9C=A8=20handle=20labels=20in=20reso?= =?UTF-8?q?urces=20too?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/resource/listResources.ts | 112 ++++++++++++++---- .../[orgId]/settings/resources/proxy/page.tsx | 1 + 2 files changed, 88 insertions(+), 25 deletions(-) diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index ab2e41204..6756d8657 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -1,7 +1,9 @@ import { db, + labels, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility, + resourceLabels, resourcePassword, resourcePincode, resources, @@ -9,8 +11,11 @@ import { sites, targetHealthCheck, targets, - userResources + userResources, + type Label } from "@server/db"; +import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; import response from "@server/lib/response"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; @@ -154,10 +159,11 @@ export type ResourceWithTargets = { siteNiceId: string; online?: boolean; // undefined for local sites }>; + labels?: Array>; }; -function queryResourcesBase() { - return db +function queryResourcesBase(isLabelFeatureEnabled: boolean) { + let query = db .select({ resourceId: resources.resourceId, name: resources.name, @@ -203,14 +209,24 @@ function queryResourcesBase() { .leftJoin( targetHealthCheck, eq(targetHealthCheck.targetId, targets.targetId) - ) - .groupBy( - resources.resourceId, - resourcePassword.passwordId, - resourcePincode.pincodeId, - resourceHeaderAuth.headerAuthId, - resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId ); + + if (isLabelFeatureEnabled) { + query = query + .leftJoin( + resourceLabels, + eq(resourceLabels.resourceId, resources.resourceId) + ) + .leftJoin(labels, eq(labels.labelId, resourceLabels.labelId)); + } + + return query.groupBy( + resources.resourceId, + resourcePassword.passwordId, + resourcePincode.pincodeId, + resourceHeaderAuth.headerAuthId, + resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId + ); } export type ListResourcesResponse = PaginatedResponse<{ @@ -288,6 +304,11 @@ export async function listResources( ); } + const isLabelFeatureEnabled = await isLicensedOrSubscribed( + orgId, + tierMatrix.labels + ); + let accessibleResources: Array<{ resourceId: number }>; if (req.user) { accessibleResources = await db @@ -369,25 +390,34 @@ export async function listResources( conditions.push(inArray(resources.resourceId, resourcesWithSite)); } if (query) { - conditions.push( - or( + const queryList = [ + like( + sql`LOWER(${resources.name})`, + "%" + query.toLowerCase() + "%" + ), + like( + sql`LOWER(${resources.niceId})`, + "%" + query.toLowerCase() + "%" + ), + like( + sql`LOWER(${resources.fullDomain})`, + "%" + query.toLowerCase() + "%" + ) + ]; + + if (isLabelFeatureEnabled) { + queryList.push( like( - sql`LOWER(${resources.name})`, - "%" + query.toLowerCase() + "%" - ), - like( - sql`LOWER(${resources.niceId})`, - "%" + query.toLowerCase() + "%" - ), - like( - sql`LOWER(${resources.fullDomain})`, + sql`LOWER(${labels.name})`, "%" + query.toLowerCase() + "%" ) - ) - ); + ); + } + + conditions.push(or(...queryList)); } - const baseQuery = queryResourcesBase().where(and(...conditions)); + const baseQuery = queryResourcesBase(isLabelFeatureEnabled).where(and(...conditions)); // we need to add `as` so that drizzle filters the result as a subquery const countQuery = db.$count(baseQuery.as("filtered_resources")); @@ -407,6 +437,35 @@ export async function listResources( ]); const resourceIdList = rows.map((row) => row.resourceId); + + let labelsForResources: Array<{ + labelId: number; + name: string; + color: string; + resourceId: number; + }> = []; + + if (isLabelFeatureEnabled) { + labelsForResources = + resourceIdList.length === 0 + ? [] + : await db + .select({ + labelId: labels.labelId, + name: labels.name, + color: labels.color, + resourceId: resourceLabels.resourceId + }) + .from(labels) + .innerJoin( + resourceLabels, + eq(resourceLabels.labelId, labels.labelId) + ) + .where( + inArray(resourceLabels.resourceId, resourceIdList) + ); + } + const allResourceTargets = resourceIdList.length === 0 ? [] @@ -458,7 +517,10 @@ export async function listResources( headerAuthId: row.headerAuthId, health: row.health ?? null, targets: [], - sites: [] + sites: [], + labels: labelsForResources.filter( + (l) => l.resourceId === row.resourceId + ) }; map.set(row.resourceId, entry); } diff --git a/src/app/[orgId]/settings/resources/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/page.tsx index b94c4daf5..8d79947b7 100644 --- a/src/app/[orgId]/settings/resources/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/page.tsx @@ -111,6 +111,7 @@ export default async function ProxyResourcesPage( protocol: resource.protocol, proxyPort: resource.proxyPort, http: resource.http, + labels: resource.labels, authState: !resource.http ? "none" : resource.sso || From ab494521b1a2bf05b3c6b2b1acdab8535cbbae8d Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 11 May 2026 18:37:16 +0200 Subject: [PATCH 125/771] =?UTF-8?q?=E2=9C=A8=20labels=20on=20proxy=20resou?= =?UTF-8?q?rces?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/resource/listResources.ts | 7 +- server/routers/site/listSites.ts | 3 +- src/components/ProxyResourcesTable.tsx | 112 ++++++++++++++++++++++- 3 files changed, 114 insertions(+), 8 deletions(-) diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 6756d8657..c6d7d036d 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -417,7 +417,9 @@ export async function listResources( conditions.push(or(...queryList)); } - const baseQuery = queryResourcesBase(isLabelFeatureEnabled).where(and(...conditions)); + const baseQuery = queryResourcesBase(isLabelFeatureEnabled).where( + and(...conditions) + ); // we need to add `as` so that drizzle filters the result as a subquery const countQuery = db.$count(baseQuery.as("filtered_resources")); @@ -463,7 +465,8 @@ export async function listResources( ) .where( inArray(resourceLabels.resourceId, resourceIdList) - ); + ) + .orderBy(asc(resourceLabels.resourceLabelId)); } const allResourceTargets = diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 829379412..99f931bb9 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -423,7 +423,8 @@ export async function listSites( siteLabels, eq(siteLabels.labelId, labels.labelId) ) - .where(inArray(siteLabels.siteId, siteIds)); + .where(inArray(siteLabels.siteId, siteIds)) + .orderBy(asc(siteLabels.siteLabelId)); } const sitesWithUpdates: SiteWithUpdateAvailable[] = rows.map((site) => { diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index 21a770a68..164171a70 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -47,6 +47,7 @@ import { Clock, Funnel, MoreHorizontal, + PlusIcon, ShieldCheck, ShieldOff, XCircle @@ -55,6 +56,7 @@ import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { + startTransition, useEffect, useMemo, useOptimistic, @@ -68,6 +70,8 @@ import z from "zod"; import { ColumnFilterButton } from "./ColumnFilterButton"; import { ControlledDataTable } from "./ui/controlled-data-table"; import UptimeMiniBar from "./UptimeMiniBar"; +import { LabelsSelector, type SelectedLabel } from "./labels-selector"; +import { LabelBadge } from "./label-badge"; export type TargetHealth = { targetId: number; @@ -538,11 +542,10 @@ export default function ProxyResourcesTable({ ), cell: ({ row }: { row: { original: ResourceRow } }) => { return ( - // - <> + ); } } @@ -707,6 +710,105 @@ export default function ProxyResourcesTable({ ); } +type ResourceLabelCellProps = { + resource: ResourceRow; + orgId: string; +}; + +function ResourceLabelCell({ resource, orgId }: ResourceLabelCellProps) { + const t = useTranslations(); + + const api = createApiClient(useEnvContext()); + + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + const router = useRouter(); + + const labels = resource.labels ?? []; + const [optimisticLabels, setOptimisticLabels] = useOptimistic(labels); + + function toggleSiteLabel( + label: SelectedLabel, + action: "attach" | "detach" + ) { + startTransition(async () => { + try { + if (action === "attach") { + setOptimisticLabels([...optimisticLabels, label]); + + await api.put( + `/org/${orgId}/label/${label.labelId}/attach`, + { resourceId: resource.id } + ); + } else { + setOptimisticLabels( + optimisticLabels.filter( + (lb) => lb.labelId !== label.labelId + ) + ); + await api.put( + `/org/${orgId}/label/${label.labelId}/detach`, + { resourceId: resource.id } + ); + } + } catch (e) { + toast({ + title: t("error"), + description: formatAxiosError(e, t("errorOccurred")), + variant: "destructive" + }); + } finally { + router.refresh(); + } + }); + } + + return ( +
+ {optimisticLabels.slice(0, 3).map((label) => ( + setIsPopoverOpen(true)} + {...label} + /> + ))} + {optimisticLabels.length > 3 && ( + + )} + + + + + + + + +
+ ); +} + function TargetStatusCell({ targets, healthStatus From 3855486a00fdf61966a0f7a232a7e49945681777 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 11 May 2026 19:27:00 +0200 Subject: [PATCH 126/771] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20prevent=20`Sitetab?= =?UTF-8?q?leCell`=20from=20rerendering=20unnecessarily?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/SitesTable.tsx | 43 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index b8064c933..dfef6d6a1 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -45,6 +45,7 @@ import { usePathname, useRouter } from "next/navigation"; import { startTransition, useEffect, + useMemo, useOptimistic, useState, useTransition @@ -180,7 +181,8 @@ export default function SitesTable({ }); } - const columns: ExtendedColumnDef[] = [ + const columns = useMemo[]>(() => { + const cols: ExtendedColumnDef[] = [ { accessorKey: "name", enableHiding: false, @@ -470,26 +472,6 @@ export default function SitesTable({ ); } }, - ...(isLabelFeatureEnabled - ? [ - { - accessorKey: "labels", - header: () => ( - - {t("labels")} - - ), - cell: ({ row }: { row: { original: SiteRow } }) => { - return ( - - ); - } - } - ] - : []), { id: "actions", enableHiding: false, @@ -544,7 +526,24 @@ export default function SitesTable({ ); } } - ]; + ]; + + if (isLabelFeatureEnabled) { + cols.splice(cols.length - 1, 0, { + accessorKey: "labels", + header: () => ( + + {t("labels")} + + ), + cell: ({ row }: { row: { original: SiteRow } }) => ( + + ) + }); + } + + return cols; + }, [isLabelFeatureEnabled, orgId, t, searchParams]); function toggleSort(column: string) { const newSearch = getNextSortOrder(column, searchParams); From d321d7275c426ebecd9b72c297c87d32c4b41656 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 11 May 2026 21:06:20 +0200 Subject: [PATCH 127/771] =?UTF-8?q?=F0=9F=9A=A7=20=20tried=20to=20memo=20p?= =?UTF-8?q?roxy=20resource=20table,=20failed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ProxyResourcesTable.tsx | 763 +++++++++++++------------ 1 file changed, 399 insertions(+), 364 deletions(-) diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index 164171a70..a8934b041 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -118,6 +118,11 @@ type ProxyResourcesTableProps = { initialFilterSite?: Selectedsite | null; }; +const booleanSearchFilterSchema = z + .enum(["true", "false"]) + .optional() + .catch(undefined); + export default function ProxyResourcesTable({ resources, orgId, @@ -224,389 +229,429 @@ export default function ProxyResourcesTable({ } } - const proxyColumns: ExtendedColumnDef[] = [ - { - accessorKey: "name", - enableHiding: false, - friendlyName: t("name"), - header: () => { - const nameOrder = getSortDirection("name", searchParams); - const Icon = - nameOrder === "asc" - ? ArrowDown01Icon - : nameOrder === "desc" - ? ArrowUp10Icon - : ChevronsUpDownIcon; + const clearSiteFilter = () => { + handleFilterChange("siteId", undefined); + setSiteFilterOpen(false); + }; - return ( - - ); - } - }, - { - id: "niceId", - accessorKey: "nice", - friendlyName: t("identifier"), - enableHiding: true, - header: () => {t("identifier")}, - cell: ({ row }) => { - return {row.original.nice || "-"}; - } - }, - { - id: "sites", - accessorFn: (row) => row.sites.map((s) => s.siteName).join(", "), - friendlyName: t("sites"), - header: () => ( - - + const onPickSite = (site: Selectedsite) => { + handleFilterChange("siteId", String(site.siteId)); + setSiteFilterOpen(false); + }; + + const siteFilterOpenRef = useRef(siteFilterOpen); + siteFilterOpenRef.current = siteFilterOpen; + + const selectedSiteRef = useRef(selectedSite); + selectedSiteRef.current = selectedSite; + + const clearSiteFilterRef = useRef(clearSiteFilter); + clearSiteFilterRef.current = clearSiteFilter; + + const onPickSiteRef = useRef(onPickSite); + onPickSiteRef.current = onPickSite; + + const proxyColumns = useMemo[]>(() => { + const cols: ExtendedColumnDef[] = [ + { + accessorKey: "name", + enableHiding: false, + friendlyName: t("name"), + header: () => { + const nameOrder = getSortDirection("name", searchParams); + const Icon = + nameOrder === "asc" + ? ArrowDown01Icon + : nameOrder === "desc" + ? ArrowUp10Icon + : ChevronsUpDownIcon; + + return ( - - {t("identifier")}, + cell: ({ row }) => { + return {row.original.nice || "-"}; + } + }, + { + id: "sites", + accessorFn: (row) => + row.sites.map((s) => s.siteName).join(", "), + friendlyName: t("sites"), + header: () => ( + -
+ -
- -
-
- ), - cell: ({ row }) => ( - - ) - }, - { - accessorKey: "protocol", - friendlyName: t("protocol"), - enableHiding: true, - header: () => {t("protocol")}, - cell: ({ row }) => { - const resourceRow = row.original; - return ( - - {resourceRow.http - ? resourceRow.ssl - ? "HTTPS" - : "HTTP" - : resourceRow.protocol.toUpperCase()} - - ); - } - }, - { - id: "status", - accessorKey: "status", - friendlyName: t("health"), - header: () => ( - - handleFilterChange("healthStatus", value) - } - searchPlaceholder={t("searchPlaceholder")} - emptyMessage={t("emptySearchOptions")} - label={t("health")} - className="p-3" - /> - ), - cell: ({ row }) => { - const resourceRow = row.original; - return ( - + +
+ +
+ + onPickSiteRef.current(site) + } + /> +
+ + ), + cell: ({ row }) => ( + - ); + ) }, - sortingFn: (rowA, rowB) => { - const statusA = rowA.original.health; - const statusB = rowB.original.health; - if (!statusA && !statusB) return 0; - if (!statusA) return 1; - if (!statusB) return -1; - const statusOrder = { - healthy: 3, - degraded: 2, - unhealthy: 1, - unknown: 0 - }; - return statusOrder[statusA] - statusOrder[statusB]; - } - }, - { - id: "statusHistory", - friendlyName: t("uptime30d"), - header: () => {t("uptime30d")}, - cell: ({ row }) => { - const resourceRow = row.original; - return ; - } - }, - { - accessorKey: "domain", - friendlyName: t("access"), - header: () => {t("access")}, - cell: ({ row }) => { - const resourceRow = row.original; - - if (!resourceRow.http) { + { + accessorKey: "protocol", + friendlyName: t("protocol"), + enableHiding: true, + header: () => {t("protocol")}, + cell: ({ row }) => { + const resourceRow = row.original; return ( -
- -
+ + {resourceRow.http + ? resourceRow.ssl + ? "HTTPS" + : "HTTP" + : resourceRow.protocol.toUpperCase()} + ); } - - if (!resourceRow.domainId) { + }, + { + id: "status", + accessorKey: "status", + friendlyName: t("health"), + header: () => ( + + handleFilterChange("healthStatus", value) + } + searchPlaceholder={t("searchPlaceholder")} + emptyMessage={t("emptySearchOptions")} + label={t("health")} + className="p-3" + /> + ), + cell: ({ row }) => { + const resourceRow = row.original; return ( -
- -
+ + ); + }, + sortingFn: (rowA, rowB) => { + const statusA = rowA.original.health; + const statusB = rowB.original.health; + if (!statusA && !statusB) return 0; + if (!statusA) return 1; + if (!statusB) return -1; + const statusOrder = { + healthy: 3, + degraded: 2, + unhealthy: 1, + unknown: 0 + }; + return statusOrder[statusA] - statusOrder[statusB]; + } + }, + { + id: "statusHistory", + friendlyName: t("uptime30d"), + header: () => {t("uptime30d")}, + cell: ({ row }) => { + const resourceRow = row.original; + return ( + ); } + }, + { + accessorKey: "domain", + friendlyName: t("access"), + header: () => {t("access")}, + cell: ({ row }) => { + const resourceRow = row.original; - const domainId = resourceRow.domainId; - const certHostname = resourceRow.fullDomain; - const showHttpsCertIndicator = - build !== "oss" && - resourceRow.ssl && - certHostname != null && - certHostname !== ""; - - return ( -
- {showHttpsCertIndicator ? ( - - ) : null} -
- {!resourceRow.wildcard ? ( + if (!resourceRow.http) { + return ( +
+
+ ); + } + + if (!resourceRow.domainId) { + return ( +
+ +
+ ); + } + + const domainId = resourceRow.domainId; + const certHostname = resourceRow.fullDomain; + const showHttpsCertIndicator = + build !== "oss" && + resourceRow.ssl && + certHostname != null && + certHostname !== ""; + + return ( +
+ {showHttpsCertIndicator ? ( + + ) : null} +
+ {!resourceRow.wildcard ? ( + + ) : ( + {resourceRow.domain} + )} +
+
+ ); + } + }, + { + accessorKey: "authState", + friendlyName: t("authentication"), + header: () => ( + + handleFilterChange("authState", value) + } + searchPlaceholder={t("searchPlaceholder")} + emptyMessage={t("emptySearchOptions")} + label={t("authentication")} + className="p-3" + /> + ), + cell: ({ row }) => { + const resourceRow = row.original; + return ( +
+ {resourceRow.authState === "protected" ? ( + + + {t("protected")} + + ) : resourceRow.authState === "not_protected" ? ( + + + {t("notProtected")} + ) : ( - {resourceRow.domain} + - )}
-
- ); - } - }, - { - accessorKey: "authState", - friendlyName: t("authentication"), - header: () => ( - - handleFilterChange("authState", value) - } - searchPlaceholder={t("searchPlaceholder")} - emptyMessage={t("emptySearchOptions")} - label={t("authentication")} - className="p-3" - /> - ), - cell: ({ row }) => { - const resourceRow = row.original; - return ( -
- {resourceRow.authState === "protected" ? ( - - - {t("protected")} - - ) : resourceRow.authState === "not_protected" ? ( - - - {t("notProtected")} - - ) : ( - - + ); + } + }, + { + accessorKey: "enabled", + friendlyName: t("enabled"), + header: () => ( + - ); - } - }, - { - accessorKey: "enabled", - friendlyName: t("enabled"), - header: () => ( - - handleFilterChange("enabled", value) - } - searchPlaceholder={t("searchPlaceholder")} - emptyMessage={t("emptySearchOptions")} - label={t("enabled")} - className="p-3" - /> - ), - cell: ({ row }) => ( - - ) - }, - ...(isLabelFeatureEnabled - ? [ - { - id: "labels", - accessorKey: "labels", - header: () => ( - - {t("labels")} - - ), - cell: ({ row }: { row: { original: ResourceRow } }) => { - return ( - - ); - } - } - ] - : []), - { - id: "actions", - enableHiding: false, - header: () => , - cell: ({ row }) => { - const resourceRow = row.original; - return ( -
- - - - - - - - {t("viewSettings")} + onValueChange={(value) => + handleFilterChange("enabled", value) + } + searchPlaceholder={t("searchPlaceholder")} + emptyMessage={t("emptySearchOptions")} + label={t("enabled")} + className="p-3" + /> + ), + cell: ({ row }) => ( + + ) + }, + { + id: "actions", + enableHiding: false, + header: () => , + cell: ({ row }) => { + const resourceRow = row.original; + return ( +
+ + + + + + + + {t("viewSettings")} + + + { + setSelectedResource(resourceRow); + setIsDeleteModalOpen(true); + }} + > + + {t("delete")} + - - { - setSelectedResource(resourceRow); - setIsDeleteModalOpen(true); - }} - > - - {t("delete")} - - - - - - - -
- ); +
+
+ + + +
+ ); + } } - } - ]; + ]; - const booleanSearchFilterSchema = z - .enum(["true", "false"]) - .optional() - .catch(undefined); + if (isLabelFeatureEnabled) { + cols.splice(cols.length - 1, 0, { + id: "labels", + accessorKey: "labels", + header: () => ( + + {t("labels")} + + ), + cell: ({ row }: { row: { original: ResourceRow } }) => ( + + ) + }); + } + + return cols; + }, [isLabelFeatureEnabled, orgId, t, searchParams]); function handleFilterChange( column: string, @@ -623,16 +668,6 @@ export default function ProxyResourcesTable({ }); } - const clearSiteFilter = () => { - handleFilterChange("siteId", undefined); - setSiteFilterOpen(false); - }; - - const onPickSite = (site: Selectedsite) => { - handleFilterChange("siteId", String(site.siteId)); - setSiteFilterOpen(false); - }; - function toggleSort(column: string) { const newSearch = getNextSortOrder(column, searchParams); From 931ba0f5401ce5f3bd6b7bcac36e5301eaeebe83 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 12 May 2026 17:46:46 +0200 Subject: [PATCH 128/771] =?UTF-8?q?=F0=9F=92=84=20`px-2`=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ProxyResourcesTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index a8934b041..51c3254b6 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -856,7 +856,7 @@ function TargetStatusCell({ if (!targets || targets.length === 0) { return ( -
+
{t("resourcesTableNoTargets")}
@@ -876,7 +876,7 @@ function TargetStatusCell({ - ); - } - }, - { - id: "niceId", - accessorKey: "niceId", - friendlyName: t("identifier"), - enableHiding: true, - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - return {row.original.niceId || "-"}; - } - }, - { - id: "sites", - accessorFn: (row) => row.sites.map((s) => s.siteName).join(", "), - friendlyName: t("sites"), - header: () => ( - - + return ( - - { + return ( + + ); + }, + cell: ({ row }) => { + return {row.original.niceId || "-"}; + } + }, + { + id: "sites", + accessorFn: (row) => + row.sites.map((s) => s.siteName).join(", "), + friendlyName: t("sites"), + header: () => ( + -
+ -
- + +
+ +
+ +
+
+ ), + cell: ({ row }) => { + const resourceRow = row.original; + return ( + -
-
- ), - cell: ({ row }) => { - const resourceRow = row.original; - return ( - - ); - } - }, - { - accessorKey: "mode", - friendlyName: t("editInternalResourceDialogMode"), - header: () => ( - ( + + handleFilterChange("mode", value) } - ]} - selectedValue={searchParams.get("mode") ?? undefined} - onValueChange={(value) => handleFilterChange("mode", value)} - searchPlaceholder={t("searchPlaceholder")} - emptyMessage={t("emptySearchOptions")} - label={t("editInternalResourceDialogMode")} - className="p-3" - /> - ), - cell: ({ row }) => { - const resourceRow = row.original; - const modeLabels: Record< - "host" | "cidr" | "port" | "http", - string - > = { - host: t("editInternalResourceDialogModeHost"), - cidr: t("editInternalResourceDialogModeCidr"), - port: t("editInternalResourceDialogModePort"), - http: t("editInternalResourceDialogModeHttp") - }; - return {modeLabels[resourceRow.mode]}; - } - }, - { - accessorKey: "destination", - friendlyName: t("resourcesTableDestination"), - header: () => ( - {t("resourcesTableDestination")} - ), - cell: ({ row }) => { - const resourceRow = row.original; - const display = formatDestinationDisplay(resourceRow); - return ( - - ); - } - }, - { - accessorKey: "alias", - friendlyName: t("resourcesTableAlias"), - header: () => ( - {t("resourcesTableAlias")} - ), - cell: ({ row }) => { - const resourceRow = row.original; - if (resourceRow.mode === "host" && resourceRow.alias) { + ), + cell: ({ row }) => { + const resourceRow = row.original; + const modeLabels: Record< + "host" | "cidr" | "port" | "http", + string + > = { + host: t("editInternalResourceDialogModeHost"), + cidr: t("editInternalResourceDialogModeCidr"), + port: t("editInternalResourceDialogModePort"), + http: t("editInternalResourceDialogModeHttp") + }; + return {modeLabels[resourceRow.mode]}; + } + }, + { + accessorKey: "destination", + friendlyName: t("resourcesTableDestination"), + header: () => ( + + {t("resourcesTableDestination")} + + ), + cell: ({ row }) => { + const resourceRow = row.original; + const display = formatDestinationDisplay(resourceRow); return ( ); } - if (resourceRow.mode === "http") { - const domainId = resourceRow.domainId; - const fullDomain = resourceRow.fullDomain; - const url = `${resourceRow.ssl ? "https" : "http"}://${fullDomain}`; - const did = - build !== "oss" && - resourceRow.ssl && - domainId != null && - domainId !== "" && - fullDomain != null && - fullDomain !== ""; + }, + { + accessorKey: "alias", + friendlyName: t("resourcesTableAlias"), + header: () => ( + {t("resourcesTableAlias")} + ), + cell: ({ row }) => { + const resourceRow = row.original; + if (resourceRow.mode === "host" && resourceRow.alias) { + return ( + + ); + } + if (resourceRow.mode === "http") { + const domainId = resourceRow.domainId; + const fullDomain = resourceRow.fullDomain; + const url = `${resourceRow.ssl ? "https" : "http"}://${fullDomain}`; + const did = + build !== "oss" && + resourceRow.ssl && + domainId != null && + domainId !== "" && + fullDomain != null && + fullDomain !== ""; - return ( -
- {did ? ( - - ) : null} -
- + return ( +
+ {did ? ( + + ) : null} +
+ +
+ ); + } + return -; + } + }, + { + accessorKey: "aliasAddress", + friendlyName: t("resourcesTableAliasAddress"), + enableHiding: true, + header: () => ( +
+ {t("resourcesTableAliasAddress")} + +
+ ), + cell: ({ row }) => { + const resourceRow = row.original; + return resourceRow.aliasAddress ? ( + + ) : ( + - + ); + } + }, + { + id: "actions", + enableHiding: false, + header: () => , + cell: ({ row }) => { + const resourceRow = row.original; + return ( +
+ + + + + + { + setSelectedInternalResource( + resourceRow + ); + setIsDeleteModalOpen(true); + }} + > + + {t("delete")} + + + + +
); } - return -; } - }, - { - accessorKey: "aliasAddress", - friendlyName: t("resourcesTableAliasAddress"), - enableHiding: true, - header: () => ( -
- {t("resourcesTableAliasAddress")} - -
- ), - cell: ({ row }) => { - const resourceRow = row.original; - return resourceRow.aliasAddress ? ( - ( + + {t("labels")} + + ), + cell: ({ row }: { row: { original: InternalResourceRow } }) => ( + - ) : ( - - - ); - } - }, - { - id: "actions", - enableHiding: false, - header: () => , - cell: ({ row }) => { - const resourceRow = row.original; - return ( -
- - - - - - { - setSelectedInternalResource( - resourceRow - ); - setIsDeleteModalOpen(true); - }} - > - - {t("delete")} - - - - - -
- ); - } + ) + }); } - ]; + + return cols; + }, [isLabelFeatureEnabled, orgId, t, searchParams]); function handleFilterChange( column: string, @@ -638,7 +694,8 @@ export default function ClientResourcesTable({ enableColumnVisibility columnVisibility={{ niceId: false, - aliasAddress: false + aliasAddress: false, + labels: false }} stickyLeftColumn="name" stickyRightColumn="actions" @@ -674,3 +731,101 @@ export default function ClientResourcesTable({ ); } + +type ClientResourceLabelCellProps = { + resource: InternalResourceRow; + orgId: string; +}; + +function ClientResourceLabelCell({ + resource, + orgId +}: ClientResourceLabelCellProps) { + const t = useTranslations(); + const api = createApiClient(useEnvContext()); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const router = useRouter(); + + const labels = resource.labels ?? []; + const [optimisticLabels, setOptimisticLabels] = useOptimistic(labels); + + function toggleResourceLabel( + label: SelectedLabel, + action: "attach" | "detach" + ) { + startTransition(async () => { + try { + if (action === "attach") { + setOptimisticLabels([...optimisticLabels, label]); + await api.put( + `/org/${orgId}/label/${label.labelId}/attach`, + { siteResourceId: resource.id } + ); + } else { + setOptimisticLabels( + optimisticLabels.filter( + (lb) => lb.labelId !== label.labelId + ) + ); + await api.put( + `/org/${orgId}/label/${label.labelId}/detach`, + { siteResourceId: resource.id } + ); + } + } catch (e) { + toast({ + title: t("error"), + description: formatAxiosError(e, t("errorOccurred")), + variant: "destructive" + }); + } finally { + router.refresh(); + } + }); + } + + return ( +
+ {optimisticLabels.slice(0, 3).map((label) => ( + setIsPopoverOpen(true)} + {...label} + /> + ))} + {optimisticLabels.length > 3 && ( + + )} + + + + + + + + +
+ ); +} From 7120ab4b228b8cc8b73e984413fbfb9e6df30b9b Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 12 May 2026 20:45:12 +0200 Subject: [PATCH 134/771] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20filter=20sites=20&?= =?UTF-8?q?=20resources=20by=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/routers/resource/listResources.ts | 61 ++++++++++-------------- server/routers/site/listSites.ts | 44 +++++++---------- 2 files changed, 41 insertions(+), 64 deletions(-) diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index c6d7d036d..49b7f2b57 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -162,8 +162,8 @@ export type ResourceWithTargets = { labels?: Array>; }; -function queryResourcesBase(isLabelFeatureEnabled: boolean) { - let query = db +function queryResourcesBase() { + return db .select({ resourceId: resources.resourceId, name: resources.name, @@ -209,24 +209,14 @@ function queryResourcesBase(isLabelFeatureEnabled: boolean) { .leftJoin( targetHealthCheck, eq(targetHealthCheck.targetId, targets.targetId) + ) + .groupBy( + resources.resourceId, + resourcePassword.passwordId, + resourcePincode.pincodeId, + resourceHeaderAuth.headerAuthId, + resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId ); - - if (isLabelFeatureEnabled) { - query = query - .leftJoin( - resourceLabels, - eq(resourceLabels.resourceId, resources.resourceId) - ) - .leftJoin(labels, eq(labels.labelId, resourceLabels.labelId)); - } - - return query.groupBy( - resources.resourceId, - resourcePassword.passwordId, - resourcePincode.pincodeId, - resourceHeaderAuth.headerAuthId, - resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId - ); } export type ListResourcesResponse = PaginatedResponse<{ @@ -390,26 +380,25 @@ export async function listResources( conditions.push(inArray(resources.resourceId, resourcesWithSite)); } if (query) { + const q = "%" + query.toLowerCase() + "%"; const queryList = [ - like( - sql`LOWER(${resources.name})`, - "%" + query.toLowerCase() + "%" - ), - like( - sql`LOWER(${resources.niceId})`, - "%" + query.toLowerCase() + "%" - ), - like( - sql`LOWER(${resources.fullDomain})`, - "%" + query.toLowerCase() + "%" - ) + like(sql`LOWER(${resources.name})`, q), + like(sql`LOWER(${resources.niceId})`, q), + like(sql`LOWER(${resources.fullDomain})`, q) ]; if (isLabelFeatureEnabled) { queryList.push( - like( - sql`LOWER(${labels.name})`, - "%" + query.toLowerCase() + "%" + inArray( + resources.resourceId, + db + .select({ id: resourceLabels.resourceId }) + .from(resourceLabels) + .innerJoin( + labels, + eq(labels.labelId, resourceLabels.labelId) + ) + .where(like(sql`LOWER(${labels.name})`, q)) ) ); } @@ -417,9 +406,7 @@ export async function listResources( conditions.push(or(...queryList)); } - const baseQuery = queryResourcesBase(isLabelFeatureEnabled).where( - and(...conditions) - ); + const baseQuery = queryResourcesBase().where(and(...conditions)); // we need to add `as` so that drizzle filters the result as a subquery const countQuery = db.$count(baseQuery.as("filtered_resources")); diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 99f931bb9..af6514f02 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -190,8 +190,8 @@ const listSitesSchema = z.object({ }) }); -function querySitesBase(isLabelFeatureEnabled: boolean) { - let query = db +function querySitesBase() { + return db .selectDistinct({ siteId: sites.siteId, niceId: sites.niceId, @@ -231,14 +231,6 @@ function querySitesBase(isLabelFeatureEnabled: boolean) { remoteExitNodes, eq(remoteExitNodes.exitNodeId, sites.exitNodeId) ); - - if (isLabelFeatureEnabled) { - query = query - .leftJoin(siteLabels, eq(siteLabels.siteId, sites.siteId)) - .leftJoin(labels, eq(labels.labelId, siteLabels.labelId)); - } - - return query; } type SiteRowBase = Awaited>[0]; @@ -346,37 +338,35 @@ export async function listSites( conditions.push(eq(sites.status, status)); } if (query) { + const q = "%" + query.toLowerCase() + "%"; const queryList = [ - like( - sql`LOWER(${sites.name})`, - "%" + query.toLowerCase() + "%" - ), - like( - sql`LOWER(${sites.niceId})`, - "%" + query.toLowerCase() + "%" - ) + like(sql`LOWER(${sites.name})`, q), + like(sql`LOWER(${sites.niceId})`, q) ]; if (isLabelFeatureEnabled) { queryList.push( - like( - sql`LOWER(${labels.name})`, - "%" + query.toLowerCase() + "%" + inArray( + sites.siteId, + db + .select({ id: siteLabels.siteId }) + .from(siteLabels) + .innerJoin( + labels, + eq(labels.labelId, siteLabels.labelId) + ) + .where(like(sql`LOWER(${labels.name})`, q)) ) ); } conditions.push(or(...queryList)); } - const baseQuery = querySitesBase(isLabelFeatureEnabled).where( - and(...conditions) - ); + const baseQuery = querySitesBase().where(and(...conditions)); // we need to add `as` so that drizzle filters the result as a subquery const countQuery = db.$count( - querySitesBase(isLabelFeatureEnabled) - .where(and(...conditions)) - .as("filtered_sites") + querySitesBase().where(and(...conditions)).as("filtered_sites") ); const siteListQuery = baseQuery From ce746a2a218a4747847489908ade4cf8bc26041c Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 12 May 2026 22:32:56 +0200 Subject: [PATCH 135/771] =?UTF-8?q?=E2=9C=A8=20Handle=20labels=20for=20mac?= =?UTF-8?q?hine=20clients?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 18 +++ server/db/sqlite/schema/schema.ts | 20 +++ .../routers/labels/attachLabelToItem.ts | 43 ++++- .../routers/labels/detachLabelFromItem.ts | 43 ++++- server/routers/client/listClients.ts | 75 +++++++-- .../[orgId]/settings/clients/machine/page.tsx | 3 +- src/components/MachineClientsTable.tsx | 148 +++++++++++++++++- 7 files changed, 320 insertions(+), 30 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 76c842d13..58e78735c 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -227,6 +227,24 @@ export const siteResourceLabels = pgTable( (t) => [unique("site_resource_label_uniq").on(t.siteResourceId, t.labelId)] ); +export const clientLabels = pgTable( + "clientLabels", + { + clientLabelId: serial("clientLabelId").primaryKey(), + clientId: integer("clientId") + .references(() => clients.clientId, { + onDelete: "cascade" + }) + .notNull(), + labelId: integer("labelId") + .references(() => labels.labelId, { + onDelete: "cascade" + }) + .notNull() + }, + (t) => [unique("client_label_uniq").on(t.clientId, t.labelId)] +); + export const targets = pgTable("targets", { targetId: serial("targetId").primaryKey(), resourceId: integer("resourceId") diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 2acbe0f2a..e3e83d222 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -252,6 +252,26 @@ export const siteResourceLabels = sqliteTable( (t) => [unique("site_resource_label_uniq").on(t.siteResourceId, t.labelId)] ); +export const clientLabels = sqliteTable( + "clientLabels", + { + clientLabelId: integer("clientLabelId").primaryKey({ + autoIncrement: true + }), + clientId: integer("clientId") + .references(() => clients.clientId, { + onDelete: "cascade" + }) + .notNull(), + labelId: integer("labelId") + .references(() => labels.labelId, { + onDelete: "cascade" + }) + .notNull() + }, + (t) => [unique("client_label_uniq").on(t.clientId, t.labelId)] +); + export const targets = sqliteTable("targets", { targetId: integer("targetId").primaryKey({ autoIncrement: true }), resourceId: integer("resourceId") diff --git a/server/private/routers/labels/attachLabelToItem.ts b/server/private/routers/labels/attachLabelToItem.ts index f98e006be..d011a606d 100644 --- a/server/private/routers/labels/attachLabelToItem.ts +++ b/server/private/routers/labels/attachLabelToItem.ts @@ -12,6 +12,8 @@ */ import { + clients, + clientLabels, db, labels, resourceLabels, @@ -24,7 +26,7 @@ import { import response from "@server/lib/response"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; -import { and, eq } from "drizzle-orm"; +import { and, eq, isNull } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; @@ -38,7 +40,8 @@ const paramsSchema = z.strictObject({ const attachLabelBodySchema = z.strictObject({ siteId: z.number().int().optional(), resourceId: z.number().int().optional(), - siteResourceId: z.number().int().optional() + siteResourceId: z.number().int().optional(), + clientId: z.number().int().optional() }); export async function attachLabelToItem( @@ -69,13 +72,14 @@ export async function attachLabelToItem( ); } - const { siteId, resourceId, siteResourceId } = parsedBody.data; + const { siteId, resourceId, siteResourceId, clientId } = + parsedBody.data; - if (!siteId && !resourceId && !siteResourceId) { + if (!siteId && !resourceId && !siteResourceId && !clientId) { return next( createHttpError( HttpCode.BAD_REQUEST, - "At least one of `siteId`, `resourceId` or `siteResourceId` should be provided." + "At least one of `siteId`, `resourceId`, `siteResourceId` or `clientId` should be provided." ) ); } @@ -175,6 +179,35 @@ export async function attachLabelToItem( .onConflictDoNothing(); } + if (clientId) { + const clientCount = await db.$count( + clients, + and( + eq(clients.clientId, clientId), + eq(clients.orgId, orgId), + isNull(clients.userId) + ) + ); + + if (clientCount === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Client with Id ${clientId} doesn't exist.` + ) + ); + } + + // idempotent, calling this endpoint multiple times should attach the label only once + await db + .insert(clientLabels) + .values({ + labelId, + clientId + }) + .onConflictDoNothing(); + } + return response(res, { data: {}, success: true, diff --git a/server/private/routers/labels/detachLabelFromItem.ts b/server/private/routers/labels/detachLabelFromItem.ts index 081349a55..9a5545312 100644 --- a/server/private/routers/labels/detachLabelFromItem.ts +++ b/server/private/routers/labels/detachLabelFromItem.ts @@ -12,6 +12,8 @@ */ import { + clients, + clientLabels, db, labels, resourceLabels, @@ -24,7 +26,7 @@ import { import response from "@server/lib/response"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; -import { and, eq } from "drizzle-orm"; +import { and, eq, isNull } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; @@ -38,7 +40,8 @@ const paramsSchema = z.strictObject({ const detachLabelBodySchema = z.strictObject({ siteId: z.number().int().optional(), resourceId: z.number().int().optional(), - siteResourceId: z.number().int().optional() + siteResourceId: z.number().int().optional(), + clientId: z.number().int().optional() }); export async function detachLabelFromItem( @@ -69,13 +72,14 @@ export async function detachLabelFromItem( ); } - const { siteId, resourceId, siteResourceId } = parsedBody.data; + const { siteId, resourceId, siteResourceId, clientId } = + parsedBody.data; - if (!siteId && !resourceId && !siteResourceId) { + if (!siteId && !resourceId && !siteResourceId && !clientId) { return next( createHttpError( HttpCode.BAD_REQUEST, - "At least one of `siteId`, `siteResourceId` or `resourceId` should be provided." + "At least one of `siteId`, `resourceId`, `siteResourceId` or `clientId` should be provided." ) ); } @@ -175,6 +179,35 @@ export async function detachLabelFromItem( ); } + if (clientId) { + const clientCount = await db.$count( + clients, + and( + eq(clients.clientId, clientId), + eq(clients.orgId, orgId), + isNull(clients.userId) + ) + ); + + if (clientCount === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Client with Id ${clientId} doesn't exist.` + ) + ); + } + + await db + .delete(clientLabels) + .where( + and( + eq(clientLabels.labelId, labelId), + eq(clientLabels.clientId, clientId) + ) + ); + } + return response(res, { data: {}, success: true, diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index f5d69857d..220f845f4 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -1,15 +1,20 @@ import { + clientLabels, clients, clientSitesAssociationsCache, currentFingerprint, db, + labels, olms, orgs, roleClients, sites, userClients, - users + users, + type Label } from "@server/db"; +import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; import response from "@server/lib/response"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; @@ -169,6 +174,7 @@ type ClientWithSites = Awaited>[0] & { siteNiceId: string | null; }>; olmUpdateAvailable?: boolean; + labels?: Array>; }; type OlmWithUpdateAvailable = ClientWithSites; @@ -255,6 +261,11 @@ export async function listClients( (client) => client.clientId ); + const isLabelFeatureEnabled = await isLicensedOrSubscribed( + orgId, + tierMatrix.labels + ); + // Get client count with filter const conditions = [ and( @@ -288,18 +299,29 @@ export async function listClients( } if (query) { - conditions.push( - or( - like( - sql`LOWER(${clients.name})`, - "%" + query.toLowerCase() + "%" - ), - like( - sql`LOWER(${clients.niceId})`, - "%" + query.toLowerCase() + "%" + const q = "%" + query.toLowerCase() + "%"; + const queryList = [ + like(sql`LOWER(${clients.name})`, q), + like(sql`LOWER(${clients.niceId})`, q) + ]; + + if (isLabelFeatureEnabled) { + queryList.push( + inArray( + clients.clientId, + db + .select({ id: clientLabels.clientId }) + .from(clientLabels) + .innerJoin( + labels, + eq(labels.labelId, clientLabels.labelId) + ) + .where(like(sql`LOWER(${labels.name})`, q)) ) - ) - ); + ); + } + + conditions.push(or(...queryList)); } const baseQuery = queryClientsBase().where(and(...conditions)); @@ -326,6 +348,30 @@ export async function listClients( const clientIds = clientsList.map((client) => client.clientId); const siteAssociations = await getSiteAssociations(clientIds); + let labelsForClients: Array<{ + labelId: number; + name: string; + color: string; + clientId: number; + }> = []; + + if (isLabelFeatureEnabled && clientIds.length > 0) { + labelsForClients = await db + .select({ + labelId: labels.labelId, + name: labels.name, + color: labels.color, + clientId: clientLabels.clientId + }) + .from(labels) + .innerJoin( + clientLabels, + eq(clientLabels.labelId, labels.labelId) + ) + .where(inArray(clientLabels.clientId, clientIds)) + .orderBy(asc(clientLabels.clientLabelId)); + } + // Group site associations by client ID const sitesByClient = siteAssociations.reduce( (acc, association) => { @@ -353,7 +399,10 @@ export async function listClients( const clientsWithSites = clientsList.map((client) => { return { ...client, - sites: sitesByClient[client.clientId] || [] + sites: sitesByClient[client.clientId] || [], + labels: labelsForClients.filter( + (l) => l.clientId === client.clientId + ) }; }); diff --git a/src/app/[orgId]/settings/clients/machine/page.tsx b/src/app/[orgId]/settings/clients/machine/page.tsx index fe9281ac7..066fdc3ea 100644 --- a/src/app/[orgId]/settings/clients/machine/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/page.tsx @@ -76,7 +76,8 @@ export default async function ClientsPage(props: ClientsPageProps) { agent: client.agent, archived: client.archived || false, blocked: client.blocked || false, - approvalState: client.approvalState ?? "approved" + approvalState: client.approvalState ?? "approved", + labels: client.labels ?? [] }; }; diff --git a/src/components/MachineClientsTable.tsx b/src/components/MachineClientsTable.tsx index 4ef22c83d..61125baad 100644 --- a/src/components/MachineClientsTable.tsx +++ b/src/components/MachineClientsTable.tsx @@ -10,8 +10,11 @@ import { DropdownMenuTrigger } from "@app/components/ui/dropdown-menu"; import { useEnvContext } from "@app/hooks/useEnvContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { cn } from "@app/lib/cn"; +import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { ArrowRight, ArrowUpDown, @@ -19,12 +22,26 @@ import { CircleSlash, ArrowDown01Icon, ArrowUp10Icon, - ChevronsUpDownIcon + ChevronsUpDownIcon, + PlusIcon } from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useMemo, useState, useTransition } from "react"; +import { + startTransition, + useMemo, + useOptimistic, + useState, + useTransition +} from "react"; +import { LabelBadge } from "./label-badge"; +import { LabelsSelector, type SelectedLabel } from "./labels-selector"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "./ui/popover"; import { Badge } from "./ui/badge"; import type { PaginationState } from "@tanstack/react-table"; import { ControlledDataTable } from "./ui/controlled-data-table"; @@ -53,6 +70,11 @@ export type ClientRow = { archived?: boolean; blocked?: boolean; approvalState: "approved" | "pending" | "denied"; + labels?: Array<{ + labelId: number; + name: string; + color: string; + }>; }; type ClientTableProps = { @@ -84,17 +106,21 @@ export default function MachineClientsTable({ ); const api = createApiClient(useEnvContext()); - const [isRefreshing, startTransition] = useTransition(); + const [isRefreshing, startRefreshTransition] = useTransition(); const [isNavigatingToAddPage, startNavigation] = useTransition(); + const { isPaidUser } = usePaidStatus(); + const isLabelFeatureEnabled = isPaidUser(tierMatrix.labels); + const defaultMachineColumnVisibility = { subnet: false, userId: false, - niceId: false + niceId: false, + labels: false }; const refreshData = () => { - startTransition(() => { + startRefreshTransition(() => { try { router.refresh(); } catch (error) { @@ -384,6 +410,24 @@ export default function MachineClientsTable({ } ]; + if (isLabelFeatureEnabled) { + baseColumns.push({ + id: "labels", + accessorKey: "labels", + header: () => ( + + {t("labels")} + + ), + cell: ({ row }: { row: { original: ClientRow } }) => ( + + ) + }); + } + // Only include actions column if there are rows without userIds if (hasRowsWithoutUserId) { baseColumns.push({ @@ -464,7 +508,7 @@ export default function MachineClientsTable({ } return baseColumns; - }, [hasRowsWithoutUserId, t, getSortDirection, toggleSort]); + }, [hasRowsWithoutUserId, isLabelFeatureEnabled, orgId, t, searchParams]); const booleanSearchFilterSchema = z .enum(["true", "false"]) @@ -591,3 +635,95 @@ export default function MachineClientsTable({ ); } + +type MachineClientLabelCellProps = { + client: ClientRow; + orgId: string; +}; + +function MachineClientLabelCell({ client, orgId }: MachineClientLabelCellProps) { + const t = useTranslations(); + const api = createApiClient(useEnvContext()); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const router = useRouter(); + + const labels = client.labels ?? []; + const [optimisticLabels, setOptimisticLabels] = useOptimistic(labels); + + function toggleClientLabel(label: SelectedLabel, action: "attach" | "detach") { + startTransition(async () => { + try { + if (action === "attach") { + setOptimisticLabels([...optimisticLabels, label]); + await api.put( + `/org/${orgId}/label/${label.labelId}/attach`, + { clientId: client.id } + ); + } else { + setOptimisticLabels( + optimisticLabels.filter( + (lb) => lb.labelId !== label.labelId + ) + ); + await api.put( + `/org/${orgId}/label/${label.labelId}/detach`, + { clientId: client.id } + ); + } + } catch (e) { + toast({ + title: t("error"), + description: formatAxiosError(e, t("errorOccurred")), + variant: "destructive" + }); + } finally { + router.refresh(); + } + }); + } + + return ( +
+ {optimisticLabels.slice(0, 3).map((label) => ( + setIsPopoverOpen(true)} + {...label} + /> + ))} + {optimisticLabels.length > 3 && ( + + )} + + + + + + + + +
+ ); +} From 89cc99f915aee9fbc8346ce39b67ed8345934cb2 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 11 May 2026 15:27:06 -0700 Subject: [PATCH 136/771] Initial rdp working --- package-lock.json | 71 ++---- package.json | 2 + src/app/rdp/RdpClient.tsx | 463 ++++++++++++++++++++++++++++++++++++++ src/app/rdp/page.tsx | 11 + 4 files changed, 494 insertions(+), 53 deletions(-) create mode 100644 src/app/rdp/RdpClient.tsx create mode 100644 src/app/rdp/page.tsx diff --git a/package-lock.json b/package-lock.json index 8c241554a..463bae493 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,8 @@ "dependencies": { "@asteasolutions/zod-to-openapi": "8.4.1", "@aws-sdk/client-s3": "3.1011.0", + "@devolutions/iron-remote-desktop": "https://s3.us-east-1.amazonaws.com/static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz", + "@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.0.tgz", "@faker-js/faker": "10.3.0", "@headlessui/react": "2.2.9", "@hookform/resolvers": "5.2.2", @@ -1058,7 +1060,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1460,6 +1461,16 @@ "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", "license": "MIT" }, + "node_modules/@devolutions/iron-remote-desktop": { + "version": "0.0.0", + "resolved": "https://s3.us-east-1.amazonaws.com/static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz", + "integrity": "sha512-96z7WShjpJJhr4I2RzhXB52GcdmVFMEVvUgoQ0a20n3gATNJ+n2V3W2i8AUeMqVR38uvcyK3e+loY5T050NgQg==" + }, + "node_modules/@devolutions/iron-remote-desktop-rdp": { + "version": "0.0.0", + "resolved": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.0.tgz", + "integrity": "sha512-qkpqYOMmSU6jIdDKOWpnLL7pb0sA/Y7Yjm4wI0/VbBIRfZPH8WdKxDU5DNp8jFF60l1zbcSJeRIq5yIAvws3Hw==" + }, "node_modules/@dotenvx/dotenvx": { "version": "1.54.1", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.54.1.tgz", @@ -2354,7 +2365,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2377,7 +2387,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2400,7 +2409,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2417,7 +2425,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2434,7 +2441,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2451,7 +2457,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2468,7 +2473,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2485,7 +2489,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2502,7 +2505,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2519,7 +2521,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2536,7 +2537,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2553,7 +2553,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2576,7 +2575,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2599,7 +2597,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2622,7 +2619,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2645,7 +2641,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2668,7 +2663,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2691,7 +2685,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2714,7 +2707,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -2734,7 +2726,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2754,7 +2745,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2774,7 +2764,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3034,7 +3023,6 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -6981,7 +6969,6 @@ "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.6.tgz", "integrity": "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw==", "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" }, @@ -8442,7 +8429,6 @@ "version": "5.90.21", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", - "peer": true, "dependencies": { "@tanstack/query-core": "5.90.20" }, @@ -8558,7 +8544,6 @@ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -8906,7 +8891,6 @@ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -9002,7 +8986,6 @@ "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -9030,7 +9013,6 @@ "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -9056,7 +9038,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -9067,7 +9048,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -9154,7 +9134,8 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@types/ws": { "version": "8.18.1", @@ -9228,7 +9209,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -9702,7 +9682,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -10152,7 +10131,6 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -10224,7 +10202,6 @@ "integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -10353,7 +10330,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -11260,7 +11236,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -11701,6 +11676,7 @@ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, "engines": { "node": ">=20" }, @@ -12335,7 +12311,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -12421,7 +12396,6 @@ "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -12558,7 +12532,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -12952,7 +12925,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -15370,6 +15342,7 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", + "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -15380,6 +15353,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -15468,7 +15442,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.15.tgz", "integrity": "sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.15", "@swc/helpers": "0.5.15", @@ -16428,7 +16401,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", @@ -16936,7 +16908,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -16968,7 +16939,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -17261,7 +17231,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -18723,8 +18692,7 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.2", @@ -19199,7 +19167,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -19627,7 +19594,6 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", @@ -19834,7 +19800,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 1da507c0d..7ed8f53fa 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ "dependencies": { "@asteasolutions/zod-to-openapi": "8.4.1", "@aws-sdk/client-s3": "3.1011.0", + "@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz", + "@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.0.tgz", "@faker-js/faker": "10.3.0", "@headlessui/react": "2.2.9", "@hookform/resolvers": "5.2.2", diff --git a/src/app/rdp/RdpClient.tsx b/src/app/rdp/RdpClient.tsx new file mode 100644 index 000000000..17d12b066 --- /dev/null +++ b/src/app/rdp/RdpClient.tsx @@ -0,0 +1,463 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Checkbox } from "@/components/ui/checkbox"; +import { toast } from "@app/hooks/useToast"; +import type { + UserInteraction, + IronError +} from "@devolutions/iron-remote-desktop/dist"; + +declare module "react" { + namespace JSX { + interface IntrinsicElements { + "iron-remote-desktop": React.DetailedHTMLProps< + React.HTMLAttributes & { + scale?: string; + verbose?: string; + flexcenter?: string; + module?: unknown; + }, + HTMLElement + >; + } + } +} + +type FormState = { + username: string; + password: string; + gatewayAddress: string; + hostname: string; + domain: string; + authtoken: string; + kdcProxyUrl: string; + pcb: string; + desktopWidth: number; + desktopHeight: number; + enableClipboard: boolean; +}; + +const isIronError = (error: unknown): error is IronError => { + return ( + typeof error === "object" && + error !== null && + typeof (error as IronError).backtrace === "function" && + typeof (error as IronError).kind === "function" + ); +}; + +export default function RdpClient() { + const [form, setForm] = useState({ + username: "Administrator", + password: "Wdvwy1W*ITK-(OK.sW?nVK%?mTl30wL0", + gatewayAddress: "ws://localhost:7171/jet/rdp", + hostname: "172.31.3.58:3389", + domain: "", + authtoken: "", + kdcProxyUrl: "", + pcb: "", + desktopWidth: 1280, + desktopHeight: 720, + enableClipboard: true + }); + + const [showLogin, setShowLogin] = useState(true); + const [moduleReady, setModuleReady] = useState(false); + const [unicodeMode, setUnicodeMode] = useState(false); + const [cursorOverrideActive, setCursorOverrideActive] = useState(false); + + const userInteractionRef = useRef(null); + const backendRef = useRef(null); + const extensionsRef = useRef<{ + displayControl: (enable: boolean) => unknown; + preConnectionBlob: (pcb: string) => unknown; + kdcProxyUrl: (url: string) => unknown; + } | null>(null); + + // Load the iron-remote-desktop modules client-side and register the + // `` custom element. + useEffect(() => { + let cancelled = false; + (async () => { + const [coreMod, rdpMod] = await Promise.all([ + import("@devolutions/iron-remote-desktop/dist"), + import("@devolutions/iron-remote-desktop-rdp/dist") + ]); + if (cancelled) return; + + await rdpMod.init("INFO"); + + backendRef.current = rdpMod.Backend; + extensionsRef.current = { + displayControl: rdpMod.displayControl, + preConnectionBlob: rdpMod.preConnectionBlob, + kdcProxyUrl: rdpMod.kdcProxyUrl + }; + // Importing the package registers the custom element as a side + // effect. Touch the default export to avoid tree-shaking. + void coreMod; + + setModuleReady(true); + })().catch((err) => { + console.error("Failed to load iron-remote-desktop modules", err); + toast({ + variant: "destructive", + title: "Failed to load RDP module", + description: `${err}` + }); + }); + + return () => { + cancelled = true; + }; + }, []); + + // Attach the "ready" listener synchronously the moment the custom + // element mounts. The custom element dispatches `ready` from its own + // `onMount`, so a deferred useEffect can race and miss it. + const remoteElementRef = (el: HTMLElement | null) => { + if (!el) return; + const onReady = (e: Event) => { + const event = e as CustomEvent; + userInteractionRef.current = event.detail.irgUserInteraction; + }; + el.addEventListener("ready", onReady); + }; + + const update = (key: K, value: FormState[K]) => { + setForm((prev) => ({ ...prev, [key]: value })); + }; + + const startSession = async () => { + const userInteraction = userInteractionRef.current; + const exts = extensionsRef.current; + if (!userInteraction || !exts) { + toast({ + variant: "destructive", + title: "Not ready", + description: "RDP module is still initializing" + }); + return; + } + + if (form.authtoken === "") { + toast({ + variant: "destructive", + title: "Missing auth token", + description: + "An auth token is required to connect through the gateway" + }); + return; + } + + toast({ + title: "Connecting...", + description: "Connection in progress" + }); + + userInteraction.setEnableClipboard(form.enableClipboard); + + const builder = userInteraction + .configBuilder() + .withUsername(form.username) + .withPassword(form.password) + .withDestination(form.hostname) + .withProxyAddress(form.gatewayAddress) + .withServerDomain(form.domain) + .withAuthToken(form.authtoken) + .withDesktopSize({ + width: form.desktopWidth, + height: form.desktopHeight + }) + .withExtension(exts.displayControl(true)); + + if (form.pcb !== "") { + builder.withExtension(exts.preConnectionBlob(form.pcb)); + } + if (form.kdcProxyUrl !== "") { + builder.withExtension(exts.kdcProxyUrl(form.kdcProxyUrl)); + } + + try { + const sessionInfo = await userInteraction.connect(builder.build()); + + toast({ title: "Connected" }); + setShowLogin(false); + userInteraction.setVisibility(true); + + const termInfo = await sessionInfo.run(); + toast({ + title: "Session terminated", + description: termInfo.reason() + }); + setShowLogin(true); + } catch (err) { + setShowLogin(true); + if (isIronError(err)) { + toast({ + variant: "destructive", + title: "Connection failed", + description: err.backtrace() + }); + } else { + toast({ + variant: "destructive", + title: "Connection failed", + description: `${err}` + }); + } + } + }; + + const ui = () => userInteractionRef.current; + + const toggleCursorKind = () => { + const u = ui(); + if (!u) return; + if (cursorOverrideActive) { + u.setCursorStyleOverride(null); + } else { + u.setCursorStyleOverride('url("crosshair.png") 7 7, default'); + } + setCursorOverrideActive((v) => !v); + }; + + return ( +
+ {showLogin && ( +
+

+ RDP Test Connection +

+ +
+ + + update("hostname", e.target.value) + } + /> + + + + update("domain", e.target.value) + } + /> + + + + update("username", e.target.value) + } + /> + + + + update("password", e.target.value) + } + /> + + + + update("gatewayAddress", e.target.value) + } + /> + + + + update("authtoken", e.target.value) + } + /> + + + update("pcb", e.target.value)} + /> + +
+ + + update( + "desktopWidth", + Number(e.target.value) || 0 + ) + } + /> + + + + update( + "desktopHeight", + Number(e.target.value) || 0 + ) + } + /> + +
+ + + update("kdcProxyUrl", e.target.value) + } + /> + +
+ + update("enableClipboard", checked === true) + } + /> + +
+ + +
+
+ )} + +
+
+ + + + + + + + +
+ + {moduleReady && ( + + )} +
+
+ ); +} + +function Field({ + label, + id, + children +}: { + label: string; + id: string; + children: React.ReactNode; +}) { + return ( +
+ + {children} +
+ ); +} diff --git a/src/app/rdp/page.tsx b/src/app/rdp/page.tsx new file mode 100644 index 000000000..52893af79 --- /dev/null +++ b/src/app/rdp/page.tsx @@ -0,0 +1,11 @@ +import RdpClient from "./RdpClient"; + +export const dynamic = "force-dynamic"; + +export const metadata = { + title: "RDP Test" +}; + +export default function RdpPage() { + return ; +} From d42b6076d2f3d0b1e8ee301edd4d060511c3a6de Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 11 May 2026 16:10:24 -0700 Subject: [PATCH 137/771] Comment out some fields --- src/app/rdp/RdpClient.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/rdp/RdpClient.tsx b/src/app/rdp/RdpClient.tsx index 17d12b066..c0c18edde 100644 --- a/src/app/rdp/RdpClient.tsx +++ b/src/app/rdp/RdpClient.tsx @@ -57,7 +57,7 @@ export default function RdpClient() { gatewayAddress: "ws://localhost:7171/jet/rdp", hostname: "172.31.3.58:3389", domain: "", - authtoken: "", + authtoken: "abc123", kdcProxyUrl: "", pcb: "", desktopWidth: 1280, @@ -281,7 +281,7 @@ export default function RdpClient() { } /> - + {/* update("pcb", e.target.value)} /> - + */}
- @@ -336,7 +336,7 @@ export default function RdpClient() { update("kdcProxyUrl", e.target.value) } /> - + */}
Date: Mon, 11 May 2026 16:53:03 -0700 Subject: [PATCH 138/771] Add first iteration of ssh proxy --- package-lock.json | 30 +++- package.json | 3 + src/app/ssh/SshClient.tsx | 355 +++++++++++++++++++++++++++++++++++++ src/app/ssh/page.tsx | 11 ++ src/types/css-modules.d.ts | 3 + 5 files changed, 399 insertions(+), 3 deletions(-) create mode 100644 src/app/ssh/SshClient.tsx create mode 100644 src/app/ssh/page.tsx create mode 100644 src/types/css-modules.d.ts diff --git a/package-lock.json b/package-lock.json index 463bae493..f9678e7b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@asteasolutions/zod-to-openapi": "8.4.1", "@aws-sdk/client-s3": "3.1011.0", - "@devolutions/iron-remote-desktop": "https://s3.us-east-1.amazonaws.com/static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz", + "@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz", "@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.0.tgz", "@faker-js/faker": "10.3.0", "@headlessui/react": "2.2.9", @@ -46,6 +46,9 @@ "@tailwindcss/forms": "0.5.11", "@tanstack/react-query": "5.90.21", "@tanstack/react-table": "8.21.3", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/xterm": "^6.0.0", "arctic": "3.7.0", "axios": "1.15.0", "better-sqlite3": "11.9.1", @@ -1463,8 +1466,8 @@ }, "node_modules/@devolutions/iron-remote-desktop": { "version": "0.0.0", - "resolved": "https://s3.us-east-1.amazonaws.com/static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz", - "integrity": "sha512-96z7WShjpJJhr4I2RzhXB52GcdmVFMEVvUgoQ0a20n3gATNJ+n2V3W2i8AUeMqVR38uvcyK3e+loY5T050NgQg==" + "resolved": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz", + "integrity": "sha512-9o7PkCw9fdvGTPs0hgsUJG10QleGgcdsSCw1ekLpUOlVXtWCuiuPH+0bPDFhLWxqbVA+8pyVhwqdOI+t1T3TNA==" }, "node_modules/@devolutions/iron-remote-desktop-rdp": { "version": "0.0.0", @@ -9663,6 +9666,27 @@ "win32" ] }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", + "license": "MIT" + }, + "node_modules/@xterm/addon-web-links": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz", + "integrity": "sha512-4Smom3RPyVp7ZMYOYDoC/9eGJJJqYhnPLGGqJ6wOBfB8VxPViJNSKdgRYb8NpaM6YSelEKbA2SStD7lGyqaobw==", + "license": "MIT" + }, + "node_modules/@xterm/xterm": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", + "integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", + "license": "MIT", + "workspaces": [ + "addons/*" + ] + }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", diff --git a/package.json b/package.json index 7ed8f53fa..33e54c3d0 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,9 @@ "@tailwindcss/forms": "0.5.11", "@tanstack/react-query": "5.90.21", "@tanstack/react-table": "8.21.3", + "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-web-links": "^0.12.0", + "@xterm/xterm": "^6.0.0", "arctic": "3.7.0", "axios": "1.15.0", "better-sqlite3": "11.9.1", diff --git a/src/app/ssh/SshClient.tsx b/src/app/ssh/SshClient.tsx new file mode 100644 index 000000000..1c0cf73d9 --- /dev/null +++ b/src/app/ssh/SshClient.tsx @@ -0,0 +1,355 @@ +"use client"; + +import "@xterm/xterm/css/xterm.css"; +import { useEffect, useRef, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +type FormState = { + gatewayAddress: string; + hostname: string; + port: string; + username: string; + password: string; + authToken: string; +}; + +export default function SshClient() { + const [form, setForm] = useState({ + gatewayAddress: "ws://localhost:7171/jet/ssh", + hostname: "", + port: "22", + username: "", + password: "", + authToken: "abc123" + }); + + const [connected, setConnected] = useState(false); + const [connecting, setConnecting] = useState(false); + const [error, setError] = useState(null); + + const terminalRef = useRef(null); + const xtermRef = useRef(null); + const fitAddonRef = useRef( + null + ); + const wsRef = useRef(null); + + // Mount the terminal div once connected. + useEffect(() => { + if (!connected || !terminalRef.current) return; + + let cancelled = false; + + (async () => { + const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = + await Promise.all([ + import("@xterm/xterm"), + import("@xterm/addon-fit"), + import("@xterm/addon-web-links") + ]); + if (cancelled || !terminalRef.current) return; + + const terminal = new Terminal({ + cursorBlink: true, + fontSize: 14, + fontFamily: "Menlo, Monaco, 'Courier New', monospace", + theme: { + background: "#0d0d0d", + foreground: "#f0f0f0" + }, + scrollback: 5000 + }); + + const fitAddon = new FitAddon(); + const webLinksAddon = new WebLinksAddon(); + terminal.loadAddon(fitAddon); + terminal.loadAddon(webLinksAddon); + + terminal.open(terminalRef.current); + fitAddon.fit(); + + xtermRef.current = terminal; + fitAddonRef.current = fitAddon; + + // Send user keystrokes to the WebSocket. + terminal.onData((data) => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send(JSON.stringify({ type: "data", data })); + } + }); + + // Send resize events. + terminal.onResize(({ cols, rows }) => { + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send( + JSON.stringify({ type: "resize", cols, rows }) + ); + } + }); + + // Send the initial size once the terminal is rendered. + const { cols, rows } = terminal; + if (wsRef.current?.readyState === WebSocket.OPEN) { + wsRef.current.send( + JSON.stringify({ type: "resize", cols, rows }) + ); + } + })().catch(console.error); + + return () => { + cancelled = true; + }; + }, [connected]); + + // Refit terminal when the window resizes. + useEffect(() => { + const onResize = () => fitAddonRef.current?.fit(); + window.addEventListener("resize", onResize); + return () => window.removeEventListener("resize", onResize); + }, []); + + // Cleanup on unmount. + useEffect(() => { + return () => { + wsRef.current?.close(); + xtermRef.current?.dispose(); + }; + }, []); + + function connect() { + setError(null); + setConnecting(true); + + const url = new URL(form.gatewayAddress); + // Pass connection parameters as query params so the proxy can route + // before any application-level framing is needed. + url.searchParams.set("host", form.hostname); + url.searchParams.set("port", form.port); + url.searchParams.set("username", form.username); + // Auth token is sent as a query param; the proxy validates it before + // forwarding any data. + url.searchParams.set("authToken", form.authToken); + + const ws = new WebSocket(url.toString(), ["ssh"]); + wsRef.current = ws; + + ws.onopen = () => { + // Send the password (or empty string) as the first frame so the + // proxy can complete SSH authentication before piping pty data. + ws.send(JSON.stringify({ type: "auth", password: form.password })); + setConnecting(false); + setConnected(true); + }; + + ws.onmessage = (evt) => { + if (typeof evt.data === "string") { + try { + const msg = JSON.parse(evt.data as string) as { + type: string; + data?: string; + error?: string; + }; + if (msg.type === "data" && msg.data) { + xtermRef.current?.write(msg.data); + } else if (msg.type === "error") { + xtermRef.current?.writeln( + `\r\n\x1b[31mError: ${msg.error}\x1b[0m\r\n` + ); + } + } catch { + xtermRef.current?.write(evt.data); + } + } else if (evt.data instanceof Blob) { + evt.data.text().then((t) => xtermRef.current?.write(t)); + } + }; + + ws.onerror = () => { + setConnecting(false); + setConnected(false); + setError("WebSocket connection failed"); + }; + + ws.onclose = (evt) => { + setConnecting(false); + setConnected(false); + xtermRef.current?.writeln( + `\r\n\x1b[33mConnection closed (code ${evt.code})\x1b[0m\r\n` + ); + }; + } + + function disconnect() { + wsRef.current?.close(); + xtermRef.current?.dispose(); + xtermRef.current = null; + setConnected(false); + } + + return ( +
+

SSH Terminal

+ + {!connected && ( +
+
+
+ + + setForm({ + ...form, + gatewayAddress: e.target.value + }) + } + placeholder="ws://localhost:7171/jet/ssh" + className="bg-neutral-800 border-neutral-700 text-white" + /> +
+ +
+ + + setForm({ + ...form, + hostname: e.target.value + }) + } + placeholder="192.168.1.1" + className="bg-neutral-800 border-neutral-700 text-white" + /> +
+ +
+ + + setForm({ ...form, port: e.target.value }) + } + placeholder="22" + className="bg-neutral-800 border-neutral-700 text-white" + /> +
+ +
+ + + setForm({ + ...form, + username: e.target.value + }) + } + placeholder="root" + className="bg-neutral-800 border-neutral-700 text-white" + /> +
+ +
+ + + setForm({ + ...form, + password: e.target.value + }) + } + className="bg-neutral-800 border-neutral-700 text-white" + /> +
+ +
+ + + setForm({ + ...form, + authToken: e.target.value + }) + } + className="bg-neutral-800 border-neutral-700 text-white" + /> +
+
+ + {error &&

{error}

} + + +
+ )} + + {connected && ( +
+
+ +
+
+
+ )} +
+ ); +} diff --git a/src/app/ssh/page.tsx b/src/app/ssh/page.tsx new file mode 100644 index 000000000..62159f301 --- /dev/null +++ b/src/app/ssh/page.tsx @@ -0,0 +1,11 @@ +import SshClient from "./SshClient"; + +export const dynamic = "force-dynamic"; + +export const metadata = { + title: "SSH Terminal" +}; + +export default function SshPage() { + return ; +} diff --git a/src/types/css-modules.d.ts b/src/types/css-modules.d.ts new file mode 100644 index 000000000..0857acac4 --- /dev/null +++ b/src/types/css-modules.d.ts @@ -0,0 +1,3 @@ +// Allow importing plain CSS files as side-effect imports (e.g. xterm.css). +declare module "*.css" {} +declare module "@xterm/xterm/css/xterm.css" {} From 3f17f1a468f27627f9fc325d8ad8c9108cd3ef41 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 11 May 2026 20:13:48 -0700 Subject: [PATCH 139/771] Support rdp --- src/app/rdp/RdpClient.tsx | 100 +++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/src/app/rdp/RdpClient.tsx b/src/app/rdp/RdpClient.tsx index c0c18edde..b3d166d76 100644 --- a/src/app/rdp/RdpClient.tsx +++ b/src/app/rdp/RdpClient.tsx @@ -8,8 +8,13 @@ import { Checkbox } from "@/components/ui/checkbox"; import { toast } from "@app/hooks/useToast"; import type { UserInteraction, - IronError + IronError, + FileTransferProvider } from "@devolutions/iron-remote-desktop/dist"; +import type { + RdpFileTransferProvider, + FileInfo +} from "@devolutions/iron-remote-desktop-rdp/dist"; declare module "react" { namespace JSX { @@ -72,6 +77,13 @@ export default function RdpClient() { const userInteractionRef = useRef(null); const backendRef = useRef(null); + // Holds the RdpFileTransferProvider constructor so we can create a fresh + // instance per session (avoids stale upload state across reconnects). + const fileTransferClassRef = useRef( + null + ); + // Active session's provider instance; replaced on each connect. + const fileTransferRef = useRef(null); const extensionsRef = useRef<{ displayControl: (enable: boolean) => unknown; preConnectionBlob: (pcb: string) => unknown; @@ -97,6 +109,11 @@ export default function RdpClient() { preConnectionBlob: rdpMod.preConnectionBlob, kdcProxyUrl: rdpMod.kdcProxyUrl }; + + // Store the class; a fresh instance is created per session. + fileTransferClassRef.current = + rdpMod.RdpFileTransferProvider as unknown as typeof RdpFileTransferProvider; + // Importing the package registers the custom element as a side // effect. Touch the default export to avoid tree-shaking. void coreMod; @@ -161,6 +178,56 @@ export default function RdpClient() { userInteraction.setEnableClipboard(form.enableClipboard); + // Dispose any previous session's provider and create a fresh one so + // there is no stale upload state from a prior connection. + fileTransferRef.current?.dispose(); + const ProviderClass = fileTransferClassRef.current; + const fileTransfer = ProviderClass ? new ProviderClass() : null; + fileTransferRef.current = fileTransfer; + + if (fileTransfer) { + // Auto-download files when the remote copies them to clipboard. + fileTransfer.on("files-available", (files: FileInfo[]) => { + const downloadable = files.filter((f) => !f.isDirectory); + if (downloadable.length === 0) return; + toast({ + title: `Downloading ${downloadable.length} file(s) from remote…` + }); + for (let i = 0; i < files.length; i++) { + const file = files[i]; + if (file.isDirectory) continue; + const { completion } = fileTransfer.downloadFile(file, i); + completion + .then((blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = file.name; + a.click(); + URL.revokeObjectURL(url); + }) + .catch((err) => { + toast({ + variant: "destructive", + title: `Download failed: ${file.name}`, + description: `${err}` + }); + }); + } + }); + + // Notify when individual uploads complete (remote pasted a file). + fileTransfer.on("upload-complete", (file: File) => { + toast({ title: `Uploaded: ${file.name}` }); + }); + + // Register with the web component so CLIPRDR extensions are + // wired up before connect() builds the session. + userInteraction.enableFileTransfer( + fileTransfer as unknown as FileTransferProvider + ); + } + const builder = userInteraction .configBuilder() .withUsername(form.username) @@ -190,6 +257,8 @@ export default function RdpClient() { userInteraction.setVisibility(true); const termInfo = await sessionInfo.run(); + fileTransferRef.current?.dispose(); + fileTransferRef.current = null; toast({ title: "Session terminated", description: termInfo.reason() @@ -401,12 +470,39 @@ export default function RdpClient() { > Meta - */} + +
+
+ )} + +
+
+ + + +
+ + {/* noVNC mounts a inside this div */} +
+
+
+ ); +} + +function Field({ + label, + id, + children +}: { + label: string; + id: string; + children: React.ReactNode; +}) { + return ( +
+ + {children} +
+ ); +} diff --git a/src/app/vnc/page.tsx b/src/app/vnc/page.tsx new file mode 100644 index 000000000..0a9b4b4c3 --- /dev/null +++ b/src/app/vnc/page.tsx @@ -0,0 +1,11 @@ +import VncClient from "./VncClient"; + +export const dynamic = "force-dynamic"; + +export const metadata = { + title: "VNC Test" +}; + +export default function VncPage() { + return ; +} From 743621eb2539485fa98e1f33ec48b394fda820c7 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 12 May 2026 21:48:59 -0700 Subject: [PATCH 141/771] Add browserGatewayTarget table --- server/db/pg/schema/privateSchema.ts | 20 ++++++++++++++++++++ server/db/sqlite/schema/privateSchema.ts | 22 ++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index 229fc9ff0..3b4f459f3 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -580,6 +580,23 @@ export const trialNotifications = pgTable("trialNotifications", { sentAt: bigint("sentAt", { mode: "number" }).notNull() }); +export const browserGatewayTarget = pgTable("browserGatewayTarget", { + browserGatewayTargetId: serial("browserGatewayTargetId").primaryKey(), + resourceId: integer("resourceId") + .references(() => resources.resourceId, { + onDelete: "cascade" + }) + .notNull(), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), + type: varchar("type").notNull(), // "ssh", "rdp", "vnc" + destination: varchar("destination").notNull(), + destinationPort: integer("destinationPort").notNull() +}); + export type Approval = InferSelectModel; export type Limit = InferSelectModel; export type Account = InferSelectModel; @@ -627,3 +644,6 @@ export type AlertEmailRecipients = InferSelectModel< >; export type AlertWebhookActions = InferSelectModel; export type TrialNotification = InferSelectModel; +export type BrowserGatewayTarget = InferSelectModel< + typeof browserGatewayTarget +>; diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index ae7360780..1fdace69b 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -588,6 +588,25 @@ export const trialNotifications = sqliteTable("trialNotifications", { sentAt: integer("sentAt").notNull() }); +export const browserGatewayTarget = sqliteTable("browserGatewayTarget", { + browserGatewayTargetId: integer("browserGatewayTargetId").primaryKey({ + autoIncrement: true + }), + resourceId: integer("resourceId") + .references(() => resources.resourceId, { + onDelete: "cascade" + }) + .notNull(), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), + type: text("type").notNull(), // "ssh", "rdp", "vnc" + destination: text("destination").notNull(), + destinationPort: integer("destinationPort").notNull() +}); + export type Approval = InferSelectModel; export type Limit = InferSelectModel; export type Account = InferSelectModel; @@ -627,3 +646,6 @@ export type AlertEmailAction = InferSelectModel; export type AlertEmailRecipient = InferSelectModel; export type AlertWebhookAction = InferSelectModel; export type TrialNotification = InferSelectModel; +export type BrowserGatewayTarget = InferSelectModel< + typeof browserGatewayTarget +>; From 4e07e9c52c8708bde20887f5598cf54b91bc7d35 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 May 2026 11:56:23 -0700 Subject: [PATCH 142/771] Working on new target type --- server/routers/newt/buildConfiguration.ts | 11 ++++++ server/routers/newt/sync.ts | 14 ++++++- server/routers/newt/targets.ts | 47 ++++++++++++++++++++++- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/server/routers/newt/buildConfiguration.ts b/server/routers/newt/buildConfiguration.ts index f87d38450..ce126f9c5 100644 --- a/server/routers/newt/buildConfiguration.ts +++ b/server/routers/newt/buildConfiguration.ts @@ -1,4 +1,6 @@ import { + browserGatewayTarget, + BrowserGatewayTarget, clients, clientSiteResourcesAssociationsCache, clientSitesAssociationsCache, @@ -310,3 +312,12 @@ export async function buildTargetConfigurationForNewtClient( udpTargets }; } + +export async function buildBrowserGatewayTargetConfigurationForNewtClient( + siteId: number +): Promise { + return await db + .select() + .from(browserGatewayTarget) + .where(eq(browserGatewayTarget.siteId, siteId)); +} diff --git a/server/routers/newt/sync.ts b/server/routers/newt/sync.ts index 6fce13ff3..d7ceefde4 100644 --- a/server/routers/newt/sync.ts +++ b/server/routers/newt/sync.ts @@ -3,6 +3,7 @@ import { eq } from "drizzle-orm"; import { sendToClient } from "#dynamic/routers/ws"; import logger from "@server/logger"; import { + buildBrowserGatewayTargetConfigurationForNewtClient, buildClientConfigurationForNewtClient, buildTargetConfigurationForNewtClient } from "./buildConfiguration"; @@ -12,6 +13,9 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) { const { tcpTargets, udpTargets, validHealthCheckTargets } = await buildTargetConfigurationForNewtClient(site.siteId); + const browserGatewayTargets = + await buildBrowserGatewayTargetConfigurationForNewtClient(site.siteId); + let exitNode: ExitNode | undefined; if (site.exitNodeId) { [exitNode] = await db @@ -36,7 +40,15 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) { }, healthCheckTargets: validHealthCheckTargets, peers: peers, - clientTargets: targets + clientTargets: targets, + browserGatewayTargets: browserGatewayTargets.map((t) => ({ + id: t.browserGatewayTargetId, + resourceId: t.resourceId, + siteId: t.siteId, + type: t.type, + destination: t.destination, + destinationPort: t.destinationPort + })) } }, { diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts index ac25fb27d..ca15e50cc 100644 --- a/server/routers/newt/targets.ts +++ b/server/routers/newt/targets.ts @@ -1,4 +1,4 @@ -import { Target, TargetHealthCheck } from "@server/db"; +import { BrowserGatewayTarget, Target, TargetHealthCheck } from "@server/db"; import { sendToClient } from "#dynamic/routers/ws"; import logger from "@server/logger"; import { canCompress } from "@server/lib/clientVersionChecks"; @@ -239,3 +239,48 @@ export async function removeTargets( { incrementConfigVersion: true, compress: canCompress(version, "newt") } ); } + +export async function sendBrowserGatewayTargets( + newtId: string, + targets: BrowserGatewayTarget[], + version?: string | null +) { + if (targets.length === 0) return; + + const payload = targets.map((t) => ({ + id: t.browserGatewayTargetId, + resourceId: t.resourceId, + siteId: t.siteId, + type: t.type, + destination: t.destination, + destinationPort: t.destinationPort + })); + + await sendToClient( + newtId, + { + type: "newt/browsergateway/add", + data: { + targets: payload + } + }, + { incrementConfigVersion: true, compress: canCompress(version, "newt") } + ); +} + +export async function removeBrowserGatewayTarget( + newtId: string, + browserGatewayTargetId: number, + version?: string | null +) { + await sendToClient( + newtId, + { + type: "newt/browsergateway/remove", + data: { + ids: [browserGatewayTargetId] + } + }, + { incrementConfigVersion: true, compress: canCompress(version, "newt") } + ); +} From de70d72e0d00914ac76de1915b32ca6c88628470 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 May 2026 17:33:16 -0700 Subject: [PATCH 143/771] Add gateway endpoints into the traefik config --- .../private/lib/traefik/getTraefikConfig.ts | 274 ++++++++++++++++++ server/routers/newt/buildConfiguration.ts | 24 +- .../routers/newt/handleNewtRegisterMessage.ts | 18 +- server/routers/newt/sync.ts | 21 +- src/app/rdp/RdpClient.tsx | 6 +- 5 files changed, 312 insertions(+), 31 deletions(-) diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index 481192fb5..e551a7a7a 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -12,6 +12,7 @@ */ import { + browserGatewayTarget, certificates, db, domainNamespaces, @@ -277,6 +278,115 @@ export async function getTraefikConfig( }); }); + // Query browser gateway targets for this exit node + const browserGatewayRows = await db + .select({ + // Resource fields + resourceId: resources.resourceId, + resourceName: resources.name, + fullDomain: resources.fullDomain, + ssl: resources.ssl, + subdomain: resources.subdomain, + domainId: resources.domainId, + enabled: resources.enabled, + wildcard: resources.wildcard, + domainCertResolver: domains.certResolver, + preferWildcardCert: domains.preferWildcardCert, + domainNamespaceId: domainNamespaces.domainNamespaceId, + // Browser gateway target fields + browserGatewayTargetId: browserGatewayTarget.browserGatewayTargetId, + bgType: browserGatewayTarget.type, + // Site fields + siteId: sites.siteId, + siteType: sites.type, + siteOnline: sites.online, + subnet: sites.subnet, + siteExitNodeId: sites.exitNodeId + }) + .from(browserGatewayTarget) + .innerJoin(sites, eq(sites.siteId, browserGatewayTarget.siteId)) + .innerJoin( + resources, + eq(resources.resourceId, browserGatewayTarget.resourceId) + ) + .leftJoin(domains, eq(domains.domainId, resources.domainId)) + .leftJoin( + domainNamespaces, + eq(domainNamespaces.domainId, resources.domainId) + ) + .where( + and( + eq(resources.enabled, true), + or( + eq(sites.exitNodeId, exitNodeId), + and( + isNull(sites.exitNodeId), + sql`(${siteTypes.includes("local") ? 1 : 0} = 1)`, + eq(sites.type, "local"), + sql`(${build != "saas" ? 1 : 0} = 1)` + ) + ), + inArray(sites.type, siteTypes) + ) + ); + + // Group browser gateway targets by resource + type BrowserGatewayResourceEntry = { + resourceId: number; + name: string; + fullDomain: string | null; + ssl: boolean | null; + subdomain: string | null; + domainId: string | null; + enabled: boolean | null; + wildcard: boolean | null; + domainCertResolver: string | null; + preferWildcardCert: boolean | null; + targets: { + browserGatewayTargetId: number; + bgType: string; + siteId: number; + siteType: string; + siteOnline: boolean | null; + subnet: string | null; + siteExitNodeId: number | null; + }[]; + }; + const browserGatewayResourcesMap = new Map< + number, + BrowserGatewayResourceEntry + >(); + + for (const row of browserGatewayRows) { + if (filterOutNamespaceDomains && row.domainNamespaceId) { + continue; + } + if (!browserGatewayResourcesMap.has(row.resourceId)) { + browserGatewayResourcesMap.set(row.resourceId, { + resourceId: row.resourceId, + name: sanitize(row.resourceName) || "", + fullDomain: row.fullDomain, + ssl: row.ssl, + subdomain: row.subdomain, + domainId: row.domainId, + enabled: row.enabled, + wildcard: row.wildcard, + domainCertResolver: row.domainCertResolver, + preferWildcardCert: row.preferWildcardCert, + targets: [] + }); + } + browserGatewayResourcesMap.get(row.resourceId)!.targets.push({ + browserGatewayTargetId: row.browserGatewayTargetId, + bgType: row.bgType, + siteId: row.siteId, + siteType: row.siteType, + siteOnline: row.siteOnline, + subnet: row.subnet, + siteExitNodeId: row.siteExitNodeId + }); + } + let siteResourcesWithFullDomain: { siteResourceId: number; fullDomain: string | null; @@ -324,6 +434,12 @@ export async function getTraefikConfig( domains.add(sr.fullDomain); } } + // Include browser gateway resource domains + for (const bgResource of browserGatewayResourcesMap.values()) { + if (bgResource.enabled && bgResource.ssl && bgResource.fullDomain) { + domains.add(bgResource.fullDomain); + } + } // get the valid certs for these domains validCerts = await getValidCertificatesForDomains(domains, true); // we are caching here because this is called often // logger.debug(`Valid certs for domains: ${JSON.stringify(validCerts)}`); @@ -925,6 +1041,164 @@ export async function getTraefikConfig( } } + // Generate Traefik config for browser gateway resources + const browserGatewayPort = 39999; + for (const [, bgResource] of browserGatewayResourcesMap.entries()) { + if (!bgResource.enabled) continue; + if (!bgResource.domainId) continue; + if (!bgResource.fullDomain) continue; + + if (!config_output.http.routers) config_output.http.routers = {}; + if (!config_output.http.services) config_output.http.services = {}; + + const fullDomain = bgResource.fullDomain; + const additionalMiddlewares = + config.getRawConfig().traefik.additional_middlewares || []; + const routerMiddlewares = [ + badgerMiddlewareName, + ...additionalMiddlewares + ]; + + const hostRule = `Host(\`${fullDomain}\`)`; + + // Build TLS config + let tls = {}; + if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) { + const domainParts = fullDomain.split("."); + let wildCard: string; + if (domainParts.length <= 2) { + wildCard = `*.${domainParts.join(".")}`; + } else { + wildCard = `*.${domainParts.slice(1).join(".")}`; + } + if (!bgResource.subdomain) { + wildCard = fullDomain; + } + + const globalDefaultResolver = + config.getRawConfig().traefik.cert_resolver; + const globalDefaultPreferWildcard = + config.getRawConfig().traefik.prefer_wildcard_cert; + const resolverName = bgResource.domainCertResolver + ? bgResource.domainCertResolver.trim() + : globalDefaultResolver; + const preferWildcard = + bgResource.preferWildcardCert !== undefined && + bgResource.preferWildcardCert !== null + ? bgResource.preferWildcardCert + : globalDefaultPreferWildcard; + + tls = { + certResolver: resolverName, + ...(preferWildcard ? { domains: [{ main: wildCard }] } : {}) + }; + } else { + const matchingCert = validCerts.find( + (cert) => cert.queriedDomain === fullDomain + ); + if (!matchingCert) { + logger.debug( + `No matching certificate found for browser gateway domain: ${fullDomain}` + ); + continue; + } + } + + const bgUiServiceName = `bg-r${bgResource.resourceId}-ui-service`; + + if (bgResource.ssl) { + const redirectRouterName = `bg-r${bgResource.resourceId}-redirect`; + config_output.http.routers![redirectRouterName] = { + entryPoints: [config.getRawConfig().traefik.http_entrypoint], + middlewares: [redirectHttpsMiddlewareName], + service: bgUiServiceName, + rule: hostRule, + priority: 100 + }; + } + + // Collect online sites for this resource (for any type) + const anySiteOnline = bgResource.targets.some((t) => t.siteOnline); + + // Group targets by type and generate per-type websocket routers and services + const typeMap = new Map(); + for (const t of bgResource.targets) { + if (!typeMap.has(t.bgType)) typeMap.set(t.bgType, []); + typeMap.get(t.bgType)!.push(t); + } + + for (const [bgType, typedTargets] of typeMap.entries()) { + const bgKey = `bg-r${bgResource.resourceId}-${bgType}`; + const bgRouterName = `${bgKey}-router`; + const bgServiceName = `${bgKey}-service`; + const bgRule = `${hostRule} && PathPrefix(\`/gateway/${bgType}\`)`; + + const servers = typedTargets + .filter((t) => { + if (!t.siteOnline && anySiteOnline) return false; + if (t.siteType === "newt") return !!t.subnet; + return false; // browser gateway only supported on newt sites + }) + .map((t) => ({ + url: `http://${t.subnet!.split("/")[0]}:${browserGatewayPort}` + })) + .filter((v, i, a) => a.findIndex((u) => u.url === v.url) === i); + + config_output.http.routers![bgRouterName] = { + entryPoints: [ + bgResource.ssl + ? config.getRawConfig().traefik.https_entrypoint + : config.getRawConfig().traefik.http_entrypoint + ], + middlewares: routerMiddlewares, + service: bgServiceName, + rule: bgRule, + priority: 110, // higher than 105 (UI router) to match /gateway/* first + ...(bgResource.ssl ? { tls } : {}) + }; + + config_output.http.services![bgServiceName] = { + loadBalancer: { + servers + } + }; + } + + // UI router: serve the browser gateway pages from the internal pangolin instance + // Covers /{type} paths for each configured type plus Next.js assets + const internalHost = config.getRawConfig().server.internal_hostname; + const internalPort = config.getRawConfig().server.next_port; + + const typePaths = Array.from(typeMap.keys()) + .map((t) => `PathPrefix(\`/${t}\`)`) + .join(" || "); + const uiRule = `${hostRule} && (${typePaths} || PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`))`; + + config_output.http.services![bgUiServiceName] = { + loadBalancer: { + servers: [ + { + url: `http://${internalHost}:${internalPort}` + } + ] + } + }; + + config_output.http.routers![`bg-r${bgResource.resourceId}-ui-router`] = + { + entryPoints: [ + bgResource.ssl + ? config.getRawConfig().traefik.https_entrypoint + : config.getRawConfig().traefik.http_entrypoint + ], + middlewares: routerMiddlewares, + service: bgUiServiceName, + rule: uiRule, + priority: 105, + ...(bgResource.ssl ? { tls } : {}) + }; + } + // Add Traefik routes for siteResource aliases (HTTP mode + SSL) so that // Traefik generates TLS certificates for those domains even when no // matching resource exists yet. diff --git a/server/routers/newt/buildConfiguration.ts b/server/routers/newt/buildConfiguration.ts index ce126f9c5..fb398236a 100644 --- a/server/routers/newt/buildConfiguration.ts +++ b/server/routers/newt/buildConfiguration.ts @@ -235,6 +235,11 @@ export async function buildTargetConfigurationForNewtClient( .from(targetHealthCheck) .where(eq(targetHealthCheck.siteId, siteId)); + const allBrowserGatewayTargets = await db + .select() + .from(browserGatewayTarget) + .where(eq(browserGatewayTarget.siteId, siteId)); + const { tcpTargets, udpTargets } = allTargets.reduce( (acc, target) => { // Filter out invalid targets @@ -306,18 +311,17 @@ export async function buildTargetConfigurationForNewtClient( (target) => target !== null ); + const browserGatewayTargets = allBrowserGatewayTargets.map((t) => ({ + id: t.browserGatewayTargetId, + type: t.type, + destination: t.destination, + destinationPort: t.destinationPort + })); + return { validHealthCheckTargets, tcpTargets, - udpTargets + udpTargets, + browserGatewayTargets }; } - -export async function buildBrowserGatewayTargetConfigurationForNewtClient( - siteId: number -): Promise { - return await db - .select() - .from(browserGatewayTarget) - .where(eq(browserGatewayTarget.siteId, siteId)); -} diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index f3902a35d..bd4aaacb3 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -43,8 +43,13 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { const siteId = newt.siteId; - const { publicKey, pingResults, newtVersion, backwardsCompatible, chainId } = - message.data; + const { + publicKey, + pingResults, + newtVersion, + backwardsCompatible, + chainId + } = message.data; if (!publicKey) { logger.warn("Public key not provided"); return; @@ -191,8 +196,12 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { .where(eq(newts.newtId, newt.newtId)); } - const { tcpTargets, udpTargets, validHealthCheckTargets } = - await buildTargetConfigurationForNewtClient(siteId, newtVersion); + const { + tcpTargets, + udpTargets, + validHealthCheckTargets, + browserGatewayTargets + } = await buildTargetConfigurationForNewtClient(siteId, newtVersion); logger.debug( `Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}` @@ -212,6 +221,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { tcp: tcpTargets }, healthCheckTargets: validHealthCheckTargets, + browserGatewayTargets: browserGatewayTargets, chainId: chainId } }, diff --git a/server/routers/newt/sync.ts b/server/routers/newt/sync.ts index d7ceefde4..b8f152bec 100644 --- a/server/routers/newt/sync.ts +++ b/server/routers/newt/sync.ts @@ -3,18 +3,18 @@ import { eq } from "drizzle-orm"; import { sendToClient } from "#dynamic/routers/ws"; import logger from "@server/logger"; import { - buildBrowserGatewayTargetConfigurationForNewtClient, buildClientConfigurationForNewtClient, buildTargetConfigurationForNewtClient } from "./buildConfiguration"; import { canCompress } from "@server/lib/clientVersionChecks"; export async function sendNewtSyncMessage(newt: Newt, site: Site) { - const { tcpTargets, udpTargets, validHealthCheckTargets } = - await buildTargetConfigurationForNewtClient(site.siteId); - - const browserGatewayTargets = - await buildBrowserGatewayTargetConfigurationForNewtClient(site.siteId); + const { + tcpTargets, + udpTargets, + validHealthCheckTargets, + browserGatewayTargets + } = await buildTargetConfigurationForNewtClient(site.siteId); let exitNode: ExitNode | undefined; if (site.exitNodeId) { @@ -41,14 +41,7 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) { healthCheckTargets: validHealthCheckTargets, peers: peers, clientTargets: targets, - browserGatewayTargets: browserGatewayTargets.map((t) => ({ - id: t.browserGatewayTargetId, - resourceId: t.resourceId, - siteId: t.siteId, - type: t.type, - destination: t.destination, - destinationPort: t.destinationPort - })) + browserGatewayTargets: browserGatewayTargets } }, { diff --git a/src/app/rdp/RdpClient.tsx b/src/app/rdp/RdpClient.tsx index b3d166d76..15ca74f9e 100644 --- a/src/app/rdp/RdpClient.tsx +++ b/src/app/rdp/RdpClient.tsx @@ -58,11 +58,11 @@ const isIronError = (error: unknown): error is IronError => { export default function RdpClient() { const [form, setForm] = useState({ username: "Administrator", - password: "Wdvwy1W*ITK-(OK.sW?nVK%?mTl30wL0", - gatewayAddress: "ws://localhost:7171/jet/rdp", + password: "Password123!", + gatewayAddress: "ws://localhost:8082/rdp", hostname: "172.31.3.58:3389", domain: "", - authtoken: "abc123", + authtoken: "pangolin-browser-gateway-dev", kdcProxyUrl: "", pcb: "", desktopWidth: 1280, From a6ae9290f28e472a0ed49b686892e73da55a90d6 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 May 2026 18:01:36 -0700 Subject: [PATCH 144/771] Serve the resource from the right place --- .../private/lib/traefik/getTraefikConfig.ts | 54 +++++++++++++------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index e551a7a7a..5def19865 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -1153,7 +1153,7 @@ export async function getTraefikConfig( middlewares: routerMiddlewares, service: bgServiceName, rule: bgRule, - priority: 110, // higher than 105 (UI router) to match /gateway/* first + priority: 110, // highest - websocket path takes precedence ...(bgResource.ssl ? { tls } : {}) }; @@ -1164,37 +1164,59 @@ export async function getTraefikConfig( }; } - // UI router: serve the browser gateway pages from the internal pangolin instance - // Covers /{type} paths for each configured type plus Next.js assets + // UI: serve the browser gateway page from the internal pangolin instance. + // The primary type is used for the path rewrite (e.g. /rdp), mirroring + // how the maintenance page rewrites everything to /maintenance-screen. + const primaryType = typeMap.keys().next().value as string; const internalHost = config.getRawConfig().server.internal_hostname; const internalPort = config.getRawConfig().server.next_port; + const uiRewriteMiddlewareName = `bg-r${bgResource.resourceId}-ui-rewrite`; + const entrypoint = bgResource.ssl + ? config.getRawConfig().traefik.https_entrypoint + : config.getRawConfig().traefik.http_entrypoint; - const typePaths = Array.from(typeMap.keys()) - .map((t) => `PathPrefix(\`/${t}\`)`) - .join(" || "); - const uiRule = `${hostRule} && (${typePaths} || PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`))`; + if (!config_output.http.middlewares) { + config_output.http.middlewares = {}; + } + + config_output.http.middlewares![uiRewriteMiddlewareName] = { + replacePathRegex: { + regex: "^/(.*)", + replacement: `/${primaryType}` + } + }; config_output.http.services![bgUiServiceName] = { loadBalancer: { servers: [ { - url: `http://${internalHost}:${internalPort}` + // url: `http://${internalHost}:${internalPort}` + url: `https://owen-devel.hostlocal.app` } ] } }; + // Assets router at higher priority so /_next files load without rewrite + config_output.http.routers![ + `bg-r${bgResource.resourceId}-assets-router` + ] = { + entryPoints: [entrypoint], + middlewares: routerMiddlewares, + service: bgUiServiceName, + rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`))`, + priority: 101, + ...(bgResource.ssl ? { tls } : {}) + }; + + // Catch-all router rewrites everything on the domain to /{primaryType} config_output.http.routers![`bg-r${bgResource.resourceId}-ui-router`] = { - entryPoints: [ - bgResource.ssl - ? config.getRawConfig().traefik.https_entrypoint - : config.getRawConfig().traefik.http_entrypoint - ], - middlewares: routerMiddlewares, + entryPoints: [entrypoint], + middlewares: [...routerMiddlewares, uiRewriteMiddlewareName], service: bgUiServiceName, - rule: uiRule, - priority: 105, + rule: hostRule, + priority: 100, ...(bgResource.ssl ? { tls } : {}) }; } From 013af49137a558b570a4fa97a4761d7832feaffe Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 May 2026 21:11:25 -0700 Subject: [PATCH 145/771] Add favicon passthrough --- server/private/lib/traefik/getTraefikConfig.ts | 11 +++++------ src/app/favicon.ico | Bin 0 -> 15406 bytes src/app/rdp/RdpClient.tsx | 15 +++------------ 3 files changed, 8 insertions(+), 18 deletions(-) create mode 100644 src/app/favicon.ico diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index 5def19865..132bb95bc 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -705,7 +705,7 @@ export async function getTraefikConfig( resource.ssl ? entrypointHttps : entrypointHttp ], service: maintenanceServiceName, - rule: `${rule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`))`, + rule: `${rule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`)) `, priority: 2001, ...(resource.ssl ? { tls } : {}) }; @@ -1190,8 +1190,7 @@ export async function getTraefikConfig( loadBalancer: { servers: [ { - // url: `http://${internalHost}:${internalPort}` - url: `https://owen-devel.hostlocal.app` + url: `http://${internalHost}:${internalPort}` } ] } @@ -1204,7 +1203,7 @@ export async function getTraefikConfig( entryPoints: [entrypoint], middlewares: routerMiddlewares, service: bgUiServiceName, - rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`))`, + rule: `${hostRule} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`, priority: 101, ...(bgResource.ssl ? { tls } : {}) }; @@ -1336,7 +1335,7 @@ export async function getTraefikConfig( config_output.http.routers[`${siteResourceRouterName}-assets`] = { entryPoints: [config.getRawConfig().traefik.https_entrypoint], service: siteResourceServiceName, - rule: `Host(\`${fullDomain}\`) && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`))`, + rule: `Host(\`${fullDomain}\`) && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`, priority: 101, tls }; @@ -1439,7 +1438,7 @@ export async function getTraefikConfig( config.getRawConfig().traefik.https_entrypoint ], service: "landing-service", - rule: `Host(\`${fullDomain}\`) && (PathRegexp(\`^/auth/resource/[^/]+$\`) || PathRegexp(\`^/auth/idp/[0-9]+/oidc/callback\`) || PathPrefix(\`/_next\`) || Path(\`/auth/org\`) || PathRegexp(\`^/__nextjs*\`))`, + rule: `Host(\`${fullDomain}\`) && (PathRegexp(\`^/auth/resource/[^/]+$\`) || PathRegexp(\`^/auth/idp/[0-9]+/oidc/callback\`) || PathPrefix(\`/_next\`) || Path(\`/auth/org\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`, priority: 203, tls: tls }; diff --git a/src/app/favicon.ico b/src/app/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bcaab339d8dd0c5be5e558c11e1af040a73e72e0 GIT binary patch literal 15406 zcmeI3e~=tS6~`y?TR({T}d);do(~PZ*U3M|JcNr&cZ5Rg{hSAmKzwf=PVJxL=zx~F)zt=Dx z?J$hJsY4T5_~qOZ{nEB$=L+7wQn|BlY|pgq9+$SgUQ>CWouSG*Pevr8ym{2$lv1TH zLg$Y>mm}9Gw3g0P>&+;g>;mUK8QUEiufHkME=p%zcA-a)Cw`f{_x0E|%DpXZl@1W> zX=|t}ZI>2NZ!Nr5@6os6-DaONL#SkI=a)6@m2=Yshl!`%`brt0M6bys9b7=bn_ouj4AiKXr+vn_&gTigX&!!rCUs}08%qaKnXuHb0a{rK; z?{-Q%$~w%xSKg_gt>OXT{b#!1b|bg>mHM^gC31fQoNK{%xwM_NXn0q~Rxp=ms**Xk zue=R7{$4{vIsZXtzMEFw85z6uVZ$g-GcE6PQWsh`Qh!rt!Aog8z3vAx%Gppu1K!kK zNgwZ%@f!c`Wb$r57*+{Q(N)1=+Rn$Rw)e`QmJF-U7z4hID949cF0luj8bFo#@-ApfxXLIf_31q(VEi0<68kijUpikr5M3!ug+{Awkv7=_+ z$Skq#nS%TNaozoK)i^>Iz0x*24g>Oe7jk+%aUYd?Ya<&1tqyF~(h5!G{!i?39RG}6 zJiJn`feg@*>zbKktjz^zvu=s4b{`b}$nEgl;_?mw7UuBj$@n{~x~%87585TYBm0E` z{jBBf2aaoy_xFSLGB&~YN$+{*n?ajq@5t?Xp^dCBfcLL>-o$tuEp&LlZc@JEVMnvb+z%`$tLmoA{Cez2@$|4Ki)_H2AE`Sne+Ze)N3}{kl1L zpX`nduV{|1Uz>Sw*{nt7U1zBEJM&zG$GMa7sryYiZZ1DIyBQ4Pk3erDa$h3txZjqt z-9zj0XgvScAirb8*9&mW>G{XDrd2vh){mepxFa*)m&OOwexL0gul+>ZTh}TE-40nW zZcp<3H`YZ@`W0Pk(yF*1%)_)tW{Ry7Y~Z>u{H^6@>>6#a78wM6mb=7AloR7j#64!- zIXI}JV;toUhQHT)Mn|cKEmiKL{+PsZmH0e`vse&qzd zdid`{5ySUMId(HGlk7>^XA$U8HT(enwn-?E1st`>T8d z!wpg1!Mpa+m3wC#jW(5S%GkriUH*@8{F<5fzN+I3e1>dcFxO!Xq>;{U_1IU~FRAv;_-tem$G_J`?2~y!+$sT&!b_ zW@Xx-zVJ@x-KzK{;2XVX3nMw~#l^G<;_9jPe;`$G4~_C^_)hF3F+B6JpX*94Svwx! z{s?1*-{36g`JPF~i#oXzN3sohwNN2bD9wd;yo{LV*w+^QlLlari`aB{SFI=L%?M?8 zFm8u8;}bXFR`t14jqTybxH>Le1+ROszn7$J=g5wpft{)IxV?Y$&r}7&cRvi4L0J_DZ6-3 z&MNNZ`zc9t5ABf)KHZVbkC&K}o4~Rpj;dfwDd#Ne+>cN9EO*IMzREQ)ey!#HBJH2T z*4-ufCUgFVSyA6Mj&{psJ>{)*46fV~ytVDm^t#9T^vcsDH!D6rV*=)u{DV=k-D&cx zItLx&hqa6y9}i52Aj=m5>~fcN#Oy22!0+kjot%1td~Aa?6*}U_t{^Yj-(?N8Wo|*Q zL)+JgT(ipg7~_v$>Q%;sx>H@3gZ8Dglon0HsKdPXjR&BZJx2KVr>@7w0mH5niF{6e*QV(ym z+_XKLx#>+2zwDfK2i;JtH@(RDF=9hPrwHjXJZmQ-Q>!x=}xNh7tZOARl`dQVsnK`^s;_)8Vy*TAvyoq^9FUkDNIxEJ(CZsvJ~F6?_a*C#afK0{3`l6WizC3a}1KO=cPf32)wctGSs{!jLF zUY5PYuq@b@`XuEwHh{8*I>a1XiC>}j&cigP`s`z>#NSS*8u}1(JK68#?(aP|hq3;65XXoPHQG;3 z2;Gp}A386-#9CsreWaa#e#Tp^bENh1T~q7NYB_6UBv^lgGfQ#((Yby9{E(Ix@yz*B zPjtvs?)_SqZ09%ht-d^4{3d$HICIX2^GwlxNIZ=%nX$YZC)=~c?>_GjV!PYebN4sQ#*kAt>+Oem-tGb;d~mN zjHO;*g#R1JQ1%(I9}<%~kmhPn~i_t&%F1HTyAY`L;w-49!A12~w|ve#b^c2&85PFP}_ ziM4Ob;jYyuc(ay09GpQ8uu41-JV);m6KC09rQdpg|IHl>Z$!sM5kG{vdIs};89H`3 zbLuoO9>;Zv=$+WRj{f3aoJl!~dU9qXh}&Z2vswFx3_C0@9hEUWiQ)Cy+g=~Kt-9uT zat1SsJ$M(JMnCS_tnzEUKPA&@e5Np$FHdLPyp;L=`dg?UZ?HyOJ3S`97RWw)*JsXH z_rWhdOkMwD3-u3uZY_r=el<4dJo^45Sl^6!QG)+9@NPZ-$QmYbXUcNFB70xxRpea5 z=33X7tG07y-7{=qg-QI(nH8Nw72T4&ygo0#3i-&suI%LwAg}fM^7oo3lmB8QU;7wq z=AHAebfI`eVpuI!l)Qr2;xzxmk$seuJ$$ICh7Z&K*Wo`xsY3CiwIsfFJ2A%5lH4iS+52(o?N`eBEq?8Docp?3 z`WY8L#?ju~Wu1M)d!q07`cBmE7R=J06`WVOf<3hp#4c=ZJc;S!Po?CXq;j7DzhIZS fyOL+wFF9B8H~KsWv6RGGiRr#w@ofog0SWvchs=Li literal 0 HcmV?d00001 diff --git a/src/app/rdp/RdpClient.tsx b/src/app/rdp/RdpClient.tsx index 15ca74f9e..cedcd5282 100644 --- a/src/app/rdp/RdpClient.tsx +++ b/src/app/rdp/RdpClient.tsx @@ -35,7 +35,6 @@ declare module "react" { type FormState = { username: string; password: string; - gatewayAddress: string; hostname: string; domain: string; authtoken: string; @@ -59,7 +58,6 @@ export default function RdpClient() { const [form, setForm] = useState({ username: "Administrator", password: "Password123!", - gatewayAddress: "ws://localhost:8082/rdp", hostname: "172.31.3.58:3389", domain: "", authtoken: "pangolin-browser-gateway-dev", @@ -233,7 +231,9 @@ export default function RdpClient() { .withUsername(form.username) .withPassword(form.password) .withDestination(form.hostname) - .withProxyAddress(form.gatewayAddress) + .withProxyAddress( + `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/gateway/rdp` + ) .withServerDomain(form.domain) .withAuthToken(form.authtoken) .withDesktopSize({ @@ -341,15 +341,6 @@ export default function RdpClient() { } /> - - - update("gatewayAddress", e.target.value) - } - /> - {/* Date: Wed, 13 May 2026 21:16:00 -0700 Subject: [PATCH 146/771] Clean up forms a bit --- src/app/rdp/RdpClient.tsx | 24 ++--------------- src/app/ssh/SshClient.tsx | 57 ++++----------------------------------- src/app/vnc/VncClient.tsx | 33 ++++------------------- 3 files changed, 12 insertions(+), 102 deletions(-) diff --git a/src/app/rdp/RdpClient.tsx b/src/app/rdp/RdpClient.tsx index cedcd5282..57be5f919 100644 --- a/src/app/rdp/RdpClient.tsx +++ b/src/app/rdp/RdpClient.tsx @@ -37,7 +37,6 @@ type FormState = { password: string; hostname: string; domain: string; - authtoken: string; kdcProxyUrl: string; pcb: string; desktopWidth: number; @@ -60,7 +59,6 @@ export default function RdpClient() { password: "Password123!", hostname: "172.31.3.58:3389", domain: "", - authtoken: "pangolin-browser-gateway-dev", kdcProxyUrl: "", pcb: "", desktopWidth: 1280, @@ -159,16 +157,6 @@ export default function RdpClient() { return; } - if (form.authtoken === "") { - toast({ - variant: "destructive", - title: "Missing auth token", - description: - "An auth token is required to connect through the gateway" - }); - return; - } - toast({ title: "Connecting...", description: "Connection in progress" @@ -235,7 +223,7 @@ export default function RdpClient() { `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/gateway/rdp` ) .withServerDomain(form.domain) - .withAuthToken(form.authtoken) + .withAuthToken("test-token") .withDesktopSize({ width: form.desktopWidth, height: form.desktopHeight @@ -341,15 +329,7 @@ export default function RdpClient() { } /> - {/* - - update("authtoken", e.target.value) - } - /> - + {/* ({ - gatewayAddress: "ws://localhost:7171/jet/ssh", hostname: "", port: "22", username: "", - password: "", - authToken: "abc123" + password: "" }); const [connected, setConnected] = useState(false); @@ -122,7 +118,8 @@ export default function SshClient() { setError(null); setConnecting(true); - const url = new URL(form.gatewayAddress); + const proxyAddress = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/gateway/ssh`; + const url = new URL(proxyAddress); // Pass connection parameters as query params so the proxy can route // before any application-level framing is needed. url.searchParams.set("host", form.hostname); @@ -130,7 +127,7 @@ export default function SshClient() { url.searchParams.set("username", form.username); // Auth token is sent as a query param; the proxy validates it before // forwarding any data. - url.searchParams.set("authToken", form.authToken); + url.searchParams.set("authToken", "test-token"); const ws = new WebSocket(url.toString(), ["ssh"]); wsRef.current = ws; @@ -195,27 +192,6 @@ export default function SshClient() { {!connected && (
-
- - - setForm({ - ...form, - gatewayAddress: e.target.value - }) - } - placeholder="ws://localhost:7171/jet/ssh" - className="bg-neutral-800 border-neutral-700 text-white" - /> -
-
- -
- - - setForm({ - ...form, - authToken: e.target.value - }) - } - className="bg-neutral-800 border-neutral-700 text-white" - /> -
{error &&

{error}

} @@ -320,10 +276,7 @@ export default function SshClient() { From 795a3d351e533b4c1568a825491b02ed56d86369 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 May 2026 21:16:40 -0700 Subject: [PATCH 147/771] Reinstall packages --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 8d6fbdffc..55c77c326 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1473,7 +1473,7 @@ "node_modules/@devolutions/iron-remote-desktop-rdp": { "version": "0.0.0", "resolved": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.0.tgz", - "integrity": "sha512-qkpqYOMmSU6jIdDKOWpnLL7pb0sA/Y7Yjm4wI0/VbBIRfZPH8WdKxDU5DNp8jFF60l1zbcSJeRIq5yIAvws3Hw==" + "integrity": "sha512-O0YVpOJDwUzekH3N2QKj+48WP+56wI0sj4VmaJkGoW5XgyAj2ONn2k3i+vk17Eavx+Vg6vAg3lwYRAOK4kKIDQ==" }, "node_modules/@dotenvx/dotenvx": { "version": "1.54.1", From 7d922ac95f82e87fb945a9d961476b0d20a158e7 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 May 2026 21:48:07 -0700 Subject: [PATCH 148/771] Add internal api get for proxy information --- server/routers/internal.ts | 2 + server/routers/resource/getBrowserTarget.ts | 76 ++++++++++++++++++ server/routers/resource/index.ts | 1 + src/app/rdp/RdpClient.tsx | 42 ++++++---- src/app/rdp/page.tsx | 24 +++++- src/app/ssh/SshClient.tsx | 87 ++++++++------------- src/app/ssh/page.tsx | 24 +++++- src/app/vnc/VncClient.tsx | 55 ++++++------- src/app/vnc/page.tsx | 24 +++++- 9 files changed, 229 insertions(+), 106 deletions(-) create mode 100644 server/routers/resource/getBrowserTarget.ts diff --git a/server/routers/internal.ts b/server/routers/internal.ts index 2fa5239cc..3caff4864 100644 --- a/server/routers/internal.ts +++ b/server/routers/internal.ts @@ -42,6 +42,8 @@ internalRouter.get("/idp", idp.listIdps); internalRouter.get("/idp/:idpId", idp.getIdp); +internalRouter.get("/resource/browser-target", resource.getBrowserTarget); + // Gerbil routes const gerbilRouter = Router(); internalRouter.use("/gerbil", gerbilRouter); diff --git a/server/routers/resource/getBrowserTarget.ts b/server/routers/resource/getBrowserTarget.ts new file mode 100644 index 000000000..b69de7521 --- /dev/null +++ b/server/routers/resource/getBrowserTarget.ts @@ -0,0 +1,76 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { resources, targets } from "@server/db"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; + +const getBrowserTargetSchema = z + .object({ + fullDomain: z.string().min(1, "fullDomain is required") + }) + .strict(); + +export type GetBrowserTargetResponse = { + ip: string; + port: number; +}; + +export async function getBrowserTarget( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsed = getBrowserTargetSchema.safeParse(req.query); + if (!parsed.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsed.error).toString() + ) + ); + } + + const { fullDomain } = parsed.data; + + const [row] = await db + .select({ + ip: targets.ip, + port: targets.port + }) + .from(targets) + .innerJoin(resources, eq(targets.resourceId, resources.resourceId)) + .where(eq(resources.fullDomain, fullDomain)) + .limit(1); + + if (!row) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "No resource found for this domain" + ) + ); + } + + return response(res, { + data: { ip: row.ip, port: row.port }, + success: true, + error: false, + message: "Browser target retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "An error occurred while retrieving the browser target" + ) + ); + } +} diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index 6a259d7fe..d8ff4dba9 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -33,3 +33,4 @@ export * from "./removeUserFromResource"; export * from "./listAllResourceNames"; export * from "./removeEmailFromResourceWhitelist"; export * from "./getStatusHistory"; +export * from "./getBrowserTarget"; diff --git a/src/app/rdp/RdpClient.tsx b/src/app/rdp/RdpClient.tsx index 57be5f919..5b61e527d 100644 --- a/src/app/rdp/RdpClient.tsx +++ b/src/app/rdp/RdpClient.tsx @@ -32,10 +32,14 @@ declare module "react" { } } +type Target = { + ip: string; + port: number; +}; + type FormState = { username: string; password: string; - hostname: string; domain: string; kdcProxyUrl: string; pcb: string; @@ -53,11 +57,16 @@ const isIronError = (error: unknown): error is IronError => { ); }; -export default function RdpClient() { +export default function RdpClient({ + target, + error +}: { + target: Target | null; + error: string | null; +}) { const [form, setForm] = useState({ - username: "Administrator", - password: "Password123!", - hostname: "172.31.3.58:3389", + username: "", + password: "", domain: "", kdcProxyUrl: "", pcb: "", @@ -214,11 +223,15 @@ export default function RdpClient() { ); } + const destination = target + ? `${target.ip}:${target.port}` + : ""; + const builder = userInteraction .configBuilder() .withUsername(form.username) .withPassword(form.password) - .withDestination(form.hostname) + .withDestination(destination) .withProxyAddress( `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/gateway/rdp` ) @@ -283,6 +296,14 @@ export default function RdpClient() { setCursorOverrideActive((v) => !v); }; + if (error) { + return ( +
+

{error}

+
+ ); + } + return (
{showLogin && ( @@ -292,15 +313,6 @@ export default function RdpClient() {
- - - update("hostname", e.target.value) - } - /> - ; +export default async function RdpPage() { + const headersList = await headers(); + const host = headersList.get("host") || ""; + const hostname = host.split(":")[0]; + + let target: { ip: string; port: number } | null = null; + let error: string | null = null; + + try { + const res = await internal.get>( + `/resource/browser-target?fullDomain=${encodeURIComponent(hostname)}` + ); + target = res.data.data; + } catch { + error = "No resource found for this domain"; + } + + return ; } diff --git a/src/app/ssh/SshClient.tsx b/src/app/ssh/SshClient.tsx index e5ac8227b..0c96f1355 100644 --- a/src/app/ssh/SshClient.tsx +++ b/src/app/ssh/SshClient.tsx @@ -6,24 +6,31 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +type Target = { + ip: string; + port: number; +}; + type FormState = { - hostname: string; - port: string; username: string; password: string; }; -export default function SshClient() { +export default function SshClient({ + target, + error +}: { + target: Target | null; + error: string | null; +}) { const [form, setForm] = useState({ - hostname: "", - port: "22", username: "", password: "" }); const [connected, setConnected] = useState(false); const [connecting, setConnecting] = useState(false); - const [error, setError] = useState(null); + const [connectError, setConnectError] = useState(null); const terminalRef = useRef(null); const xtermRef = useRef(null); @@ -115,18 +122,14 @@ export default function SshClient() { }, []); function connect() { - setError(null); + setConnectError(null); setConnecting(true); const proxyAddress = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}/gateway/ssh`; const url = new URL(proxyAddress); - // Pass connection parameters as query params so the proxy can route - // before any application-level framing is needed. - url.searchParams.set("host", form.hostname); - url.searchParams.set("port", form.port); + url.searchParams.set("host", target?.ip ?? ""); + url.searchParams.set("port", String(target?.port ?? 22)); url.searchParams.set("username", form.username); - // Auth token is sent as a query param; the proxy validates it before - // forwarding any data. url.searchParams.set("authToken", "test-token"); const ws = new WebSocket(url.toString(), ["ssh"]); @@ -166,7 +169,7 @@ export default function SshClient() { ws.onerror = () => { setConnecting(false); setConnected(false); - setError("WebSocket connection failed"); + setConnectError("WebSocket connection failed"); }; ws.onclose = (evt) => { @@ -185,6 +188,14 @@ export default function SshClient() { setConnected(false); } + if (error) { + return ( +
+

{error}

+
+ ); + } + return (

SSH Terminal

@@ -192,43 +203,7 @@ export default function SshClient() { {!connected && (
-
- - - setForm({ - ...form, - hostname: e.target.value - }) - } - placeholder="192.168.1.1" - className="bg-neutral-800 border-neutral-700 text-white" - /> -
- -
- - - setForm({ ...form, port: e.target.value }) - } - placeholder="22" - className="bg-neutral-800 border-neutral-700 text-white" - /> -
- -
+
-
+
- {error &&

{error}

} + {connectError && ( +

{connectError}

+ )} + ); + }, + cell: ({ row }) => + }, + { + accessorKey: "actions", + enableHiding: false, + header: () => { + return {t("actions")}; + }, + cell: ({ row }) => ( + + + + + + {t("edit")} + {}}> + + {t("delete")} + + + + + ) + } + ], + [searchParams, t] + ); + + async function deleteLabel() { + // ... + } + + return ( + <> + {selectedLabel && ( + { + setIsDeleteModalOpen(val); + setSelectedLabel(null); + }} + dialog={ +
+

{t("resourceQuestionRemove")}

+

{t("resourceMessageRemove")}

+
+ } + buttonText={t("resourceDeleteConfirm")} + onConfirm={async () => {}} + string={selectedLabel.name} + title={t("resourceDelete")} + /> + )} + + + ); +} + +type EditLabelCellProps = { + label: LabelRow; +}; + +function EditLabelCell({ label }: EditLabelCellProps) { + const t = useTranslations(); + + return ( +
+
+ + {label.name} + + {/* */} +
+ ); +} diff --git a/src/components/labels-selector.tsx b/src/components/labels-selector.tsx index 64a80b26a..1f7714d07 100644 --- a/src/components/labels-selector.tsx +++ b/src/components/labels-selector.tsx @@ -1,6 +1,15 @@ +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; import { orgQueries } from "@app/lib/queries"; +import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; import { useQuery } from "@tanstack/react-query"; -import { useActionState, useMemo, useState, useTransition } from "react"; +import type { AxiosResponse } from "axios"; +import { useTranslations } from "next-intl"; +import { useActionState, useMemo, useState } from "react"; +import { useDebounce } from "use-debounce"; +import { Button } from "./ui/button"; +import { Checkbox } from "./ui/checkbox"; import { Command, CommandEmpty, @@ -9,11 +18,6 @@ import { CommandItem, CommandList } from "./ui/command"; -import { Checkbox } from "./ui/checkbox"; -import { useTranslations } from "next-intl"; -import { useDebounce } from "use-debounce"; -import { type Selectedsite, SiteOnlineStatus } from "./site-selector"; -import { Button } from "./ui/button"; import { Select, SelectContent, @@ -21,11 +25,6 @@ import { SelectTrigger, SelectValue } from "./ui/select"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import type { CreateOrEditLabelResponse } from "@server/routers/labels/types"; -import type { AxiosResponse } from "axios"; -import { toast } from "@app/hooks/useToast"; export type SelectedLabel = { name: string; From 9a88394efe9523161f04809bc97a54f83d60c1c9 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 14 May 2026 21:17:58 +0200 Subject: [PATCH 154/771] =?UTF-8?q?=F0=9F=9B=82=20gate=20label=20endpoints?= =?UTF-8?q?=20behing=20subscription?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/private/middlewares/verifySubscription.ts | 2 +- server/private/routers/external.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/server/private/middlewares/verifySubscription.ts b/server/private/middlewares/verifySubscription.ts index 27bd25dfe..92d5d9cfe 100644 --- a/server/private/middlewares/verifySubscription.ts +++ b/server/private/middlewares/verifySubscription.ts @@ -25,7 +25,7 @@ export function verifyValidSubscription(tiers: Tier[]) { next: NextFunction ): Promise { try { - if (build != "saas") { + if (build !== "saas") { return next(); } diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index 8745dffdf..481e3a302 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -737,6 +737,7 @@ authenticated.get( "/org/:orgId/labels", verifyValidLicense, verifyOrgAccess, + verifyValidSubscription(tierMatrix.labels), verifyUserHasAction(ActionsEnum.listOrgLabels), labels.listOrgLabels ); @@ -745,6 +746,7 @@ authenticated.post( "/org/:orgId/labels", verifyValidLicense, verifyOrgAccess, + verifyValidSubscription(tierMatrix.labels), verifyUserHasAction(ActionsEnum.createOrgLabel), labels.createOrgLabel ); @@ -753,6 +755,7 @@ authenticated.patch( "/org/:orgId/label/:labelId", verifyValidLicense, verifyOrgAccess, + verifyValidSubscription(tierMatrix.labels), verifyUserHasAction(ActionsEnum.updateOrgLabel), labels.updateOrgLabel ); @@ -769,6 +772,7 @@ authenticated.put( "/org/:orgId/label/:labelId/attach", verifyValidLicense, verifyOrgAccess, + verifyValidSubscription(tierMatrix.labels), verifyUserHasAction(ActionsEnum.attachLabelToItem), labels.attachLabelToItem ); @@ -777,6 +781,7 @@ authenticated.put( "/org/:orgId/label/:labelId/detach", verifyValidLicense, verifyOrgAccess, + verifyValidSubscription(tierMatrix.labels), verifyUserHasAction(ActionsEnum.detachLabelFromItem), labels.detachLabelFromItem ); From eac36ee442195919cbca1a46d2792902468bd277 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 14 May 2026 22:15:43 +0200 Subject: [PATCH 155/771] =?UTF-8?q?=E2=9C=A8=20delete=20label?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 5 +++++ src/components/OrgLabelsTable.tsx | 35 ++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 07482bf80..e42662968 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -255,6 +255,11 @@ "resourceGoTo": "Go to Resource", "resourceDelete": "Delete Resource", "resourceDeleteConfirm": "Confirm Delete Resource", + "labelDelete": "Delete Label", + "labelDeleteConfirm": "Confirm Delete Label", + "labelErrorDelete": "Failed to delete label", + "labelMessageRemove": "This action is permanent. All sites, resources, and clients tagged with this label will be untagged.", + "labelQuestionRemove": "Are you sure you want to remove the label from the organization?", "visibility": "Visibility", "enabled": "Enabled", "disabled": "Disabled", diff --git a/src/components/OrgLabelsTable.tsx b/src/components/OrgLabelsTable.tsx index bc06b9101..0c6349a61 100644 --- a/src/components/OrgLabelsTable.tsx +++ b/src/components/OrgLabelsTable.tsx @@ -141,7 +141,12 @@ export default function OrgLabelsTable({ {t("edit")} - {}}> + { + setSelectedLabel(row.original); + setIsDeleteModalOpen(true); + }} + > {t("delete")} @@ -154,8 +159,22 @@ export default function OrgLabelsTable({ [searchParams, t] ); - async function deleteLabel() { - // ... + function deleteLabel(label: LabelRow) { + startRefreshTransition(async () => { + await api + .delete(`/org/${orgId}/label/${label.labelId}`) + .catch((e) => { + toast({ + variant: "destructive", + title: t("labelErrorDelete"), + description: formatAxiosError(e, t("labelErrorDelete")) + }); + }) + .then(() => { + router.refresh(); + setIsDeleteModalOpen(false); + }); + }); } return ( @@ -169,14 +188,14 @@ export default function OrgLabelsTable({ }} dialog={
-

{t("resourceQuestionRemove")}

-

{t("resourceMessageRemove")}

+

{t("labelQuestionRemove")}

+

{t("labelMessageRemove")}

} - buttonText={t("resourceDeleteConfirm")} - onConfirm={async () => {}} + buttonText={t("labelDeleteConfirm")} + onConfirm={async () => deleteLabel(selectedLabel)} string={selectedLabel.name} - title={t("resourceDelete")} + title={t("labelDelete")} /> )} Date: Thu, 14 May 2026 22:42:01 +0200 Subject: [PATCH 156/771] =?UTF-8?q?=F0=9F=9A=A7=20=20wip:=20create=20label?= =?UTF-8?q?=20dialog?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ClientResourcesTable.tsx | 4 +- .../CreateInternalResourceDialog.tsx | 88 +++++++++---------- src/components/CreateOrgLabelDialog.tsx | 74 ++++++++++++++++ src/components/OrgLabelsTable.tsx | 79 ++++------------- 4 files changed, 137 insertions(+), 108 deletions(-) create mode 100644 src/components/CreateOrgLabelDialog.tsx diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index 7ebef5795..156cc7f41 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -204,8 +204,8 @@ export default function ClientResourcesTable({ siteId: number ) => { try { - await api.delete(`/site-resource/${resourceId}`).then(() => { - startTransition(() => { + startTransition(async () => { + await api.delete(`/site-resource/${resourceId}`).then(() => { router.refresh(); setIsDeleteModalOpen(false); }); diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index 4d2bc0916..dc1dacd4b 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -16,7 +16,7 @@ import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { useTranslations } from "next-intl"; -import { useState } from "react"; +import { useState, useTransition } from "react"; import { cleanForFQDN, InternalResourceForm, @@ -39,30 +39,30 @@ export default function CreateInternalResourceDialog({ }: CreateInternalResourceDialogProps) { const t = useTranslations(); const api = createApiClient(useEnvContext()); - const [isSubmitting, setIsSubmitting] = useState(false); const [isHttpModeDisabled, setIsHttpModeDisabled] = useState(false); + const [isSubmitting, startTransition] = useTransition(); - async function handleSubmit(values: InternalResourceFormValues) { - setIsSubmitting(true); - try { - let data = { ...values }; - if ( - (data.mode === "host" || data.mode === "http") && - isHostname(data.destination) - ) { - const currentAlias = data.alias?.trim() || ""; - if (!currentAlias) { - let aliasValue = data.destination; - if (data.destination.toLowerCase() === "localhost") { - aliasValue = `${cleanForFQDN(data.name)}.internal`; + function handleSubmit(values: InternalResourceFormValues) { + startTransition(async () => { + try { + let data = { ...values }; + if ( + (data.mode === "host" || data.mode === "http") && + isHostname(data.destination) + ) { + const currentAlias = data.alias?.trim() || ""; + if (!currentAlias) { + let aliasValue = data.destination; + if (data.destination.toLowerCase() === "localhost") { + aliasValue = `${cleanForFQDN(data.name)}.internal`; + } + data = { ...data, alias: aliasValue }; } - data = { ...data, alias: aliasValue }; } - } - await api.put>( - `/org/${orgId}/site-resource`, - { + await api.put< + AxiosResponse<{ data: { siteResourceId: number } }> + >(`/org/${orgId}/site-resource`, { name: data.name, siteIds: data.siteIds, mode: data.mode, @@ -106,32 +106,30 @@ export default function CreateInternalResourceDialog({ clientIds: data.clients ? data.clients.map((c) => parseInt(c.id)) : [] - } - ); + }); - toast({ - title: t("createInternalResourceDialogSuccess"), - description: t( - "createInternalResourceDialogInternalResourceCreatedSuccessfully" - ), - variant: "default" - }); - setOpen(false); - onSuccess?.(); - } catch (error) { - toast({ - title: t("createInternalResourceDialogError"), - description: formatAxiosError( - error, - t( - "createInternalResourceDialogFailedToCreateInternalResource" - ) - ), - variant: "destructive" - }); - } finally { - setIsSubmitting(false); - } + toast({ + title: t("createInternalResourceDialogSuccess"), + description: t( + "createInternalResourceDialogInternalResourceCreatedSuccessfully" + ), + variant: "default" + }); + setOpen(false); + onSuccess?.(); + } catch (error) { + toast({ + title: t("createInternalResourceDialogError"), + description: formatAxiosError( + error, + t( + "createInternalResourceDialogFailedToCreateInternalResource" + ) + ), + variant: "destructive" + }); + } + }); } return ( diff --git a/src/components/CreateOrgLabelDialog.tsx b/src/components/CreateOrgLabelDialog.tsx new file mode 100644 index 000000000..f06f979ca --- /dev/null +++ b/src/components/CreateOrgLabelDialog.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { createApiClient } from "@app/lib/api"; +import { useTranslations } from "next-intl"; +import { useState, useTransition } from "react"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "./Credenza"; +import { Button } from "./ui/button"; + +export type CreateOrgLabelDialogProps = { + open: boolean; + setOpen: (val: boolean) => void; + orgId: string; + onSuccess?: () => void; +}; + +export function CreateOrgLabelDialog({ + open, + setOpen, + orgId, + onSuccess +}: CreateOrgLabelDialogProps) { + const t = useTranslations(); + const api = createApiClient(useEnvContext()); + const [isSubmitting, startTransition] = useTransition(); + + return ( + + + + + {t("createInternalResourceDialogCreateClientResource")} + + + {t( + "createInternalResourceDialogCreateClientResourceDescription" + )} + + + + <> + + + + + + + + + + ); +} diff --git a/src/components/OrgLabelsTable.tsx b/src/components/OrgLabelsTable.tsx index 0c6349a61..bcb6f59ea 100644 --- a/src/components/OrgLabelsTable.tsx +++ b/src/components/OrgLabelsTable.tsx @@ -53,7 +53,7 @@ export default function OrgLabelsTable({ rowCount }: OrgLabelsTableProps) { const router = useRouter(); - const pathname = usePathname(); + const { navigate: filter, isNavigating: isFiltering, @@ -63,13 +63,13 @@ export default function OrgLabelsTable({ const [selectedLabel, setSelectedLabel] = useState(null); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [isRefreshing, startRefreshTransition] = useTransition(); + const [isRefreshing, startTransition] = useTransition(); const api = createApiClient(useEnvContext()); const t = useTranslations(); function refreshData() { - startRefreshTransition(async () => { + startTransition(async () => { try { router.refresh(); } catch { @@ -82,11 +82,6 @@ export default function OrgLabelsTable({ }); } - function toggleSort(column: string) { - const newSearch = getNextSortOrder(column, searchParams); - filter({ searchParams: newSearch }); - } - const handlePaginationChange = (newPage: PaginationState) => { searchParams.set("page", (newPage.pageIndex + 1).toString()); searchParams.set("pageSize", newPage.pageSize.toString()); @@ -105,25 +100,21 @@ export default function OrgLabelsTable({ accessorKey: "name", enableHiding: false, header: () => { - const nameOrder = getSortDirection("name", searchParams); - const Icon = - nameOrder === "asc" - ? ArrowDown01Icon - : nameOrder === "desc" - ? ArrowUp10Icon - : ChevronsUpDownIcon; - return ( - - ); + return {t("name")}; }, - cell: ({ row }) => + cell: ({ row }) => ( +
+
+ + {row.original.name} +
+ ) }, { accessorKey: "actions", @@ -160,7 +151,7 @@ export default function OrgLabelsTable({ ); function deleteLabel(label: LabelRow) { - startRefreshTransition(async () => { + startTransition(async () => { await api .delete(`/org/${orgId}/label/${label.labelId}`) .catch((e) => { @@ -214,37 +205,3 @@ export default function OrgLabelsTable({ ); } - -type EditLabelCellProps = { - label: LabelRow; -}; - -function EditLabelCell({ label }: EditLabelCellProps) { - const t = useTranslations(); - - return ( -
-
- - {label.name} - - {/* */} -
- ); -} From 68d7b0a41688a1fccaf33375480184f6a119d7dc Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 14 May 2026 22:43:29 +0200 Subject: [PATCH 157/771] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20label?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/CreateEditOrgLabelForm.tsx | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 src/components/CreateEditOrgLabelForm.tsx diff --git a/src/components/CreateEditOrgLabelForm.tsx b/src/components/CreateEditOrgLabelForm.tsx new file mode 100644 index 000000000..117131aca --- /dev/null +++ b/src/components/CreateEditOrgLabelForm.tsx @@ -0,0 +1,7 @@ +"use client"; + +export type CreateEditOrgLabelProps = {}; + +export function CreateEditOrgLabel({}: CreateEditOrgLabelProps) { + return <>; +} From 18d380ce30f3331b0b20c83b019e9f40b3eda5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Sch=C3=A4fer?= Date: Fri, 15 May 2026 18:35:58 +0000 Subject: [PATCH 158/771] fix(security): normalize request parameters and update dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marc Schäfer --- drizzle.sqlite.config.ts | 2 +- install/go.mod | 4 +- install/go.sum | 8 +- next.config.ts | 7 +- package-lock.json | 5596 +++++++---------- package.json | 102 +- server/index.ts | 2 +- server/lib/requestParams.ts | 11 + .../integration/verifyAccessTokenAccess.ts | 9 +- .../integration/verifyApiKeyApiKeyAccess.ts | 7 +- .../integration/verifyApiKeyDomainAccess.ts | 13 +- .../integration/verifyApiKeyIdpAccess.ts | 11 +- .../integration/verifyApiKeyOrgAccess.ts | 5 +- .../verifyApiKeySiteResourceAccess.ts | 6 +- .../integration/verifyApiKeyTargetAccess.ts | 6 +- server/middlewares/verifyAccessTokenAccess.ts | 11 +- server/middlewares/verifyApiKeyAccess.ts | 27 +- server/middlewares/verifyDomainAccess.ts | 11 +- server/middlewares/verifyLimits.ts | 6 +- server/middlewares/verifyOrgAccess.ts | 3 +- .../verifySiteProvisioningKeyAccess.ts | 19 +- server/middlewares/verifyTargetAccess.ts | 4 +- server/middlewares/verifyUserIsOrgOwner.ts | 3 +- .../middlewares/verifyCertificateAccess.ts | 45 +- server/private/middlewares/verifyIdpAccess.ts | 11 +- .../middlewares/verifyRemoteExitNodeAccess.ts | 18 +- .../generatedLicense/generateNewLicense.ts | 50 +- .../generatedLicense/listGeneratedLicenses.ts | 3 +- server/routers/auth/securityKey.ts | 8 +- server/routers/domain/getDomain.ts | 1 - server/routers/resource/getUserResources.ts | 9 +- .../proxy/[niceId]/authentication/page.tsx | 2 +- src/components/ui/chart.tsx | 73 +- src/types/css.d.ts | 1 + tsconfig.enterprise.json | 57 +- tsconfig.oss.json | 57 +- tsconfig.saas.json | 57 +- 37 files changed, 2656 insertions(+), 3609 deletions(-) create mode 100644 server/lib/requestParams.ts create mode 100644 src/types/css.d.ts diff --git a/drizzle.sqlite.config.ts b/drizzle.sqlite.config.ts index d8344f942..86eebda05 100644 --- a/drizzle.sqlite.config.ts +++ b/drizzle.sqlite.config.ts @@ -1,4 +1,4 @@ -import { APP_PATH } from "@server/lib/consts"; +import { APP_PATH } from "./server/lib/consts"; import { defineConfig } from "drizzle-kit"; import path from "path"; diff --git a/install/go.mod b/install/go.mod index 878ab39a2..7f50d4e9e 100644 --- a/install/go.mod +++ b/install/go.mod @@ -5,7 +5,7 @@ go 1.25.0 require ( github.com/charmbracelet/huh v1.0.0 github.com/charmbracelet/lipgloss v1.1.0 - golang.org/x/term v0.42.0 + golang.org/x/term v0.43.0 gopkg.in/yaml.v3 v3.0.1 ) @@ -33,6 +33,6 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.43.0 // indirect + golang.org/x/sys v0.44.0 // indirect golang.org/x/text v0.23.0 // indirect ) diff --git a/install/go.sum b/install/go.sum index 8730d4268..7ed97929f 100644 --- a/install/go.sum +++ b/install/go.sum @@ -69,10 +69,10 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= -golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= -golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/next.config.ts b/next.config.ts index 630a3416f..078a459d6 100644 --- a/next.config.ts +++ b/next.config.ts @@ -5,12 +5,7 @@ const withNextIntl = createNextIntlPlugin(); const nextConfig: NextConfig = { reactStrictMode: false, - eslint: { - ignoreDuringBuilds: true - }, - experimental: { - reactCompiler: true - }, + reactCompiler: true, output: "standalone" }; diff --git a/package-lock.json b/package-lock.json index 8c241554a..69ad91121 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,10 @@ "version": "0.0.0", "license": "SEE LICENSE IN LICENSE AND README.md", "dependencies": { - "@asteasolutions/zod-to-openapi": "8.4.1", - "@aws-sdk/client-s3": "3.1011.0", - "@faker-js/faker": "10.3.0", - "@headlessui/react": "2.2.9", + "@asteasolutions/zod-to-openapi": "8.5.0", + "@aws-sdk/client-s3": "3.1047.0", + "@faker-js/faker": "10.4.0", + "@headlessui/react": "2.2.10", "@hookform/resolvers": "5.2.2", "@monaco-editor/react": "4.7.0", "@node-rs/argon2": "2.0.2", @@ -36,16 +36,17 @@ "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-tooltip": "1.2.8", - "@react-email/components": "1.0.8", - "@react-email/render": "2.0.4", - "@react-email/tailwind": "2.0.5", + "@react-email/body": "0.3.0", + "@react-email/components": "1.0.12", + "@react-email/render": "2.0.8", + "@react-email/tailwind": "2.0.7", "@simplewebauthn/browser": "13.3.0", "@simplewebauthn/server": "13.3.0", "@tailwindcss/forms": "0.5.11", - "@tanstack/react-query": "5.90.21", + "@tanstack/react-query": "5.100.10", "@tanstack/react-table": "8.21.3", "arctic": "3.7.0", - "axios": "1.15.0", + "axios": "1.16.1", "better-sqlite3": "11.9.1", "canvas-confetti": "1.9.4", "class-variance-authority": "0.7.1", @@ -57,76 +58,76 @@ "d3": "7.9.0", "drizzle-orm": "0.45.2", "express": "5.2.1", - "express-rate-limit": "8.3.0", + "express-rate-limit": "8.5.2", "glob": "13.0.6", "helmet": "8.1.0", "http-errors": "2.0.1", "input-otp": "1.4.2", - "ioredis": "5.10.0", + "ioredis": "5.10.1", "jmespath": "0.16.0", "js-yaml": "4.1.1", "jsonwebtoken": "9.0.3", "lucide-react": "0.577.0", - "maxmind": "5.0.5", + "maxmind": "5.0.6", "moment": "2.30.1", - "next": "15.5.15", - "next-intl": "4.8.3", + "next": "16.2.6", + "next-intl": "4.12.0", "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", - "nodemailer": "8.0.5", + "nodemailer": "8.0.7", "oslo": "1.2.1", "pg": "8.20.0", - "posthog-node": "5.28.0", + "posthog-node": "5.34.1", "qrcode.react": "4.2.0", - "react": "19.2.4", + "react": "19.2.6", "react-day-picker": "9.14.0", - "react-dom": "19.2.4", + "react-dom": "19.2.6", "react-easy-sort": "1.8.0", - "react-hook-form": "7.71.2", + "react-hook-form": "7.75.0", "react-icons": "5.6.0", - "recharts": "2.15.4", + "recharts": "3.8.1", "reodotdev": "1.1.0", - "resend": "6.9.2", - "semver": "7.7.4", + "resend": "6.12.3", + "semver": "7.8.0", "sshpk": "1.18.0", "stripe": "20.4.1", "swagger-ui-express": "5.0.1", - "tailwind-merge": "3.5.0", + "tailwind-merge": "3.6.0", "topojson-client": "3.1.0", "tw-animate-css": "1.4.0", - "use-debounce": "10.1.0", - "uuid": "13.0.0", + "use-debounce": "10.1.1", + "uuid": "14.0.0", "vaul": "1.1.2", "visionscarto-world-atlas": "1.0.0", "winston": "3.19.0", "winston-daily-rotate-file": "5.0.0", - "ws": "8.19.0", - "yaml": "2.8.3", + "ws": "8.20.1", + "yaml": "2.9.0", "yargs": "18.0.0", - "zod": "4.3.6", + "zod": "4.4.3", "zod-validation-error": "5.0.0" }, "devDependencies": { - "@dotenvx/dotenvx": "1.54.1", + "@dotenvx/dotenvx": "1.66.0", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@react-email/preview-server": "5.2.10", - "@tailwindcss/postcss": "4.2.2", - "@tanstack/react-query-devtools": "5.91.3", + "@react-email/ui": "^6.1.4", + "@tailwindcss/postcss": "4.3.0", + "@tanstack/react-query-devtools": "5.100.10", "@types/better-sqlite3": "7.6.13", "@types/cookie-parser": "1.4.10", "@types/cors": "2.8.19", "@types/crypto-js": "4.2.2", "@types/d3": "7.4.3", "@types/express": "5.0.6", - "@types/express-session": "1.18.2", + "@types/express-session": "1.19.0", "@types/jmespath": "0.15.2", "@types/js-yaml": "4.0.9", "@types/jsonwebtoken": "9.0.10", - "@types/node": "25.3.5", - "@types/nodemailer": "7.0.11", + "@types/node": "25.8.0", + "@types/nodemailer": "8.0.0", "@types/nprogress": "0.2.3", - "@types/pg": "8.18.0", + "@types/pg": "8.20.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@types/semver": "7.7.1", @@ -137,18 +138,18 @@ "@types/yargs": "17.0.35", "babel-plugin-react-compiler": "1.0.0", "drizzle-kit": "0.31.10", - "esbuild": "0.27.4", - "esbuild-node-externals": "1.20.1", - "eslint": "10.0.3", - "eslint-config-next": "16.1.7", - "postcss": "8.5.8", - "prettier": "3.8.1", - "react-email": "5.2.10", - "tailwindcss": "4.2.2", - "tsc-alias": "1.8.16", - "tsx": "4.21.0", - "typescript": "5.9.3", - "typescript-eslint": "8.56.1" + "esbuild": "0.28.0", + "esbuild-node-externals": "1.22.0", + "eslint": "10.3.0", + "eslint-config-next": "16.2.6", + "postcss": "8.5.14", + "prettier": "3.8.3", + "react-email": "6.1.4", + "tailwindcss": "4.3.0", + "tsc-alias": "1.8.17", + "tsx": "4.22.0", + "typescript": "6.0.3", + "typescript-eslint": "8.59.3" } }, "node_modules/@alloc/quick-lru": { @@ -165,9 +166,9 @@ } }, "node_modules/@asteasolutions/zod-to-openapi": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.4.1.tgz", - "integrity": "sha512-WmJUsFINbnWxGvHSd16aOjgKf+5GsfdxruO2YDLcgplsidakCauik1lhlk83YDH06265Yd1XtUyF24o09uygpw==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.5.0.tgz", + "integrity": "sha512-SABbKiObg5dLRiTFnqiW1WWwGcg1BJfmHtT2asIBnBHg6Smy/Ms2KHc650+JI4Hw7lSkdiNebEGXpwoxfben8Q==", "license": "MIT", "dependencies": { "openapi3-ts": "^4.1.2" @@ -215,44 +216,6 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", @@ -268,44 +231,6 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-crypto/sha256-js": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", @@ -340,104 +265,37 @@ "tslib": "^2.6.2" } }, - "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", - "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1011.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1011.0.tgz", - "integrity": "sha512-jY7CGX+vfM/DSi4K8UwaZKoXnhqchmAbKFB1kIuHMfPPqW7l3jC/fUVDb95/njMsB2ymYOTusZEzoCTeUB/4qA==", + "version": "3.1047.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1047.0.tgz", + "integrity": "sha512-gk8g31eqvgf7eLCpkVjWs9KL7gYgkomt3FT2o9tbIe6goYrBheN2lHxhCsTn1zFYbt7EwrZXTGkQPIQNIN0c5w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-node": "^3.972.21", - "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", - "@aws-sdk/middleware-expect-continue": "^3.972.8", - "@aws-sdk/middleware-flexible-checksums": "^3.974.0", - "@aws-sdk/middleware-host-header": "^3.972.8", - "@aws-sdk/middleware-location-constraint": "^3.972.8", - "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-sdk-s3": "^3.972.20", - "@aws-sdk/middleware-ssec": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", - "@aws-sdk/signature-v4-multi-region": "^3.996.8", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", - "@smithy/eventstream-serde-browser": "^4.2.12", - "@smithy/eventstream-serde-config-resolver": "^4.3.12", - "@smithy/eventstream-serde-node": "^4.2.12", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/hash-blob-browser": "^4.2.13", - "@smithy/hash-node": "^4.2.12", - "@smithy/hash-stream-node": "^4.2.12", - "@smithy/invalid-dependency": "^4.2.12", - "@smithy/md5-js": "^4.2.12", - "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.12", - "@smithy/util-stream": "^4.5.19", - "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.13", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/credential-provider-node": "^3.972.41", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.12", + "@aws-sdk/middleware-expect-continue": "^3.972.11", + "@aws-sdk/middleware-flexible-checksums": "^3.974.18", + "@aws-sdk/middleware-host-header": "^3.972.11", + "@aws-sdk/middleware-location-constraint": "^3.972.10", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.12", + "@aws-sdk/middleware-sdk-s3": "^3.972.39", + "@aws-sdk/middleware-ssec": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.40", + "@aws-sdk/region-config-resolver": "^3.972.14", + "@aws-sdk/signature-v4-multi-region": "^3.996.26", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.9", + "@aws-sdk/util-user-agent-browser": "^3.972.11", + "@aws-sdk/util-user-agent-node": "^3.973.26", + "@smithy/core": "^3.24.1", + "@smithy/fetch-http-handler": "^5.4.1", + "@smithy/node-http-handler": "^4.7.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -445,23 +303,16 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", - "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", + "version": "3.974.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.10.tgz", + "integrity": "sha512-ZGFFlYynBR78Y/F8b/7y4i4sgW/iGwJSjoM7AZo5Et6vyr4/L0bunN+uzKMsvecCZyqcPp4RRK7Rs17l0kMujg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/xml-builder": "^3.972.11", - "@smithy/core": "^3.23.11", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-utf8": "^4.2.2", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.24", + "@smithy/core": "^3.24.1", + "@smithy/signature-v4": "^5.4.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -469,12 +320,12 @@ } }, "node_modules/@aws-sdk/crc64-nvme": { - "version": "3.972.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.5.tgz", - "integrity": "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.8.tgz", + "integrity": "sha512-fVfUCL/Xh2zINYMPZvj+iBn6XWouQf0DAnjaWCI9MkmqXzL2Iy5FoQB8O7syFe6gN6AH1ecDDU58T51Ou0kFkA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -482,15 +333,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.18.tgz", - "integrity": "sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.36.tgz", + "integrity": "sha512-gE+CGuPZD1eqUWGSrM8CXDjlwuPujIuwI+IlorD1wE2RcANKKT4jscB9GY1nTJbjmXzD18sycsYbgCG5m3n4/g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -498,20 +349,17 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.20.tgz", - "integrity": "sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==", + "version": "3.972.38", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.38.tgz", + "integrity": "sha512-cHZo3bV6zN9joDQ2AYVctfzHTKStxWKwnGu0z7GwCUC+DAtB3qL/+26l+a63RbmFbVvb1JK+0vJKodN3hRMwyw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/types": "^3.973.6", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.4.16", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.19", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/fetch-http-handler": "^5.4.1", + "@smithy/node-http-handler": "^4.7.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -519,24 +367,23 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.20.tgz", - "integrity": "sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==", + "version": "3.972.40", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.40.tgz", + "integrity": "sha512-0NFGS9I3PD2yMveQqqpwpRdyZVStzgk0Yr2rZHh80kV/QNqQCK5lSrksvU3nBcRNSUF5Uk8rL3Xk0EVR+UVAnA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/credential-provider-env": "^3.972.18", - "@aws-sdk/credential-provider-http": "^3.972.20", - "@aws-sdk/credential-provider-login": "^3.972.20", - "@aws-sdk/credential-provider-process": "^3.972.18", - "@aws-sdk/credential-provider-sso": "^3.972.20", - "@aws-sdk/credential-provider-web-identity": "^3.972.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/credential-provider-env": "^3.972.36", + "@aws-sdk/credential-provider-http": "^3.972.38", + "@aws-sdk/credential-provider-login": "^3.972.40", + "@aws-sdk/credential-provider-process": "^3.972.36", + "@aws-sdk/credential-provider-sso": "^3.972.40", + "@aws-sdk/credential-provider-web-identity": "^3.972.40", + "@aws-sdk/nested-clients": "^3.997.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/credential-provider-imds": "^4.3.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -544,18 +391,16 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", - "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", + "version": "3.972.40", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.40.tgz", + "integrity": "sha512-IEIl+UQnrEjZP53TSl91e8LBephi4i1Mt9WZrMgN8pOg6xPOLZdkN1GhsEzjkMD1TQy4Fp2dwWA/9ToTQFOlLA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/nested-clients": "^3.997.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -563,22 +408,21 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.21.tgz", - "integrity": "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==", + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.41.tgz", + "integrity": "sha512-h6BlclpsPGkx7Pv7ukr8oKVqN3jvxnH5n9ZIUQa8focr1ZkKd2MYiPJ2Nv9GI97dohJVJBfZAsTp/qoZL5R1pw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.18", - "@aws-sdk/credential-provider-http": "^3.972.20", - "@aws-sdk/credential-provider-ini": "^3.972.20", - "@aws-sdk/credential-provider-process": "^3.972.18", - "@aws-sdk/credential-provider-sso": "^3.972.20", - "@aws-sdk/credential-provider-web-identity": "^3.972.20", - "@aws-sdk/types": "^3.973.6", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/credential-provider-env": "^3.972.36", + "@aws-sdk/credential-provider-http": "^3.972.38", + "@aws-sdk/credential-provider-ini": "^3.972.40", + "@aws-sdk/credential-provider-process": "^3.972.36", + "@aws-sdk/credential-provider-sso": "^3.972.40", + "@aws-sdk/credential-provider-web-identity": "^3.972.40", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/credential-provider-imds": "^4.3.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -586,16 +430,15 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.18", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.18.tgz", - "integrity": "sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.36.tgz", + "integrity": "sha512-eDQ6X7clTAOxXegOx4rGT1hyfusGEYdJGCGo0Ym2+CKeMQBjk+SJSxSVev11NJew5xJHJ/c3hryl2awKaxuSEA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -603,18 +446,17 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.20.tgz", - "integrity": "sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==", + "version": "3.972.40", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.40.tgz", + "integrity": "sha512-jaABbsoOkGlKg5kaHetYmUV6mWM57H89ia0Yksom1XxC847mfjmEVb4p7VijS1sjPbXjUii4cftJuwsl4MXkRg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/token-providers": "3.1009.0", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/nested-clients": "^3.997.8", + "@aws-sdk/token-providers": "3.1047.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -622,17 +464,16 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.20.tgz", - "integrity": "sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==", + "version": "3.972.40", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.40.tgz", + "integrity": "sha512-bfIrM8IIzbRtXRQWx/vNEUBLTImLZyX5uKk8uSdeSAZ4Mj3Yi4UnRJLK4FkQLWErbM3McpVLQ1DaM6XO66Ed5g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/nested-clients": "^3.997.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -640,17 +481,15 @@ } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.8.tgz", - "integrity": "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==", + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.12.tgz", + "integrity": "sha512-MAG0Adg7FFEwuoeLbb5SBnXDW7S2EpNTwHnQ4h3pJqSKVQOhOmugyA1MfMh6AD4SAfx0lko4htZdwkNoLqFj5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-config-provider": "^4.2.2", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -658,14 +497,14 @@ } }, "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.8.tgz", - "integrity": "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.11.tgz", + "integrity": "sha512-xpobcctR1AHSrvkiArgTyLffn78Lt9unPMpa/yic9RKn+bOf/5M55UIM6RaPL5xKzI06/GSsTDywTWvzEAbyyw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -673,24 +512,19 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.974.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.0.tgz", - "integrity": "sha512-BmdDjqvnuYaC4SY7ypHLXfCSsGYGUZkjCLSZyUAAYn1YT28vbNMJNDwhlfkvvE+hQHG5RJDlEmYuvBxcB9jX1g==", + "version": "3.974.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.18.tgz", + "integrity": "sha512-2noO+4ARfC+8vOIyvJvQE6bioVaTRkUcPvUoM/jgwXcweZnZovSZ6OCs/cs+NU2p7yvuwuJT/7LkTzBSj5pU4A==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/crc64-nvme": "^3.972.5", - "@aws-sdk/types": "^3.973.6", - "@smithy/is-array-buffer": "^4.2.2", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.19", - "@smithy/util-utf8": "^4.2.2", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/crc64-nvme": "^3.972.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -698,14 +532,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", - "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.11.tgz", + "integrity": "sha512-CBC6+tVYaOJo7QXgN1zJ4Ba2f3/Cpy4eRViYFimXW/O5Mn8hBmgXXzHu4vy4ubT80YWnp8aCFygr7dTOa14yQg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -713,13 +547,13 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.8.tgz", - "integrity": "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.10.tgz", + "integrity": "sha512-rI3NZvJcEvjoD0+0PI0iUAwlPw2IlSlhyvgBK/3WkKJQE/YiKFedd9dMN2lVacdNxPNhxL/jzQaKQdrGtQagjQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -727,13 +561,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", - "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.10.tgz", + "integrity": "sha512-OOuGvvz1Dm20SjZo5oEBePFqxt5nf8AwkNDSyUHvD9/bfNASmstcYxFAHUowy4n6Io7mWUZ04JURZwSBvyQanQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -741,15 +575,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", - "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.12.tgz", + "integrity": "sha512-5eltYxKB4MfdQv7/VhWxRbAVQKow5dz9votRFigTYrWJHMQXwLMltlbk7KFWSZh5NDBySfmjT7Jv/DWfYCmDng==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", + "@aws-sdk/types": "^3.973.8", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -757,24 +591,17 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.20", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.20.tgz", - "integrity": "sha512-yhva/xL5H4tWQgsBjwV+RRD0ByCzg0TcByDCLp3GXdn/wlyRNfy8zsswDtCvr1WSKQkSQYlyEzPuWkJG0f5HvQ==", + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.39.tgz", + "integrity": "sha512-cimoQxecHHNad+lv2g7QJ24Cxqh1P0EULJSxyX4YD95BUIGeGRPumbdEXpHPxNkJRU99DVmh7u16Y+uhFu31Yw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-arn-parser": "^3.972.3", - "@smithy/core": "^3.23.11", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.19", - "@smithy/util-utf8": "^4.2.2", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/signature-v4-multi-region": "^3.996.26", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/signature-v4": "^5.4.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -782,13 +609,13 @@ } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.8.tgz", - "integrity": "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==", + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.10.tgz", + "integrity": "sha512-Gli9A0u8EVVb+5bFDGS/QbSVg28w/wpEidg1ggVcSj65BDTdGR6punsOcVjqdiu1i42WHWo51MCvARPIIz9juw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -796,18 +623,16 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", - "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", + "version": "3.972.40", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.40.tgz", + "integrity": "sha512-QLpD+HNQtL1Mc49/GRa6RmZvi/TEYBWPevC9F3L+j96IoG3xOSRctdQfbkX0lETb3TX9QQXU1oGYDmAB+YJprA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@smithy/core": "^3.23.11", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-retry": "^4.2.12", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.9", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -815,48 +640,28 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.996.10", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", - "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", + "version": "3.997.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.8.tgz", + "integrity": "sha512-/Vw2M27w+0APfMDzDpvv8auA4WiJ4D22+lC61pMS2M8Wk+4IydeRqh5utbrh+A5gQRxgUYd/xz3tdv8nQlmiHg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/middleware-host-header": "^3.972.8", - "@aws-sdk/middleware-logger": "^3.972.8", - "@aws-sdk/middleware-recursion-detection": "^3.972.8", - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/region-config-resolver": "^3.972.8", - "@aws-sdk/types": "^3.973.6", - "@aws-sdk/util-endpoints": "^3.996.5", - "@aws-sdk/util-user-agent-browser": "^3.972.8", - "@aws-sdk/util-user-agent-node": "^3.973.7", - "@smithy/config-resolver": "^4.4.11", - "@smithy/core": "^3.23.11", - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/hash-node": "^4.2.12", - "@smithy/invalid-dependency": "^4.2.12", - "@smithy/middleware-content-length": "^4.2.12", - "@smithy/middleware-endpoint": "^4.4.25", - "@smithy/middleware-retry": "^4.4.42", - "@smithy/middleware-serde": "^4.2.14", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/node-http-handler": "^4.4.16", - "@smithy/protocol-http": "^5.3.12", - "@smithy/smithy-client": "^4.12.5", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-body-length-node": "^4.2.3", - "@smithy/util-defaults-mode-browser": "^4.3.41", - "@smithy/util-defaults-mode-node": "^4.2.44", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.12", - "@smithy/util-utf8": "^4.2.2", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/middleware-host-header": "^3.972.11", + "@aws-sdk/middleware-logger": "^3.972.10", + "@aws-sdk/middleware-recursion-detection": "^3.972.12", + "@aws-sdk/middleware-user-agent": "^3.972.40", + "@aws-sdk/region-config-resolver": "^3.972.14", + "@aws-sdk/signature-v4-multi-region": "^3.996.26", + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/util-endpoints": "^3.996.9", + "@aws-sdk/util-user-agent-browser": "^3.972.11", + "@aws-sdk/util-user-agent-node": "^3.973.26", + "@smithy/core": "^3.24.1", + "@smithy/fetch-http-handler": "^5.4.1", + "@smithy/node-http-handler": "^4.7.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -864,15 +669,14 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", - "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "version": "3.972.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.14.tgz", + "integrity": "sha512-VuLXVmm7+lKVxqFcOItPkXhjbJ02iUfxkxheRu41SfWf6/xrZup2A2SwHZos/LeQGu3SBHeqTQht80Uo3ienPA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/config-resolver": "^4.4.11", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -880,16 +684,15 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.8.tgz", - "integrity": "sha512-n1qYFD+tbqZuyskVaxUE+t10AUz9g3qzDw3Tp6QZDKmqsjfDmZBd4GIk2EKJJNtcCBtE5YiUjDYA+3djFAFBBg==", + "version": "3.996.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.26.tgz", + "integrity": "sha512-2N62veqdMZBCwQUHsbhtnaovOFjOa5Dn3dAD1nRqFTUXR4QmirT3HZnfus/L1DS08Vm5CkoKmL0iMVt6YbqEag==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.20", - "@aws-sdk/types": "^3.973.6", - "@smithy/protocol-http": "^5.3.12", - "@smithy/signature-v4": "^5.3.12", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/signature-v4": "^5.4.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -897,17 +700,16 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1009.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1009.0.tgz", - "integrity": "sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==", + "version": "3.1047.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1047.0.tgz", + "integrity": "sha512-GwJUeMijpeO2SOGGLRg4q2Nj9foBUBd7hTALYVId+m8fQmA4P2hITp5dmrZFd4AjEkSVmt2eFqmk3TttF7HZeQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.20", - "@aws-sdk/nested-clients": "^3.996.10", - "@aws-sdk/types": "^3.973.6", - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", + "@aws-sdk/core": "^3.974.10", + "@aws-sdk/nested-clients": "^3.997.8", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -915,24 +717,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", - "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", - "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", "license": "Apache-2.0", "dependencies": { + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -940,15 +730,14 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.996.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", - "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "version": "3.996.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.9.tgz", + "integrity": "sha512-ibx8Vd73rCTHekNGeXX8cpGWoBKbNAlwKHL3yjSxxttu5QnNDaSAM7/0MFYDjU31/F4lyrPoQcGirT0ew61xcg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-endpoints": "^3.3.3", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -956,40 +745,39 @@ } }, "node_modules/@aws-sdk/util-locate-window": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", - "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", - "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.11.tgz", + "integrity": "sha512-kq3RS6XQtHMrLFShbkem6h+8fxazB3jEIsbMC6aaSInOciRGE+eGAqTgJ+obO7Euo/pjM8thVqLiLISEH9X9DA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.6", - "@smithy/types": "^4.13.1", + "@aws-sdk/types": "^3.973.8", + "@smithy/types": "^4.14.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", - "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", + "version": "3.973.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.26.tgz", + "integrity": "sha512-9bHR/EERjhrUGyo1qW620ogbGBtCglYB/pEtcm85sVd4/Ah+bwdLI3g1aJf75oNwNwh7+fw+8wOk/OCWHjzVmA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.21", - "@aws-sdk/types": "^3.973.6", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-config-provider": "^4.2.2", + "@aws-sdk/middleware-user-agent": "^3.972.40", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.1", + "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, "engines": { @@ -1005,13 +793,14 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.16.tgz", - "integrity": "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==", + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", + "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", - "fast-xml-parser": "5.5.8", + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" }, "engines": { @@ -1043,9 +832,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", "dev": true, "license": "MIT", "engines": { @@ -1058,7 +847,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1085,9 +873,9 @@ } }, "node_modules/@babel/core/node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1147,9 +935,9 @@ } }, "node_modules/@babel/generator/node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1179,6 +967,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1214,9 +1012,9 @@ } }, "node_modules/@babel/helper-module-imports/node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1267,9 +1065,9 @@ } }, "node_modules/@babel/helper-module-transforms/node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1361,15 +1159,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -1386,9 +1175,9 @@ } }, "node_modules/@babel/template/node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "dev": true, "license": "MIT", "dependencies": { @@ -1420,6 +1209,16 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", @@ -1461,9 +1260,9 @@ "license": "MIT" }, "node_modules/@dotenvx/dotenvx": { - "version": "1.54.1", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.54.1.tgz", - "integrity": "sha512-41gU3q7v05GM92QPuPUf4CmUw+mmF8p4wLUh6MCRlxpCkJ9ByLcY9jUf6MwrMNmiKyG/rIckNxj9SCfmNCmCqw==", + "version": "1.66.0", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.66.0.tgz", + "integrity": "sha512-qlQFhHUjhRDybrinqLAD0MClVZDOrsq80O8eD5iSjz3Qa/4f3Jg7SQrOaSobrRyP1QaWIYLGtGpj2c7H0D8NUw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1474,8 +1273,9 @@ "fdir": "^6.2.0", "ignore": "^5.3.0", "object-treeify": "1.1.33", - "picomatch": "^4.0.2", - "which": "^4.0.0" + "picomatch": "^4.0.4", + "which": "^4.0.0", + "yocto-spinner": "^1.1.0" }, "bin": { "dotenvx": "src/cli/dotenvx.js" @@ -1492,14 +1292,14 @@ "license": "Apache-2.0" }, "node_modules/@ecies/ciphers": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.5.tgz", - "integrity": "sha512-GalEZH4JgOMHYYcYmVqnFirFsjZHeoGMDt9IxEnM9F7GRUUyUksJ7Ou53L83WHJq3RWKD3AcBpo0iQh0oMpf8A==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@ecies/ciphers/-/ciphers-0.2.6.tgz", + "integrity": "sha512-patgsRPKGkhhoBjETV4XxD0En4ui5fbX0hzayqI3M8tvNMGUoUvmyYAIWwlxBc1KX5cturfqByYdj5bYGRpN9g==", "dev": true, "license": "MIT", "engines": { "bun": ">=1", - "deno": ">=2", + "deno": ">=2.7.10", "node": ">=16" }, "peerDependencies": { @@ -1507,20 +1307,20 @@ } }, "node_modules/@emnapi/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", - "integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", - "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "license": "MIT", "optional": true, "dependencies": { @@ -1528,9 +1328,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", - "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "license": "MIT", "optional": true, "dependencies": { @@ -1578,9 +1378,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], @@ -1595,9 +1395,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -1612,9 +1412,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -1629,9 +1429,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -1646,9 +1446,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -1663,9 +1463,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -1680,9 +1480,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -1697,9 +1497,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -1714,9 +1514,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -1731,9 +1531,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -1748,9 +1548,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -1765,9 +1565,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -1782,9 +1582,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -1799,9 +1599,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -1816,9 +1616,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -1833,9 +1633,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -1850,9 +1650,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -1867,9 +1667,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", "cpu": [ "arm64" ], @@ -1884,9 +1684,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -1901,9 +1701,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", "cpu": [ "arm64" ], @@ -1918,9 +1718,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -1935,9 +1735,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", "cpu": [ "arm64" ], @@ -1952,9 +1752,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], @@ -1969,9 +1769,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -1986,9 +1786,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], @@ -2003,9 +1803,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -2062,13 +1862,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.3", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" }, @@ -2077,22 +1877,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", - "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1" + "@eslint/core": "^1.2.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2103,9 +1903,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2113,13 +1913,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { @@ -2127,9 +1927,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.3.0.tgz", - "integrity": "sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.4.0.tgz", + "integrity": "sha512-sDBWI3yLy8EcDzgobvJTWq1MJYzAkQdpjXuPukga9wXonhpMRvd1Izuo2Qgwey2OiEoRIBr35RMU9HJRoOHzpw==", "funding": [ { "type": "opencollective", @@ -2143,22 +1943,22 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.10" + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" } }, "node_modules/@floating-ui/react": { @@ -2177,12 +1977,12 @@ } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.4" + "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", @@ -2190,67 +1990,45 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", "license": "MIT" }, - "node_modules/@formatjs/ecma402-abstract": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.1.1.tgz", - "integrity": "sha512-jhZbTwda+2tcNrs4kKvxrPLPjx8QsBCLCUgrrJ/S+G9YrGHWLhAyFMMBHJBnBoOwuLHd7L14FgYudviKaxkO2Q==", - "license": "MIT", - "dependencies": { - "@formatjs/fast-memoize": "3.1.0", - "@formatjs/intl-localematcher": "0.8.1", - "decimal.js": "^10.6.0", - "tslib": "^2.8.1" - } - }, "node_modules/@formatjs/fast-memoize": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz", - "integrity": "sha512-b5mvSWCI+XVKiz5WhnBCY3RJ4ZwfjAidU0yVlKa3d3MSgKmH1hC3tBGEAtYyN5mqL7N0G5x0BOUYyO8CEupWgg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.8.1" - } + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.5.tgz", + "integrity": "sha512-KLi3fan6WnCHmigd9pmEEN8Hid0v4wiFBW576M/d07KMWYecf1CvyMI3n34vCmHT4AoVqG2n702kiHbXjzZX2A==", + "license": "MIT" }, "node_modules/@formatjs/icu-messageformat-parser": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.1.tgz", - "integrity": "sha512-sSDmSvmmoVQ92XqWb499KrIhv/vLisJU8ITFrx7T7NZHUmMY7EL9xgRowAosaljhqnj/5iufG24QrdzB6X3ItA==", + "version": "3.5.9", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.9.tgz", + "integrity": "sha512-PZm6O9JI/gUPtQV9r2eaMuLb4yWqV2vz+ot03ORHWTKO343LSpZi0TqeXLB2ZZGDXLCw2SbfgsQ0GxoxXMl79g==", "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": "3.1.1", - "@formatjs/icu-skeleton-parser": "2.1.1", - "tslib": "^2.8.1" + "@formatjs/icu-skeleton-parser": "2.1.9" } }, "node_modules/@formatjs/icu-skeleton-parser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.1.tgz", - "integrity": "sha512-PSFABlcNefjI6yyk8f7nyX1DC7NHmq6WaCHZLySEXBrXuLOB2f935YsnzuPjlz+ibhb9yWTdPeVX1OVcj24w2Q==", - "license": "MIT", - "dependencies": { - "@formatjs/ecma402-abstract": "3.1.1", - "tslib": "^2.8.1" - } + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.9.tgz", + "integrity": "sha512-rsxswgHMfU1zUgB2byc08fesf83wLGjFnzLCEtuf00mx2doiqc6pYrf67raI37XqdRcGUviQepk2UKGqpng74Q==", + "license": "MIT" }, "node_modules/@formatjs/intl-localematcher": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz", - "integrity": "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==", + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.8.tgz", + "integrity": "sha512-pBr2hVKWvkHVnfXegW+53NT9U2uaVQCc+EgzLPCCwXqBA3nvM5fPbK9IcJlNjV+NMKGyZ2F3ZSG78iGdxAAqbA==", "license": "MIT", "dependencies": { - "@formatjs/fast-memoize": "3.1.0", - "tslib": "^2.8.1" + "@formatjs/fast-memoize": "3.1.5" } }, "node_modules/@headlessui/react": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz", - "integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==", + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.10.tgz", + "integrity": "sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA==", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.26.16", @@ -2286,29 +2064,43 @@ } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2338,9 +2130,9 @@ } }, "node_modules/@img/colour": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", - "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", "license": "MIT", "optional": true, "engines": { @@ -2348,13 +2140,12 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", - "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2367,17 +2158,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.3" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", - "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2390,17 +2180,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.3" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2411,13 +2200,12 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2428,13 +2216,12 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", - "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2445,13 +2232,12 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", - "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2462,13 +2248,28 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", - "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", "cpu": [ "ppc64" ], - "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2479,13 +2280,12 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", - "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2496,13 +2296,12 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", - "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2513,13 +2312,12 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", - "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2530,13 +2328,12 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", - "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2547,13 +2344,12 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", - "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2566,17 +2362,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.3" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", - "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2589,17 +2384,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.3" + "@img/sharp-libvips-linux-arm64": "1.2.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", - "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2612,17 +2406,38 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.3" + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", - "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2635,17 +2450,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.3" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", - "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2658,17 +2472,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.3" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", - "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2681,17 +2494,16 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", - "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2704,21 +2516,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", - "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.5.0" + "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -2728,13 +2539,12 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", - "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2748,13 +2558,12 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", - "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2768,13 +2577,12 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", - "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2787,6 +2595,33 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@internationalized/date": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.1.tgz", + "integrity": "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/number": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.6.tgz", + "integrity": "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@internationalized/string": { + "version": "3.2.8", + "resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.8.tgz", + "integrity": "sha512-NdbMQUSfXLYIQol5VyMtinm9pZDciiMfN7RtmSuSB78io1hqwJ0naYfxyW6vgxWBkzWymQa/3uLDlbfmshtCaA==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, "node_modules/@ioredis/commands": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", @@ -2885,15 +2720,15 @@ } }, "node_modules/@next/env": { - "version": "15.5.15", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.15.tgz", - "integrity": "sha512-vcmyu5/MyFzN7CdqRHO3uHO44p/QPCZkuTUXroeUmhNP8bL5PHFEhik22JUazt+CDDoD6EpBYRCaS2pISL+/hg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", + "integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.7.tgz", - "integrity": "sha512-v/bRGOJlfRCO+NDKt0bZlIIWjhMKU8xbgEQBo+rV9C8S6czZvs96LZ/v24/GvpEnovZlL4QDpku/RzWHVbmPpA==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.6.tgz", + "integrity": "sha512-Z8l6o4JWKUl755x4R+wogD86KPeU+Ckw4K+SYG4kHeOJtRenDeK+OSbGcqZpDtbwn9DsJVdir2UxmwXuinUbUw==", "dev": true, "license": "MIT", "dependencies": { @@ -2901,9 +2736,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.15.tgz", - "integrity": "sha512-6PvFO2Tzt10GFK2Ro9tAVEtacMqRmTarYMFKAnV2vYMdwWc73xzmDQyAV7SwEdMhzmiRoo7+m88DuiXlJlGeaw==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", + "integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", "cpu": [ "arm64" ], @@ -2917,9 +2752,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.15", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.15.tgz", - "integrity": "sha512-G+YNV+z6FDZTp/+IdGyIMFqalBTaQSnvAA+X/hrt+eaTRFSznRMz9K7rTmzvM6tDmKegNtyzgufZW0HwVzEqaQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", + "integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", "cpu": [ "x64" ], @@ -2933,9 +2768,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.15.tgz", - "integrity": "sha512-eVkrMcVIBqGfXB+QUC7jjZ94Z6uX/dNStbQFabewAnk13Uy18Igd1YZ/GtPRzdhtm7QwC0e6o7zOQecul4iC1w==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", + "integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", "cpu": [ "arm64" ], @@ -2949,9 +2784,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.15.tgz", - "integrity": "sha512-RwSHKMQ7InLy5GfkY2/n5PcFycKA08qI1VST78n09nN36nUPqCvGSMiLXlfUmzmpQpF6XeBYP2KRWHi0UW3uNg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", + "integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", "cpu": [ "arm64" ], @@ -2965,9 +2800,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.15.tgz", - "integrity": "sha512-nplqvY86LakS+eeiuWsNWvfmK8pFcOEW7ZtVRt4QH70lL+0x6LG/m1OpJ/tvrbwjmR8HH9/fH2jzW1GlL03TIg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", + "integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", "cpu": [ "x64" ], @@ -2981,9 +2816,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.15", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.15.tgz", - "integrity": "sha512-eAgl9NKQ84/sww0v81DQINl/vL2IBxD7sMybd0cWRw6wqgouVI53brVRBrggqBRP/NWeIAE1dm5cbKYoiMlqDQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", + "integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", "cpu": [ "x64" ], @@ -2997,9 +2832,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.15.tgz", - "integrity": "sha512-GJVZC86lzSquh0MtvZT+L7G8+jMnJcldloOjA8Kf3wXvBrvb6OGe2MzPuALxFshSm/IpwUtD2mIoof39ymf52A==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", + "integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", "cpu": [ "arm64" ], @@ -3013,9 +2848,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.15", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.15.tgz", - "integrity": "sha512-nFucjVdwlFqxh/JG3hWSJ4p8+YJV7Ii8aPDuBQULB6DzUF4UNZETXLfEUk+oI2zEznWWULPt7MeuTE6xtK1HSA==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", + "integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", "cpu": [ "x64" ], @@ -3034,7 +2869,6 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -3071,6 +2905,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@node-rs/argon2": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-2.0.2.tgz", @@ -3701,16 +3547,16 @@ "license": "MIT" }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" @@ -3720,25 +3566,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -3756,9 +3602,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -3776,9 +3622,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -3796,9 +3642,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], @@ -3816,9 +3662,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], @@ -3836,9 +3682,9 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], @@ -3856,9 +3702,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], @@ -3876,9 +3722,9 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], @@ -3896,9 +3742,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], @@ -3916,9 +3762,9 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], @@ -3936,9 +3782,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -3956,9 +3802,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -3976,9 +3822,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -3995,155 +3841,152 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/watcher/node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", - "license": "Apache-2.0", - "bin": { - "detect-libc": "bin/detect-libc.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/@peculiar/asn1-android": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.6.0.tgz", - "integrity": "sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.7.0.tgz", + "integrity": "sha512-iD3VskhVQnM4nE3PN9cBdPTR7JrqZy3FYk+uD2CeG6DUqKoANqaEfx0f7izPmW+Qm5JBM35ek+viLCmjy18ByQ==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-schema": "^2.7.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-cms": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", - "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.7.0.tgz", + "integrity": "sha512-hew63shtzzvBcSHbhm+cyAmKe6AIfinT9hzEqSPjDC6opTTMKmTkQ0gHuN2KsWlvqiKw1S/fS94fhag/FJkioQ==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "@peculiar/asn1-x509-attr": "^2.7.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-csr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", - "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.7.0.tgz", + "integrity": "sha512-VVsAyGqErT9D1SY4aEqozThXMVI+ssVRiv2DDeYuvpBKLIgZ3hYs3Ay3u/VSoKq6ESFi9cf6rf3IOOzfwh7oMA==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-ecc": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", - "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.7.0.tgz", + "integrity": "sha512-n7KEs/Q/wrB415cxy4fHOBhegp4NdJ15fkJPwcB/3/8iNBQC2L/N7SChJPKDJPZGYH0jD4Tg4/0vnHmwghnbKw==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-pfx": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", - "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.7.0.tgz", + "integrity": "sha512-V/nrlQVmhg7lYAsM7E13UDL5erAwFv6kCIVFqNaMIHSVi7dngcT839JkRTkQBqznMG98l2XjxYk74ZztAohZzA==", "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-rsa": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-cms": "^2.7.0", + "@peculiar/asn1-pkcs8": "^2.7.0", + "@peculiar/asn1-rsa": "^2.7.0", + "@peculiar/asn1-schema": "^2.7.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-pkcs8": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", - "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.7.0.tgz", + "integrity": "sha512-9GTl1nE8Mx1kTZ+7QyYatDyKsm34QcWRBFkY1iPvWC3X4Dona5s/tlLiQsx5WzVdZqiMBZNYT0buyw4/vbhnjw==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-pkcs9": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", - "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.7.0.tgz", + "integrity": "sha512-Bh7m+OuIaSEllPQcSd9OSp93F4ROWH7sbITWV8MI+8dwsjE5111/87VxiWVvYFKyww3vp39geLv9ENqhwWHcew==", "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.1", - "@peculiar/asn1-pfx": "^2.6.1", - "@peculiar/asn1-pkcs8": "^2.6.1", - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", - "@peculiar/asn1-x509-attr": "^2.6.1", + "@peculiar/asn1-cms": "^2.7.0", + "@peculiar/asn1-pfx": "^2.7.0", + "@peculiar/asn1-pkcs8": "^2.7.0", + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", + "@peculiar/asn1-x509-attr": "^2.7.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-rsa": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", - "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.7.0.tgz", + "integrity": "sha512-/qvENQrXyTZURjMqSeofHul0JJt2sNSzSwk36pl2olkHbaioMQgrASDZAlHXl0xUlnVbHj0uGgOrBMTb5x2aJQ==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-schema": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", - "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.7.0.tgz", + "integrity": "sha512-W8ZfWzLmQnrcky+eh3tni4IozMdqBDiHWU0N+vve/UGjMaUs8c0L7A2oEdkBXS8rTpWDpK/aoI3DG/L/hxmxPg==", "license": "MIT", "dependencies": { + "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-x509": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", - "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.7.0.tgz", + "integrity": "sha512-mUn9RRrkGDnG4ALfunDmzyRW5dg+sWCj/pfnCCqEHYbkGxEpvUt6iVJv8Yw1cyp6SWZ26ZE5oSmI5SqEaen15g==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.6", - "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", - "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.7.0.tgz", + "integrity": "sha512-NS8e7SOgXipkzUPLF/sce7ukpMpWjhxYsH0n6Y+bHYo4TTxOb95Zv7hqwSuL212mj5YxovjdOKQOgH1As3E94w==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-schema": "^2.7.0", + "@peculiar/asn1-x509": "^2.7.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, + "node_modules/@peculiar/utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz", + "integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, "node_modules/@peculiar/x509": { "version": "1.14.3", "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", @@ -4167,14 +4010,20 @@ } }, "node_modules/@posthog/core": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.23.2.tgz", - "integrity": "sha512-zTDdda9NuSHrnwSOfFMxX/pyXiycF4jtU1kTr8DL61dHhV+7LF6XF1ndRZZTuaGGbfbb/GJYkEsjEX9SXfNZeQ==", + "version": "1.29.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.29.1.tgz", + "integrity": "sha512-q+/t/DZALr50YTE0dFgfGSS9EgwcyAlqsn+JS61wLkwdcDM5yu/YTDM8oMKmJupsyjSZlVkDuHZAMd4ab7AxzQ==", "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6" + "@posthog/types": "1.373.4" } }, + "node_modules/@posthog/types": { + "version": "1.373.4", + "resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.373.4.tgz", + "integrity": "sha512-n+0AbGRYYsbi+CQXQi2rF1lwTSyASlaogcw4YSkzB5KeMa4Y6nhNb7+TTnu9aVor+BycsQYCa2OsBrMMbaTekw==", + "license": "MIT" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -6344,16 +6193,13 @@ "license": "MIT" }, "node_modules/@react-aria/focus": { - "version": "3.21.2", - "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz", - "integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==", + "version": "3.22.0", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.22.0.tgz", + "integrity": "sha512-ZfDOVuVhqDsM9mkNji3QUZ/d40JhlVgXrDkrfXylM1035QCrcTHN7m2DpbE95sU2A8EQb4wikvt5jM6K/73BPg==", "license": "Apache-2.0", "dependencies": { - "@react-aria/interactions": "^3.25.6", - "@react-aria/utils": "^3.31.0", - "@react-types/shared": "^3.32.1", "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" + "react-aria": "3.48.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", @@ -6361,49 +6207,14 @@ } }, "node_modules/@react-aria/interactions": { - "version": "3.25.6", - "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz", - "integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==", + "version": "3.28.0", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.28.0.tgz", + "integrity": "sha512-OXwdU1EWFdMxmr/K1CXNGJzmNlCClByb+PuCaqUyzBymHPCGVhawirLIon/CrIN5psh3AiWpHSh4H0WeJdVpng==", "license": "Apache-2.0", "dependencies": { - "@react-aria/ssr": "^3.9.10", - "@react-aria/utils": "^3.31.0", - "@react-stately/flags": "^3.1.2", - "@react-types/shared": "^3.32.1", - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", - "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/ssr": { - "version": "3.9.10", - "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", - "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" - } - }, - "node_modules/@react-aria/utils": { - "version": "3.31.0", - "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.31.0.tgz", - "integrity": "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==", - "license": "Apache-2.0", - "dependencies": { - "@react-aria/ssr": "^3.9.10", - "@react-stately/flags": "^3.1.2", - "@react-stately/utils": "^3.10.8", - "@react-types/shared": "^3.32.1", + "@react-types/shared": "^3.34.0", "@swc/helpers": "^0.5.0", - "clsx": "^2.0.0" + "react-aria": "3.48.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", @@ -6411,9 +6222,10 @@ } }, "node_modules/@react-email/body": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.2.1.tgz", - "integrity": "sha512-ljDiQiJDu/Fq//vSIIP0z5Nuvt4+DX1RqGasstChDGJB/14ogd4VdNS9aacoede/ZjGy3o3Qb+cxyS+XgM6SwQ==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.3.0.tgz", + "integrity": "sha512-uGo0BOOzjbMUo3lu+BIDWayvn5o6Xyfmnlla5VGf05n8gHMvO1ll7U4FtzWe3hxMLwt53pmc4iE0M+B5slG+Ug==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6426,6 +6238,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.1.tgz", "integrity": "sha512-qXyj7RZLE7POy9BMKSoqQ00tOXThjOZSUnI2Yu9i29IHngPlmrNayIWBoVKtElES7OWwypUcpiajwi1mUWx6/A==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6438,6 +6251,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.2.1.tgz", "integrity": "sha512-M3B7JpVH4ytgn83/ujRR1k1DQHvTeABiDM61OvAbjLRPhC/5KLHU5KkzIbbuGIrjWwxAbL1kSQzU8MhLEtSxyw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "dependencies": { "prismjs": "^1.30.0" @@ -6453,6 +6267,7 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.6.tgz", "integrity": "sha512-jfhebvv3dVsp3OdPgKXnk8+e2pBiDVZejDOBFzBa/IblrAJ9cQDkN6rBD5IyEg8hTOxwbw3iaI/yZFmDmIguIA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6465,6 +6280,7 @@ "version": "0.0.14", "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.14.tgz", "integrity": "sha512-f+W+Bk2AjNO77zynE33rHuQhyqVICx4RYtGX9NKsGUg0wWjdGP0qAuIkhx9Rnmk4/hFMo1fUrtYNqca9fwJdHg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6474,12 +6290,13 @@ } }, "node_modules/@react-email/components": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.8.tgz", - "integrity": "sha512-zY81ED6o5MWMzBkr9uZFuT24lWarT+xIbOZxI6C9dsFmCWBczM8IE1BgOI8rhpUK4JcYVDy1uKxYAFqsx2Bc4w==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.12.tgz", + "integrity": "sha512-tH18JhPDWgE+3jnYkzyB6ZrZdfNnEsFe4PwmuXmlOw4NGIysP8wPY5aXZg++pTG9qUabXg1nzX/FGHGkObH8xQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "dependencies": { - "@react-email/body": "0.2.1", + "@react-email/body": "0.3.0", "@react-email/button": "0.2.1", "@react-email/code-block": "0.2.1", "@react-email/code-inline": "0.0.6", @@ -6494,10 +6311,10 @@ "@react-email/link": "0.0.13", "@react-email/markdown": "0.0.18", "@react-email/preview": "0.0.14", - "@react-email/render": "2.0.4", + "@react-email/render": "2.0.6", "@react-email/row": "0.0.13", "@react-email/section": "0.0.17", - "@react-email/tailwind": "2.0.5", + "@react-email/tailwind": "2.0.7", "@react-email/text": "0.1.6" }, "engines": { @@ -6507,10 +6324,28 @@ "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, + "node_modules/@react-email/components/node_modules/@react-email/render": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.6.tgz", + "integrity": "sha512-xOzaYkH3jLZKqN5MqrTXYnmqBYUnZSVbkxdb5PGGmDcK6sKDVMliaDiSwfXajRC9JtSHTcGc2tmGLHWuCgVpog==", + "license": "MIT", + "dependencies": { + "html-to-text": "^9.0.5", + "prettier": "^3.5.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, "node_modules/@react-email/container": { "version": "0.0.16", "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.16.tgz", "integrity": "sha512-QWBB56RkkU0AJ9h+qy33gfT5iuZknPC7Un/IjZv9B0QmMIK+WWacc0cH6y2SV5Cv/b99hU94fjEMOOO4enpkbQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6523,6 +6358,7 @@ "version": "0.0.10", "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.10.tgz", "integrity": "sha512-0urVSgCmQIfx5r7Xc586miBnQUVnGp3OTYUm8m5pwtQRdTRO5XrTtEfNJ3JhYhSOruV0nD8fd+dXtKXobum6tA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6535,6 +6371,7 @@ "version": "0.0.13", "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.13.tgz", "integrity": "sha512-AJg6le/08Gz4tm+6MtKXqtNNyKHzmooOCdmtqmWxD7FxoAdU1eVcizhtQ0gcnVaY6ethEyE/hnEzQxt1zu5Kog==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6547,6 +6384,7 @@ "version": "0.0.16", "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.16.tgz", "integrity": "sha512-jmsKnQm1ykpBzw4hCYHwBkt5pW2jScXffPeEH5ZRF5tZeF5b1pvlFTO9han7C0pCkZYo1kEvWiRtx69yfCIwuw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6559,6 +6397,7 @@ "version": "0.0.12", "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.12.tgz", "integrity": "sha512-TwmOmBDibavUQpXBxpmZYi2Iks/yeZOzFYh+di9EltMSnEabH8dMZXrl+pxNXzCgZ2XE8HY7VmUL65Lenfu5PA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6571,6 +6410,7 @@ "version": "0.0.12", "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.12.tgz", "integrity": "sha512-KTShZesan+UsreU7PDUV90afrZwU5TLwYlALuCSU0OT+/U8lULNNbAUekg+tGwCnOfIKYtpDPKkAMRdYlqUznw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6583,6 +6423,7 @@ "version": "0.0.12", "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.12.tgz", "integrity": "sha512-sRCpEARNVTf3FQhZOC+JTvu5r6ubiYWkT0ucYXg8ctkyi4G8QG+jgYPiNUqVeTLA2STOfmPM/nrk1nb84y6CPQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6595,6 +6436,7 @@ "version": "0.0.13", "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.13.tgz", "integrity": "sha512-lkWc/NjOcefRZMkQoSDDbuKBEBDES9aXnFEOuPH845wD3TxPwh+QTf0fStuzjoRLUZWpHnio4z7qGGRYusn/sw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6607,6 +6449,7 @@ "version": "0.0.18", "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.18.tgz", "integrity": "sha512-gSuYK5fsMbGk87jDebqQ6fa2fKcWlkf2Dkva8kMONqLgGCq8/0d+ZQYMEJsdidIeBo3kmsnHZPrwdFB4HgjUXg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "dependencies": { "marked": "^15.0.12" @@ -6622,6 +6465,7 @@ "version": "0.0.14", "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.14.tgz", "integrity": "sha512-aYK8q0IPkBXyMsbpMXgxazwHxYJxTrXrV95GFuu2HbEiIToMwSyUgb8HDFYwPqqfV03/jbwqlsXmFxsOd+VNaw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6630,257 +6474,10 @@ "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, - "node_modules/@react-email/preview-server": { - "version": "5.2.10", - "resolved": "https://registry.npmjs.org/@react-email/preview-server/-/preview-server-5.2.10.tgz", - "integrity": "sha512-cYi21KF+Z/HGXT8RpkQMNFFubBafxyoB9Hn/wrslfDNtdoews2MdsDo6XXKkZvDTRG9SxQN3HGk4v4aoQZc20g==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "0.27.3", - "next": "16.1.7" - } - }, - "node_modules/@react-email/preview-server/node_modules/@next/env": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz", - "integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@react-email/preview-server/node_modules/@next/swc-darwin-arm64": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz", - "integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@react-email/preview-server/node_modules/@next/swc-darwin-x64": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.7.tgz", - "integrity": "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.7.tgz", - "integrity": "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.7.tgz", - "integrity": "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.7.tgz", - "integrity": "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.7.tgz", - "integrity": "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@react-email/preview-server/node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.7.tgz", - "integrity": "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@react-email/preview-server/node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.7.tgz", - "integrity": "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@react-email/preview-server/node_modules/@swc/helpers": { - "version": "0.5.15", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", - "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/next": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz", - "integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@next/env": "16.1.7", - "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.9.19", - "caniuse-lite": "^1.0.30001579", - "postcss": "8.4.31", - "styled-jsx": "5.1.6" - }, - "bin": { - "next": "dist/bin/next" - }, - "engines": { - "node": ">=20.9.0" - }, - "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.7", - "@next/swc-darwin-x64": "16.1.7", - "@next/swc-linux-arm64-gnu": "16.1.7", - "@next/swc-linux-arm64-musl": "16.1.7", - "@next/swc-linux-x64-gnu": "16.1.7", - "@next/swc-linux-x64-musl": "16.1.7", - "@next/swc-win32-arm64-msvc": "16.1.7", - "@next/swc-win32-x64-msvc": "16.1.7", - "sharp": "^0.34.4" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.1.0", - "@playwright/test": "^1.51.1", - "babel-plugin-react-compiler": "*", - "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", - "sass": "^1.3.0" - }, - "peerDependenciesMeta": { - "@opentelemetry/api": { - "optional": true - }, - "@playwright/test": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - }, - "sass": { - "optional": true - } - } - }, - "node_modules/@react-email/preview-server/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/@react-email/render": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.4.tgz", - "integrity": "sha512-kht2oTFQ1SwrLpd882ahTvUtNa9s53CERHstiTbzhm6aR2Hbykp/mQ4tpPvsBGkKAEvKRlDEoooh60Uk6nHK1g==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.8.tgz", + "integrity": "sha512-5udvVr3U/WuGJZfLdLBOhkzrqRWd2Q5ZYmF7ppcy7FzWcwgshdqLMNqJOXcVzAXJXg/2bm7D+WGJzTtZOZMQnQ==", "license": "MIT", "dependencies": { "html-to-text": "^9.0.5", @@ -6898,6 +6495,7 @@ "version": "0.0.13", "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.13.tgz", "integrity": "sha512-bYnOac40vIKCId7IkwuLAAsa3fKfSfqCvv6epJKmPE0JBuu5qI4FHFCl9o9dVpIIS08s/ub+Y/txoMt0dYziGw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6910,6 +6508,7 @@ "version": "0.0.17", "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.17.tgz", "integrity": "sha512-qNl65ye3W0Rd5udhdORzTV9ezjb+GFqQQSae03NDzXtmJq6sqVXNWNiVolAjvJNypim+zGXmv6J9TcV5aNtE/w==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "engines": { "node": ">=20.0.0" @@ -6919,9 +6518,10 @@ } }, "node_modules/@react-email/tailwind": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.5.tgz", - "integrity": "sha512-7Ey+kiWliJdxPMCLYsdDts8ffp4idlP//w4Ui3q/A5kokVaLSNKG8DOg/8qAuzWmRiGwNQVOKBk7PXNlK5W+sg==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.7.tgz", + "integrity": "sha512-kGw80weVFXikcnCXbigTGXGWQ0MRCSYNCudcdkWxebkWYd0FG6/NPoN3V1p/u68/4+NxZwYPVi2fhnp0x23HdA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", "dependencies": { "tailwindcss": "^4.1.18" @@ -6930,17 +6530,17 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@react-email/body": "0.2.1", - "@react-email/button": "0.2.1", - "@react-email/code-block": "0.2.1", - "@react-email/code-inline": "0.0.6", - "@react-email/container": "0.0.16", - "@react-email/heading": "0.0.16", - "@react-email/hr": "0.0.12", - "@react-email/img": "0.0.12", - "@react-email/link": "0.0.13", - "@react-email/preview": "0.0.14", - "@react-email/text": "0.1.6", + "@react-email/body": ">=0", + "@react-email/button": ">=0", + "@react-email/code-block": ">=0", + "@react-email/code-inline": ">=0", + "@react-email/container": ">=0", + "@react-email/heading": ">=0", + "@react-email/hr": ">=0", + "@react-email/img": ">=0", + "@react-email/link": ">=0", + "@react-email/preview": ">=0", + "@react-email/text": ">=0", "react": "^18.0 || ^19.0 || ^19.0.0-rc" }, "peerDependenciesMeta": { @@ -6980,8 +6580,8 @@ "version": "0.1.6", "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.6.tgz", "integrity": "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" }, @@ -6989,36 +6589,62 @@ "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, - "node_modules/@react-stately/flags": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", - "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", - "license": "Apache-2.0", + "node_modules/@react-email/ui": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@react-email/ui/-/ui-6.1.4.tgz", + "integrity": "sha512-FyMCx7uNV0ugZY9ZWNAnWFTrXnYOhRQm97ybbZapJCQLu6uhZHmluXv2HcsSZ0RaMAzuLJgw0ziLQjq4uYEiNg==", + "dev": true, + "license": "MIT", "dependencies": { - "@swc/helpers": "^0.5.0" - } - }, - "node_modules/@react-stately/utils": { - "version": "3.10.8", - "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz", - "integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==", - "license": "Apache-2.0", - "dependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + "esbuild": "0.28.0", + "next": "16.2.6" } }, "node_modules/@react-types/shared": { - "version": "3.32.1", - "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz", - "integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==", + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.34.0.tgz", + "integrity": "sha512-gp6xo/s2lX54AlTjOiqwDnxA7UW79BNvI9dB9pr3LZTzRKCd1ZA+ZbgKw/ReIiWuvvVw/8QFJpnqeeFyLocMcQ==", "license": "Apache-2.0", "peerDependencies": { "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.8", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.8.tgz", + "integrity": "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -7077,76 +6703,14 @@ "node": ">=20.0.0" } }, - "node_modules/@smithy/abort-controller": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", - "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/chunked-blob-reader": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", - "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/chunked-blob-reader-native": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", - "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-base64": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/config-resolver": { - "version": "4.4.11", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", - "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-config-provider": "^4.2.2", - "@smithy/util-endpoints": "^3.3.3", - "@smithy/util-middleware": "^4.2.12", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/@smithy/core": { - "version": "3.23.12", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", - "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-body-length-browser": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-stream": "^4.5.20", - "@smithy/util-utf8": "^4.2.2", - "@smithy/uuid": "^1.1.2", + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -7154,85 +6718,13 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", - "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-codec": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz", - "integrity": "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.13.1", - "@smithy/util-hex-encoding": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz", - "integrity": "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz", - "integrity": "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz", - "integrity": "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz", - "integrity": "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/eventstream-codec": "^4.2.12", - "@smithy/types": "^4.13.1", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -7240,72 +6732,13 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.15", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", - "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", - "@smithy/util-base64": "^4.3.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-blob-browser": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.13.tgz", - "integrity": "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/chunked-blob-reader": "^5.2.2", - "@smithy/chunked-blob-reader-native": "^4.2.3", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-node": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", - "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/hash-stream-node": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.12.tgz", - "integrity": "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/invalid-dependency": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", - "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -7313,215 +6746,25 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", - "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/md5-js": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.12.tgz", - "integrity": "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-content-length": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", - "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.26", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.26.tgz", - "integrity": "sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.12", - "@smithy/middleware-serde": "^4.2.15", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "@smithy/url-parser": "^4.2.12", - "@smithy/util-middleware": "^4.2.12", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-retry": { - "version": "4.4.43", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz", - "integrity": "sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/service-error-classification": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", - "@smithy/types": "^4.13.1", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-retry": "^4.2.12", - "@smithy/uuid": "^1.1.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-serde": { - "version": "4.2.15", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", - "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-stack": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", - "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/node-config-provider": { - "version": "4.3.12", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", - "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/shared-ini-file-loader": "^4.4.7", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, "node_modules/@smithy/node-http-handler": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", - "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/querystring-builder": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/property-provider": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", - "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/protocol-http": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", - "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-builder": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", - "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", - "@smithy/util-uri-escape": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/querystring-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", - "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/service-error-classification": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", - "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", - "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -7529,36 +6772,13 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.12", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", - "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-middleware": "^4.2.12", - "@smithy/util-uri-escape": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/smithy-client": { - "version": "4.12.6", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.6.tgz", - "integrity": "sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.23.12", - "@smithy/middleware-endpoint": "^4.4.26", - "@smithy/middleware-stack": "^4.2.12", - "@smithy/protocol-http": "^5.3.12", - "@smithy/types": "^4.13.1", - "@smithy/util-stream": "^4.5.20", + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", "tslib": "^2.6.2" }, "engines": { @@ -7566,61 +6786,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", - "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/url-parser": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", - "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/querystring-parser": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-base64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", - "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", - "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-body-length-node": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", - "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7630,184 +6798,29 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", - "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.2", + "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-config-provider": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", - "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.42", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.42.tgz", - "integrity": "sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.45", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.45.tgz", - "integrity": "sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/config-resolver": "^4.4.11", - "@smithy/credential-provider-imds": "^4.2.12", - "@smithy/node-config-provider": "^4.3.12", - "@smithy/property-provider": "^4.2.12", - "@smithy/smithy-client": "^4.12.6", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-endpoints": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", - "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/node-config-provider": "^4.3.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", - "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-middleware": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", - "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-retry": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", - "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/service-error-classification": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-stream": { - "version": "4.5.20", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", - "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/fetch-http-handler": "^5.3.15", - "@smithy/node-http-handler": "^4.5.0", - "@smithy/types": "^4.13.1", - "@smithy/util-base64": "^4.3.2", - "@smithy/util-buffer-from": "^4.2.2", - "@smithy/util-hex-encoding": "^4.2.2", - "@smithy/util-utf8": "^4.2.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-uri-escape": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", - "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, "node_modules/@smithy/util-utf8": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", - "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" }, "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-waiter": { - "version": "4.2.13", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.13.tgz", - "integrity": "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/abort-controller": "^4.2.12", - "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/uuid": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", - "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, "node_modules/@so-ric/colorspace": { @@ -7833,6 +6846,12 @@ "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@standard-schema/utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", @@ -7840,14 +6859,14 @@ "license": "MIT" }, "node_modules/@swc/core": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.3.tgz", - "integrity": "sha512-Qd8eBPkUFL4eAONgGjycZXj1jFCBW8Fd+xF0PzdTlBCWQIV1xnUT7B93wUANtW3KGjl3TRcOyxwSx/u/jyKw/Q==", + "version": "1.15.33", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.33.tgz", + "integrity": "sha512-jOlwnFV2xhuuZeAUILGFULeR6vDPfijEJ57evfocwznQldLU3w2cZ9bSDryY9ip+AsM3r1NJKzf47V2NXebkeQ==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.25" + "@swc/types": "^0.1.26" }, "engines": { "node": ">=10" @@ -7857,16 +6876,18 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.3", - "@swc/core-darwin-x64": "1.15.3", - "@swc/core-linux-arm-gnueabihf": "1.15.3", - "@swc/core-linux-arm64-gnu": "1.15.3", - "@swc/core-linux-arm64-musl": "1.15.3", - "@swc/core-linux-x64-gnu": "1.15.3", - "@swc/core-linux-x64-musl": "1.15.3", - "@swc/core-win32-arm64-msvc": "1.15.3", - "@swc/core-win32-ia32-msvc": "1.15.3", - "@swc/core-win32-x64-msvc": "1.15.3" + "@swc/core-darwin-arm64": "1.15.33", + "@swc/core-darwin-x64": "1.15.33", + "@swc/core-linux-arm-gnueabihf": "1.15.33", + "@swc/core-linux-arm64-gnu": "1.15.33", + "@swc/core-linux-arm64-musl": "1.15.33", + "@swc/core-linux-ppc64-gnu": "1.15.33", + "@swc/core-linux-s390x-gnu": "1.15.33", + "@swc/core-linux-x64-gnu": "1.15.33", + "@swc/core-linux-x64-musl": "1.15.33", + "@swc/core-win32-arm64-msvc": "1.15.33", + "@swc/core-win32-ia32-msvc": "1.15.33", + "@swc/core-win32-x64-msvc": "1.15.33" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -7878,9 +6899,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.3.tgz", - "integrity": "sha512-AXfeQn0CvcQ4cndlIshETx6jrAM45oeUrK8YeEY6oUZU/qzz0Id0CyvlEywxkWVC81Ajpd8TQQ1fW5yx6zQWkQ==", + "version": "1.15.33", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.33.tgz", + "integrity": "sha512-N+L0uXhuO7FIfzqwgxmzv0zIpV0qEp8wPX3QQs2p4atjMoywup2JTeDlXPw+z9pWJGCae3JjM+tZ6myclI+2gA==", "cpu": [ "arm64" ], @@ -7894,9 +6915,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.3.tgz", - "integrity": "sha512-p68OeCz1ui+MZYG4wmfJGvcsAcFYb6Sl25H9TxWl+GkBgmNimIiRdnypK9nBGlqMZAcxngNPtnG3kEMNnvoJ2A==", + "version": "1.15.33", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.33.tgz", + "integrity": "sha512-/Il4QHSOhV4FekbsDtkrNmKbsX26oSysvgrRswa/RYOHXAkwXDbB4jaeKq6PsJLSPkzJ2KzQ061gtBnk0vNHfA==", "cpu": [ "x64" ], @@ -7910,9 +6931,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.3.tgz", - "integrity": "sha512-Nuj5iF4JteFgwrai97mUX+xUOl+rQRHqTvnvHMATL/l9xE6/TJfPBpd3hk/PVpClMXG3Uvk1MxUFOEzM1JrMYg==", + "version": "1.15.33", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.33.tgz", + "integrity": "sha512-C64hBnBxq4viOPQ8hlx+2lJ23bzZBGnjw7ryALmS+0Q3zHmwO8lw1/DArLENw4Q18/0w5wdEO1k3m1wWNtKGqQ==", "cpu": [ "arm" ], @@ -7926,9 +6947,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.3.tgz", - "integrity": "sha512-2Nc/s8jE6mW2EjXWxO/lyQuLKShcmTrym2LRf5Ayp3ICEMX6HwFqB1EzDhwoMa2DcUgmnZIalesq2lG3krrUNw==", + "version": "1.15.33", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.33.tgz", + "integrity": "sha512-TRJfnJbX3jqpxRDRoieMzRiCBS5jOmXNb3iQXmcgjFEHKLnAgK1RZRU8Cq1MsPqO4jAJp/ld1G4O3fXuxv85uw==", "cpu": [ "arm64" ], @@ -7942,9 +6963,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.3.tgz", - "integrity": "sha512-j4SJniZ/qaZ5g8op+p1G9K1z22s/EYGg1UXIb3+Cg4nsxEpF5uSIGEE4mHUfA70L0BR9wKT2QF/zv3vkhfpX4g==", + "version": "1.15.33", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.33.tgz", + "integrity": "sha512-il7tYM+CpUNzieQbwAjFT1P8zqAhmGWNAGhQZBnxurXZ0aNn+5nqYFTEUKNZl7QibtT0uQXzTZrNGHCIj6Y1Og==", "cpu": [ "arm64" ], @@ -7957,10 +6978,42 @@ "node": ">=10" } }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.33", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.33.tgz", + "integrity": "sha512-ZtNBwN0Z7CFj9Il0FcPaKdjgP7URyKu/3RfH46vq+0paOBqLj4NYldD6Qo//Duif/7IOtAraUfDOmp0PLAufog==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.33", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.33.tgz", + "integrity": "sha512-De1IyajoOmhOYYjw/lx66bKlyDpHZTueqwpDrWgf5O7T6d1ODeJJO9/OqMBmrBQc5C+dNnlmIufHsp4QVCWufA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.3.tgz", - "integrity": "sha512-aKttAZnz8YB1VJwPQZtyU8Uk0BfMP63iDMkvjhJzRZVgySmqt/apWSdnoIcZlUoGheBrcqbMC17GGUmur7OT5A==", + "version": "1.15.33", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.33.tgz", + "integrity": "sha512-mGTH0YxmUN+x6vRN/I6NOk5X0ogNktkwPnJ94IMvR7QjhRDwL0O8RXEDhyUM0YtwWrryBOqaJQBX4zruxEPRGw==", "cpu": [ "x64" ], @@ -7974,9 +7027,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.3.tgz", - "integrity": "sha512-oe8FctPu1gnUsdtGJRO2rvOUIkkIIaHqsO9xxN0bTR7dFTlPTGi2Fhk1tnvXeyAvCPxLIcwD8phzKg6wLv9yug==", + "version": "1.15.33", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.33.tgz", + "integrity": "sha512-hj628ZkSEJf6zMf5VMbYrG2O6QqyTIp2qwY6VlCjvIa9lAEZ5c2lfPblCLVGYubTeLJDxadLB/CxqQYOQABeEQ==", "cpu": [ "x64" ], @@ -7990,9 +7043,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.3.tgz", - "integrity": "sha512-L9AjzP2ZQ/Xh58e0lTRMLvEDrcJpR7GwZqAtIeNLcTK7JVE+QineSyHp0kLkO1rttCHyCy0U74kDTj0dRz6raA==", + "version": "1.15.33", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.33.tgz", + "integrity": "sha512-GV2oohtN2/5+KSccl86VULu3aT+LrISC8uzgSq0FRnikpD+Zwc+sBlXmoKQ+Db6jI57ITUOIB8jRkdGMABC29g==", "cpu": [ "arm64" ], @@ -8006,9 +7059,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.3.tgz", - "integrity": "sha512-B8UtogMzErUPDWUoKONSVBdsgKYd58rRyv2sHJWKOIMCHfZ22FVXICR4O/VwIYtlnZ7ahERcjayBHDlBZpR0aw==", + "version": "1.15.33", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.33.tgz", + "integrity": "sha512-gtyvzSNR8DHKfFEA2uqb8Ld1myqi6uEg2jyeUq3ikn5ytYs7H8RpZYC8mdy4NXr8hfcdJfCLXPlYaqqfBXpoEQ==", "cpu": [ "ia32" ], @@ -8022,9 +7075,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.3.tgz", - "integrity": "sha512-SpZKMR9QBTecHeqpzJdYEfgw30Oo8b/Xl6rjSzBt1g0ZsXyy60KLXrp6IagQyfTYqNYE/caDvwtF2FPn7pomog==", + "version": "1.15.33", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.33.tgz", + "integrity": "sha512-d6fRqQSkJI+kmMEBWaDQ7TMl8+YjLYbwRUPZQ9DY0ORBJeTzOrG0twvfvlZ2xgw6jA0ScQKgfBm4vHLSLl5Hqg==", "cpu": [ "x64" ], @@ -8044,18 +7097,18 @@ "license": "Apache-2.0" }, "node_modules/@swc/helpers": { - "version": "0.5.17", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", - "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" } }, "node_modules/@swc/types": { - "version": "0.1.25", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.25.tgz", - "integrity": "sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==", + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", "license": "Apache-2.0", "dependencies": { "@swc/counter": "^0.1.3" @@ -8083,49 +7136,49 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", - "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", + "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "^5.19.0", + "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.2.2" + "tailwindcss": "4.3.0" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", - "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", + "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", "dev": true, "license": "MIT", "engines": { "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-arm64": "4.2.2", - "@tailwindcss/oxide-darwin-x64": "4.2.2", - "@tailwindcss/oxide-freebsd-x64": "4.2.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", - "@tailwindcss/oxide-linux-x64-musl": "4.2.2", - "@tailwindcss/oxide-wasm32-wasi": "4.2.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" + "@tailwindcss/oxide-android-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-arm64": "4.3.0", + "@tailwindcss/oxide-darwin-x64": "4.3.0", + "@tailwindcss/oxide-freebsd-x64": "4.3.0", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.0", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.0", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.0", + "@tailwindcss/oxide-linux-x64-musl": "4.3.0", + "@tailwindcss/oxide-wasm32-wasi": "4.3.0", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.0", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.0" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", - "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.0.tgz", + "integrity": "sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==", "cpu": [ "arm64" ], @@ -8140,9 +7193,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", - "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.0.tgz", + "integrity": "sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==", "cpu": [ "arm64" ], @@ -8157,9 +7210,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", - "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.0.tgz", + "integrity": "sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==", "cpu": [ "x64" ], @@ -8174,9 +7227,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", - "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.0.tgz", + "integrity": "sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==", "cpu": [ "x64" ], @@ -8191,9 +7244,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", - "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.0.tgz", + "integrity": "sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==", "cpu": [ "arm" ], @@ -8208,9 +7261,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", - "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.0.tgz", + "integrity": "sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==", "cpu": [ "arm64" ], @@ -8225,9 +7278,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", - "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.0.tgz", + "integrity": "sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==", "cpu": [ "arm64" ], @@ -8242,9 +7295,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", - "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.0.tgz", + "integrity": "sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==", "cpu": [ "x64" ], @@ -8259,9 +7312,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", - "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.0.tgz", + "integrity": "sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==", "cpu": [ "x64" ], @@ -8276,9 +7329,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", - "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.0.tgz", + "integrity": "sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -8294,10 +7347,10 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.8.1", - "@emnapi/runtime": "^1.8.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.1", + "@emnapi/core": "^1.10.0", + "@emnapi/runtime": "^1.10.0", + "@emnapi/wasi-threads": "^1.2.1", + "@napi-rs/wasm-runtime": "^1.1.4", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, @@ -8306,18 +7359,18 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.8.1", + "version": "1.10.0", "dev": true, "inBundle": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.1.0", + "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.8.1", + "version": "1.10.0", "dev": true, "inBundle": true, "license": "MIT", @@ -8327,7 +7380,7 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.1.0", + "version": "1.2.1", "dev": true, "inBundle": true, "license": "MIT", @@ -8337,19 +7390,21 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", + "version": "1.1.4", "dev": true, "inBundle": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { @@ -8370,9 +7425,9 @@ "optional": true }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", - "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.0.tgz", + "integrity": "sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==", "cpu": [ "arm64" ], @@ -8387,9 +7442,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", - "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.0.tgz", + "integrity": "sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==", "cpu": [ "x64" ], @@ -8404,23 +7459,23 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.2.tgz", - "integrity": "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.3.0.tgz", + "integrity": "sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.2.2", - "@tailwindcss/oxide": "4.2.2", - "postcss": "^8.5.6", - "tailwindcss": "4.2.2" + "@tailwindcss/node": "4.3.0", + "@tailwindcss/oxide": "4.3.0", + "postcss": "^8.5.10", + "tailwindcss": "4.3.0" } }, "node_modules/@tanstack/query-core": { - "version": "5.90.20", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", - "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz", + "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==", "license": "MIT", "funding": { "type": "github", @@ -8428,9 +7483,9 @@ } }, "node_modules/@tanstack/query-devtools": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.93.0.tgz", - "integrity": "sha512-+kpsx1NQnOFTZsw6HAFCW3HkKg0+2cepGtAWXjiiSOJJ1CtQpt72EE2nyZb+AjAbLRPoeRmPJ8MtQd8r8gsPdg==", + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.100.10.tgz", + "integrity": "sha512-3DmJf25hDPus5IpVvp6ujXv6bKV2zPzI9vpbAmpJigsL/H6DPvPjmf7/Q9yVKEke//8fgeQ45abjgnLuyYxAiw==", "dev": true, "license": "MIT", "funding": { @@ -8439,12 +7494,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.90.21", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", - "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", - "peer": true, + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.10.tgz", + "integrity": "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q==", + "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.90.20" + "@tanstack/query-core": "5.100.10" }, "funding": { "type": "github", @@ -8455,20 +7510,20 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.91.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.3.tgz", - "integrity": "sha512-nlahjMtd/J1h7IzOOfqeyDh5LNfG0eULwlltPEonYy0QL+nqrBB+nyzJfULV+moL7sZyxc2sHdNJki+vLA9BSA==", + "version": "5.100.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.100.10.tgz", + "integrity": "sha512-zes0+o9ef5rAZXJ9f/SeaLs2nufJaeVkZkl/Or9NGrWVF41kL9Od9ED9nCwtQlgiF2VGtrzhEw5AU/igAO+aAg==", "dev": true, "license": "MIT", "dependencies": { - "@tanstack/query-devtools": "5.93.0" + "@tanstack/query-devtools": "5.100.10" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.90.20", + "@tanstack/react-query": "^5.100.10", "react": "^18 || ^19" } }, @@ -8493,12 +7548,12 @@ } }, "node_modules/@tanstack/react-virtual": { - "version": "3.13.12", - "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz", - "integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==", + "version": "3.13.24", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.24.tgz", + "integrity": "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==", "license": "MIT", "dependencies": { - "@tanstack/virtual-core": "3.13.12" + "@tanstack/virtual-core": "3.14.0" }, "funding": { "type": "github", @@ -8523,9 +7578,9 @@ } }, "node_modules/@tanstack/virtual-core": { - "version": "3.13.12", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz", - "integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==", + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz", + "integrity": "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==", "license": "MIT", "funding": { "type": "github", @@ -8533,9 +7588,9 @@ } }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "license": "MIT", "optional": true, "dependencies": { @@ -8558,7 +7613,6 @@ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -8838,9 +7892,9 @@ "license": "MIT" }, "node_modules/@types/d3-shape": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", - "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "license": "MIT", "dependencies": { "@types/d3-path": "*" @@ -8894,9 +7948,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -8906,7 +7960,6 @@ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -8914,9 +7967,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", - "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", "dev": true, "license": "MIT", "dependencies": { @@ -8927,9 +7980,9 @@ } }, "node_modules/@types/express-session": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.2.tgz", - "integrity": "sha512-k+I0BxwVXsnEU2hV77cCobC08kIsn4y44C3gC0b46uxZVMaXA04lSPgRLR/bSL2w0t0ShJiG8o4jPzRG/nscFg==", + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.19.0.tgz", + "integrity": "sha512-GbypG0bog68UbOq2tSAp7SclvCUm3ha1uDi58OPRGK1NfRvCIu7Gz0M7fTGtpNG1T9a29GpuurQj9zEcT/lMXQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8997,20 +8050,19 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.3.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", - "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", + "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { - "undici-types": "~7.18.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/nodemailer": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz", - "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-8.0.0.tgz", + "integrity": "sha512-fyf8jWULsCo0d0BuoQ75i6IeoHs47qcqxWc7yUdUcV0pOZGjUTTOvwdG1PRXUDqN/8A64yQdQdnA2pZgcdi+cA==", "dev": true, "license": "MIT", "dependencies": { @@ -9025,12 +8077,11 @@ "license": "MIT" }, "node_modules/@types/pg": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz", - "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -9038,9 +8089,9 @@ } }, "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", "dev": true, "license": "MIT" }, @@ -9056,7 +8107,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, - "peer": true, + "license": "MIT", "dependencies": { "csstype": "^3.2.2" } @@ -9067,7 +8118,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -9154,7 +8204,14 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", - "optional": true + "optional": true, + "peer": true + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" }, "node_modules/@types/ws": { "version": "8.18.1", @@ -9184,20 +8241,20 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", - "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/type-utils": "8.56.1", - "@typescript-eslint/utils": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9207,9 +8264,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.56.1", + "@typescript-eslint/parser": "^8.59.3", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -9223,17 +8280,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", - "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3" }, "engines": { @@ -9245,18 +8301,18 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", - "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.56.1", - "@typescript-eslint/types": "^8.56.1", + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", "debug": "^4.4.3" }, "engines": { @@ -9267,18 +8323,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", - "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1" + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9289,9 +8345,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", - "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", "dev": true, "license": "MIT", "engines": { @@ -9302,21 +8358,21 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", - "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9327,13 +8383,13 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", - "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", "dev": true, "license": "MIT", "engines": { @@ -9345,21 +8401,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", - "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.56.1", - "@typescript-eslint/tsconfig-utils": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/visitor-keys": "8.56.1", + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9369,20 +8425,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/utils": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", - "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.56.1", - "@typescript-eslint/types": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1" + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -9393,17 +8449,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", - "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -9702,7 +8758,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -9720,10 +8775,22 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -9756,9 +8823,9 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -10037,13 +9104,13 @@ } }, "node_modules/asn1js": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.6.tgz", - "integrity": "sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.10.tgz", + "integrity": "sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==", "license": "BSD-3-Clause", "dependencies": { "pvtsutils": "^1.3.6", - "pvutils": "^1.1.3", + "pvutils": "^1.1.5", "tslib": "^2.8.1" }, "engines": { @@ -10089,9 +9156,9 @@ "license": "MIT" }, "node_modules/atomically": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.0.tgz", - "integrity": "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz", + "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10116,9 +9183,9 @@ } }, "node_modules/axe-core": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", - "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.4.tgz", + "integrity": "sha512-KunSNx+TVpkAw/6ULfhnx+HWRecjqZGTOyquAoWHYLRSdK1tB5Ihce1ZW+UY3fj33bYAFWPu7W/GRSmmrCGuxA==", "dev": true, "license": "MPL-2.0", "engines": { @@ -10126,13 +9193,14 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, @@ -10152,7 +9220,6 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.26.0" } @@ -10197,10 +9264,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.8", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", - "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", - "dev": true, + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -10224,7 +9290,6 @@ "integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -10264,9 +9329,9 @@ } }, "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", @@ -10275,7 +9340,7 @@ "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", - "qs": "^6.14.0", + "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" }, @@ -10288,9 +9353,9 @@ } }, "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -10310,9 +9375,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" @@ -10325,6 +9390,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -10334,9 +9400,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -10353,13 +9419,12 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -10415,15 +9480,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -10463,9 +9528,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001759", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", - "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", "funding": [ { "type": "opencollective", @@ -10515,14 +9580,11 @@ "license": "ISC" }, "node_modules/citty": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", - "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", "dev": true, - "license": "MIT", - "dependencies": { - "consola": "^3.2.3" - } + "license": "MIT" }, "node_modules/class-variance-authority": { "version": "0.7.1", @@ -10536,19 +9598,6 @@ "url": "https://polar.sh/cva" } }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -10569,58 +9618,6 @@ "node": ">=20" } }, - "node_modules/cliui/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", @@ -10749,9 +9746,9 @@ "license": "MIT" }, "node_modules/conf": { - "version": "15.0.2", - "resolved": "https://registry.npmjs.org/conf/-/conf-15.0.2.tgz", - "integrity": "sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw==", + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-15.1.0.tgz", + "integrity": "sha512-Uy5YN9KEu0WWDaZAVJ5FAmZoaJt9rdK6kH+utItPyGsCqCgaTKkrmZx3zoE0/3q6S3bcp3Ihkk+ZqPxWxFK5og==", "dev": true, "license": "MIT", "dependencies": { @@ -10773,9 +9770,9 @@ } }, "node_modules/conf/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "dev": true, "license": "MIT", "dependencies": { @@ -10796,27 +9793,10 @@ "dev": true, "license": "MIT" }, - "node_modules/confbox": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", - "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, "node_modules/content-disposition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", - "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", "engines": { "node": ">=18" @@ -10842,6 +9822,15 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/cookie-parser": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", @@ -10855,15 +9844,6 @@ "node": ">= 0.8.0" } }, - "node_modules/cookie-parser/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/cookie-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", @@ -10891,6 +9871,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -10905,12 +9886,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, "license": "ISC" }, "node_modules/cross-spawn/node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -10928,10 +9911,25 @@ "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==", "license": "MIT" }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true, "license": "MIT" }, "node_modules/d3": { @@ -11149,9 +10147,9 @@ } }, "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "license": "ISC", "engines": { "node": ">=12" @@ -11260,7 +10258,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -11480,12 +10477,6 @@ } } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "license": "MIT" - }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", @@ -11569,9 +10560,9 @@ } }, "node_modules/delaunator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", - "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", "license": "ISC", "dependencies": { "robust-predicates": "^3.0.2" @@ -11645,16 +10636,6 @@ "node": ">=0.10.0" } }, - "node_modules/dom-helpers": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", - "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.7", - "csstype": "^3.0.2" - } - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -11697,13 +10678,11 @@ } }, "node_modules/dompurify": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", - "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz", + "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==", "license": "(MPL-2.0 OR Apache-2.0)", - "engines": { - "node": ">=20" - }, + "peer": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -11739,9 +10718,9 @@ } }, "node_modules/dotenv": { - "version": "17.2.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", - "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -11926,13 +10905,13 @@ } }, "node_modules/eciesjs": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.16.tgz", - "integrity": "sha512-dS5cbA9rA2VR4Ybuvhg6jvdmp46ubLn3E+px8cG/35aEDNclrqoCjg6mt0HYZ/M+OoESS3jSkCrqk1kWAEhWAw==", + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/eciesjs/-/eciesjs-0.4.18.tgz", + "integrity": "sha512-wG99Zcfcys9fZux7Cft8BAX/YrOJLJSZ3jyYPfhZHqN2E+Ffx+QXBDsv3gubEgPtV6dTzJMSQUwk1H98/t/0wQ==", "dev": true, "license": "MIT", "dependencies": { - "@ecies/ciphers": "^0.2.4", + "@ecies/ciphers": "^0.2.5", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.7", "@noble/hashes": "^1.8.0" @@ -11950,9 +10929,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.321", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", - "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", + "version": "1.5.356", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.356.tgz", + "integrity": "sha512-9NgFd7m5t5MCJ5rUSjJITUXAH9mEGlrlofnMf4YEr+pz6JlP7cWmTAH+JFmbPnaSW8koVTkuW7pacORWAnA5Yw==", "dev": true, "license": "ISC" }, @@ -11963,6 +10942,16 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", + "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", @@ -11988,21 +10977,22 @@ } }, "node_modules/engine.io": { - "version": "6.6.4", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", - "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.7.tgz", + "integrity": "sha512-DgOngfDKM2EviOH3Mr9m7ks1q8roetLy/IMmYthAYzbpInMbYc/GS+fWFA3rl1gvwKVsQrVV61fo5emD1y3OJQ==", "dev": true, "license": "MIT", "dependencies": { "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", + "@types/ws": "^8.5.12", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.7.2", "cors": "~2.8.5", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1" + "ws": "~8.18.3" }, "engines": { "node": ">=10.2.0" @@ -12032,34 +11022,6 @@ "node": ">= 0.6" } }, - "node_modules/engine.io/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/engine.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/engine.io/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -12094,9 +11056,9 @@ } }, "node_modules/engine.io/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", "engines": { @@ -12116,14 +11078,14 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.20.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", - "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -12155,9 +11117,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", - "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -12242,16 +11204,16 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", - "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", + "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.24.1", + "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", @@ -12263,8 +11225,7 @@ "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", - "math-intrinsics": "^1.1.0", - "safe-array-concat": "^1.1.3" + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -12328,14 +11289,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.46.1.tgz", + "integrity": "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -12343,48 +11313,48 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/esbuild-node-externals": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/esbuild-node-externals/-/esbuild-node-externals-1.20.1.tgz", - "integrity": "sha512-uVs+TC+PBiav2LoTz8WZT/ootINw9Rns5JJyVznlfZH1qOyZxWCPzeXklY04UtZut5qUeFFaEWtcH7XoMwiTTQ==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/esbuild-node-externals/-/esbuild-node-externals-1.22.0.tgz", + "integrity": "sha512-5MOqbBYAjGayiA6JPCDJ+QOvW1Xl6M9LlQbYMgwbE0DE/pv6NyXKzPzQhksU6lY1ZB8MWMn6Ov6IGooMAZnTyQ==", "dev": true, "license": "MIT", "dependencies": { - "find-up": "^5.0.0" + "empathic": "^2.0.0" }, "engines": { "node": ">=12" }, "peerDependencies": { - "esbuild": "0.12 - 0.27" + "esbuild": "0.12 - 0.28" } }, "node_modules/escalade": { @@ -12416,19 +11386,18 @@ } }, "node_modules/eslint": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", - "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.3.0.tgz", + "integrity": "sha512-XbEXaRva5cF0ZQB8w6MluHA0kZZfV2DuCMJ3ozyEOHLwDpZX2Lmm/7Pp0xdJmI0GL1W05VH5VwIFHEm1Vcw2gw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.1", - "@eslint/plugin-kit": "^0.6.1", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -12439,7 +11408,7 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", - "espree": "^11.1.1", + "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", @@ -12473,13 +11442,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.1.7", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.7.tgz", - "integrity": "sha512-FTq1i/QDltzq+zf9aB/cKWAiZ77baG0V7h8dRQh3thVx7I4dwr6ZXQrWKAaTB7x5VwVXlzoUTyMLIVQPLj2gJg==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.6.tgz", + "integrity": "sha512-z2ELYSkyrrJ6cuunTU8vhsT/RpouPkjaSah06nVW6Rg2Hpg0Vs8s497/e5s8G8qtdp4ccsiovz5P1rv+5VSW2Q==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.1.7", + "@next/eslint-plugin-next": "16.2.6", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -12507,9 +11476,9 @@ "license": "MIT" }, "node_modules/eslint-config-next/node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -12558,7 +11527,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -12660,39 +11628,6 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, - "node_modules/eslint-config-next/node_modules/eslint-plugin-react-hooks": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", - "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" - } - }, - "node_modules/eslint-config-next/node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-config-next/node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", @@ -12706,30 +11641,6 @@ "node": "*" } }, - "node_modules/eslint-config-next/node_modules/resolve": { - "version": "2.0.0-next.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", - "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.1", - "node-exports-info": "^1.6.0", - "object-keys": "^1.1.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/eslint-config-next/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -12740,29 +11651,16 @@ "semver": "bin/semver.js" } }, - "node_modules/eslint-config-next/node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, "node_modules/eslint-import-resolver-node": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", - "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", + "integrity": "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==", "dev": true, "license": "MIT", "dependencies": { "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" + "is-core-module": "^2.16.1", + "resolve": "^2.0.0-next.6" } }, "node_modules/eslint-import-resolver-node/node_modules/debug": { @@ -12803,6 +11701,39 @@ "ms": "^2.1.1" } }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks/node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/eslint-scope": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", @@ -12909,9 +11840,9 @@ } }, "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, "node_modules/execa": { @@ -12952,7 +11883,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -12992,12 +11922,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", - "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", + "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", "dependencies": { - "ip-address": "10.1.0" + "ip-address": "^10.2.0" }, "engines": { "node": ">= 16" @@ -13009,15 +11939,6 @@ "express": ">= 4.11" } }, - "node_modules/express/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express/node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", @@ -13027,13 +11948,6 @@ "node": ">=6.6.0" } }, - "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -13041,15 +11955,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-equals": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.3.3.tgz", - "integrity": "sha512-/boTcHZeIAQ2r/tL11voclBHDeP9WPxLt+tyAbVSyyXuUFyh0Tne7gJZTqGbxnvj79TjLdCXLOY7UIPhyG5MTw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -13101,9 +12006,9 @@ "license": "Unlicense" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -13118,9 +12023,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", "funding": [ { "type": "github", @@ -13129,13 +12034,14 @@ ], "license": "MIT", "dependencies": { - "path-expression-matcher": "^1.1.3" + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" } }, "node_modules/fast-xml-parser": { - "version": "5.5.8", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", - "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", "funding": [ { "type": "github", @@ -13144,18 +12050,19 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.0", - "strnum": "^2.2.0" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -13218,6 +12125,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -13480,9 +12388,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "license": "MIT", "engines": { "node": ">=18" @@ -13569,9 +12477,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", - "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { @@ -13627,13 +12535,16 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { @@ -13763,9 +12674,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -13855,6 +12766,19 @@ "url": "https://opencollective.com/express" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -13878,9 +12802,9 @@ } }, "node_modules/icu-minify": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.8.3.tgz", - "integrity": "sha512-65Av7FLosNk7bPbmQx5z5XG2Y3T2GFppcjiXh4z1idHeVgQxlDpAmkGoYI0eFzAvrOnjpWTL5FmPDhsdfRMPEA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.12.0.tgz", + "integrity": "sha512-zDmM05uav3t3+kxSfRrNlmyXOdj2b+uHA+p04CG32eJabtaHbugXujuL+YfRkwP9joAnf0Uh+RMGCKD5NLa5rQ==", "funding": [ { "type": "individual", @@ -13922,6 +12846,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -13979,21 +12913,19 @@ } }, "node_modules/intl-messageformat": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.1.2.tgz", - "integrity": "sha512-ucSrQmZGAxfiBHfBRXW/k7UC8MaGFlEj4Ry1tKiDcmgwQm1y3EDl40u+4VNHYomxJQMJi9NEI3riDRlth96jKg==", + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.6.tgz", + "integrity": "sha512-afAN2yNN7zjB77G1ZC5L8GtLrEshyBvOQXz88flxCO/ocTIQist98gu0r/O6H/SSiQhQsOOtWPxmCEvtDABXXQ==", "license": "BSD-3-Clause", "dependencies": { - "@formatjs/ecma402-abstract": "3.1.1", - "@formatjs/fast-memoize": "3.1.0", - "@formatjs/icu-messageformat-parser": "3.5.1", - "tslib": "^2.8.1" + "@formatjs/fast-memoize": "3.1.5", + "@formatjs/icu-messageformat-parser": "3.5.9" } }, "node_modules/ioredis": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz", - "integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==", + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", "license": "MIT", "dependencies": { "@ioredis/commands": "1.5.1", @@ -14015,9 +12947,9 @@ } }, "node_modules/ip-address": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", - "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", "engines": { "node": ">= 12" @@ -14140,13 +13072,13 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -14277,6 +13209,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -14416,6 +13349,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -14470,13 +13416,13 @@ "license": "MIT" }, "node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", + "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/iterator.prototype": { @@ -14498,9 +13444,9 @@ } }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "dev": true, "license": "MIT", "bin": { @@ -14999,12 +13945,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -15059,6 +13999,23 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, + "node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", @@ -15089,13 +14046,12 @@ } }, "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, "node_modules/lucide-react": { @@ -15139,19 +14095,26 @@ } }, "node_modules/maxmind": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.5.tgz", - "integrity": "sha512-1lcH2kMjbBpCFhuHaMU32vz8CuOsKttRcWMQyXvtlklopCzN7NNHSVR/h9RYa8JPuFTGmkn2vYARm+7cIGuqDw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/maxmind/-/maxmind-5.0.6.tgz", + "integrity": "sha512-5bvd/u+kIaTqaGM+xkXjatzQw1dQfSmlLggr2W1EKMyMxSgx2woZyusLpNpZ4DdPmL+1bbJWeo4LXsi6bC0Iew==", "license": "MIT", "dependencies": { "mmdb-lib": "3.0.2", - "tiny-lru": "11.4.7" + "tiny-lru": "13.0.0" }, "engines": { "node": ">=12", "npm": ">=6" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -15217,6 +14180,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -15230,6 +14194,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -15308,12 +14273,12 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -15370,6 +14335,7 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", + "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -15380,6 +14346,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -15408,9 +14375,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -15464,14 +14431,14 @@ } }, "node_modules/next": { - "version": "15.5.15", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.15.tgz", - "integrity": "sha512-VSqCrJwtLVGwAVE0Sb/yikrQfkwkZW9p+lL/J4+xe+G3ZA+QnWPqgcfH1tDUEuk9y+pthzzVFp4L/U8JerMfMQ==", + "version": "16.2.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", + "integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", "license": "MIT", - "peer": true, "dependencies": { - "@next/env": "15.5.15", + "@next/env": "16.2.6", "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -15480,18 +14447,18 @@ "next": "dist/bin/next" }, "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.15", - "@next/swc-darwin-x64": "15.5.15", - "@next/swc-linux-arm64-gnu": "15.5.15", - "@next/swc-linux-arm64-musl": "15.5.15", - "@next/swc-linux-x64-gnu": "15.5.15", - "@next/swc-linux-x64-musl": "15.5.15", - "@next/swc-win32-arm64-msvc": "15.5.15", - "@next/swc-win32-x64-msvc": "15.5.15", - "sharp": "^0.34.3" + "@next/swc-darwin-arm64": "16.2.6", + "@next/swc-darwin-x64": "16.2.6", + "@next/swc-linux-arm64-gnu": "16.2.6", + "@next/swc-linux-arm64-musl": "16.2.6", + "@next/swc-linux-x64-gnu": "16.2.6", + "@next/swc-linux-x64-musl": "16.2.6", + "@next/swc-win32-arm64-msvc": "16.2.6", + "@next/swc-win32-x64-msvc": "16.2.6", + "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -15517,9 +14484,9 @@ } }, "node_modules/next-intl": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.8.3.tgz", - "integrity": "sha512-PvdBDWg+Leh7BR7GJUQbCDVVaBRn37GwDBWc9sv0rVQOJDQ5JU1rVzx9EEGuOGYo0DHAl70++9LQ7HxTawdL7w==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.12.0.tgz", + "integrity": "sha512-v8KpppWG0yLLlChJ3Of6uoPew9LeRDBAtY6vpJmF7YJmBZlHEzzoEL4w1g1dAU+VleEPNoXNm9hg1eEsKWV5hw==", "funding": [ { "type": "individual", @@ -15531,16 +14498,15 @@ "@formatjs/intl-localematcher": "^0.8.1", "@parcel/watcher": "^2.4.1", "@swc/core": "^1.15.2", - "icu-minify": "^4.8.3", + "icu-minify": "^4.12.0", "negotiator": "^1.0.0", - "next-intl-swc-plugin-extractor": "^4.8.3", + "next-intl-swc-plugin-extractor": "^4.12.0", "po-parser": "^2.1.1", - "use-intl": "^4.8.3" + "use-intl": "^4.12.0" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", - "typescript": "^5.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -15549,9 +14515,9 @@ } }, "node_modules/next-intl-swc-plugin-extractor": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.8.3.tgz", - "integrity": "sha512-YcaT+R9z69XkGhpDarVFWUprrCMbxgIQYPUaXoE6LGVnLjGdo8hu3gL6bramDVjNKViYY8a/pXPy7Bna0mXORg==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.12.0.tgz", + "integrity": "sha512-jUxVEu1Nryjt4YgaDktSys7ioOgQfcNPF/SF2dbPNxbVb6U+P1INRgHeCVN+EC59H2rnTFIQwbddmOCrUWFr3g==", "license": "MIT" }, "node_modules/next-themes": { @@ -15573,34 +14539,6 @@ "tslib": "^2.8.0" } }, - "node_modules/next/node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, "node_modules/nextjs-toploader": { "version": "3.9.17", "resolved": "https://registry.npmjs.org/nextjs-toploader/-/nextjs-toploader-3.9.17.tgz", @@ -15620,9 +14558,9 @@ } }, "node_modules/node-abi": { - "version": "3.85.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz", - "integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==", + "version": "3.92.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", + "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", "license": "MIT", "dependencies": { "semver": "^7.3.5" @@ -15679,16 +14617,16 @@ } }, "node_modules/node-releases": { - "version": "2.0.36", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", - "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", "dev": true, "license": "MIT" }, "node_modules/nodemailer": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz", - "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==", + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.7.tgz", + "integrity": "sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -15724,23 +14662,21 @@ "license": "MIT" }, "node_modules/nypm": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", - "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.6.tgz", + "integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==", "dev": true, "license": "MIT", "dependencies": { - "citty": "^0.1.6", - "consola": "^3.4.2", + "citty": "^0.2.2", "pathe": "^2.0.3", - "pkg-types": "^2.3.0", - "tinyexec": "^1.0.1" + "tinyexec": "^1.1.1" }, "bin": { "nypm": "dist/cli.mjs" }, "engines": { - "node": "^14.16.0 || >=16.10.0" + "node": ">=18" } }, "node_modules/object-assign": { @@ -16332,9 +15268,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", - "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "funding": [ { "type": "github", @@ -16350,6 +15286,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -16378,19 +15315,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz", - "integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/path-to-regexp": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", - "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -16428,7 +15356,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", @@ -16523,7 +15450,6 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -16532,16 +15458,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-types": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz", - "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "node_modules/picospinner": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/picospinner/-/picospinner-3.0.0.tgz", + "integrity": "sha512-lGA1TNsmy2bxvRsTI2cV01kfTwKzZjnZSDmF9llYNyMHMrU4sP87lQ5taiIKm88L3cbswjl008nwyGc3WpNvzg==", "dev": true, "license": "MIT", - "dependencies": { - "confbox": "^0.2.2", - "exsolve": "^1.0.7", - "pathe": "^2.0.3" + "engines": { + "node": ">=18.0.0" } }, "node_modules/plimit-lit": { @@ -16574,16 +15498,15 @@ } }, "node_modules/postal-mime": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.3.tgz", - "integrity": "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==", + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.4.tgz", + "integrity": "sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==", "license": "MIT-0" }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "dev": true, + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "funding": [ { "type": "opencollective", @@ -16618,9 +15541,9 @@ } }, "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", "license": "MIT", "engines": { "node": ">=0.10.0" @@ -16648,12 +15571,12 @@ } }, "node_modules/posthog-node": { - "version": "5.28.0", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.28.0.tgz", - "integrity": "sha512-EETYV0zA+7BLQmXzY+vGyDMoQK8uHf8f/1utbRjKncI41gPkw+4piGP7l4UT5Luld+4vQpJPOR1q1YrbXm7XjQ==", + "version": "5.34.1", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.34.1.tgz", + "integrity": "sha512-kGl0kSfh2+Ey3KL5Sji3yv9W5xwPK9sTkINRoFqCh9fbYXWWY6Zwi5Psv2QmRcbYiMJBk/iecnoOKVDRRga6PA==", "license": "MIT", "dependencies": { - "@posthog/core": "1.23.2" + "@posthog/core": "1.29.1" }, "engines": { "node": "^20.20.0 || >=22.22.0" @@ -16671,6 +15594,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", "license": "MIT", "dependencies": { "detect-libc": "^2.0.0", @@ -16704,9 +15628,9 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -16752,6 +15676,12 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -16775,9 +15705,9 @@ } }, "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -16822,9 +15752,9 @@ } }, "node_modules/qs": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", - "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -16892,9 +15822,9 @@ } }, "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -16932,15 +15862,35 @@ } }, "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", + "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } }, + "node_modules/react-aria": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.48.0.tgz", + "integrity": "sha512-jQjd4rBEIMqecBaAKYJbVGK6EqIHLa5znVQ7jwFyK5vCyljoj6KhgtiahmcIPsG5vG5vEDLw+ba+bEWn6A2P4w==", + "license": "Apache-2.0", + "dependencies": { + "@internationalized/date": "^3.12.1", + "@internationalized/number": "^3.6.6", + "@internationalized/string": "^3.2.8", + "@react-types/shared": "^3.34.0", + "@swc/helpers": "^0.5.0", + "aria-hidden": "^1.2.3", + "clsx": "^2.0.0", + "react-stately": "3.46.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, "node_modules/react-day-picker": { "version": "9.14.0", "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.14.0.tgz", @@ -16964,16 +15914,15 @@ } }, "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz", + "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.4" + "react": "^19.2.6" } }, "node_modules/react-easy-sort": { @@ -16993,64 +15942,44 @@ } }, "node_modules/react-email": { - "version": "5.2.10", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-5.2.10.tgz", - "integrity": "sha512-Ys8yR5/a0nXf5u2GlT2UV93PJHC3ZnuMnNebEn7I5UE9XfMFPtlpgDs02mPJOJn49fhJjDTWIUlZD1vmQPDgJg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-6.1.4.tgz", + "integrity": "sha512-UKCfry4W7zkAWoJX1ngaWgPrUazOebxI8IYrO8TBEqgFmmz97VqZ84ell2x36Fdvtzd/UI5e4ZOywlsXeydwgQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "7.27.0", "@babel/traverse": "7.27.0", + "@react-email/render": ">=2.0.8", "chokidar": "^4.0.3", "commander": "^13.0.0", "conf": "^15.0.2", + "css-tree": "3.2.1", "debounce": "^2.0.0", - "esbuild": "0.27.3", + "esbuild": "^0.28.0", "glob": "^13.0.6", "jiti": "2.4.2", "log-symbols": "^7.0.0", + "marked": "^15.0.12", "mime-types": "^3.0.0", "normalize-path": "^3.0.0", - "nypm": "0.6.2", - "ora": "^8.0.0", + "nypm": "0.6.6", + "picospinner": "^3.0.0", + "prismjs": "^1.30.0", "prompts": "2.4.2", "socket.io": "^4.8.1", + "tailwindcss": "^4.1.18", "tsconfig-paths": "4.2.0" }, "bin": { - "email": "dist/index.mjs" + "email": "dist/cli/index.mjs" }, "engines": { "node": ">=20.0.0" - } - }, - "node_modules/react-email/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/react-email/node_modules/cli-cursor": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", - "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/react-email/node_modules/commander": { @@ -17063,39 +15992,6 @@ "node": ">=18" } }, - "node_modules/react-email/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/react-email/node_modules/is-interactive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-email/node_modules/is-unicode-supported": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", - "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/react-email/node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -17106,141 +16002,6 @@ "jiti": "lib/jiti-cli.mjs" } }, - "node_modules/react-email/node_modules/log-symbols": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", - "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0", - "yoctocolors": "^2.1.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-email/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-email/node_modules/ora": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", - "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "cli-cursor": "^5.0.0", - "cli-spinners": "^2.9.2", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.0.0", - "log-symbols": "^6.0.0", - "stdin-discarder": "^0.2.2", - "string-width": "^7.2.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-email/node_modules/ora/node_modules/log-symbols": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "is-unicode-supported": "^1.3.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-email/node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-email/node_modules/restore-cursor": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", - "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/react-email/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/react-email/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/react-email/node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -17257,11 +16018,10 @@ } }, "node_modules/react-hook-form": { - "version": "7.71.2", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", - "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", + "version": "7.75.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.75.0.tgz", + "integrity": "sha512-Ovv94H+0p3sJ7B9B5QxPuCP1u8V/cHuVGyH55cSwodYDtoJwK+fqk3vjfIgSX59I2U/bU4z0nRJ9HMLpNiWEmw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -17283,10 +16043,34 @@ } }, "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" + "version": "19.2.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.6.tgz", + "integrity": "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } }, "node_modules/react-remove-scroll": { "version": "2.7.2", @@ -17335,19 +16119,21 @@ } } }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "license": "MIT", + "node_modules/react-stately": { + "version": "3.46.0", + "resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.46.0.tgz", + "integrity": "sha512-OdxhWvHgs2L4OJGIs7hnuTr5WjjMM6enhNEAMRqiekhF8+ITvA2LRwNftOZwcogaoCslGYq5S2VQTQwnm0GbCA==", + "license": "Apache-2.0", "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" + "@internationalized/date": "^3.12.1", + "@internationalized/number": "^3.6.6", + "@internationalized/string": "^3.2.8", + "@react-types/shared": "^3.34.0", + "@swc/helpers": "^0.5.0", + "use-sync-external-store": "^1.6.0" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, "node_modules/react-style-singleton": { @@ -17372,22 +16158,6 @@ } } }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -17417,43 +16187,35 @@ } }, "node_modules/recharts": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", - "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", "license": "MIT", + "workspaces": [ + "www" + ], "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "license": "MIT", - "dependencies": { - "decimal.js-light": "^2.4.1" - } - }, - "node_modules/recharts/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "license": "MIT" - }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -17475,6 +16237,21 @@ "node": ">=4" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect-metadata": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", @@ -17554,14 +16331,20 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resend": { - "version": "6.9.2", - "resolved": "https://registry.npmjs.org/resend/-/resend-6.9.2.tgz", - "integrity": "sha512-uIM6CQ08tS+hTCRuKBFbOBvHIGaEhqZe8s4FOgqsVXSbQLAhmNWpmUhG3UAtRnmcwTWFUqnHa/+Vux8YGPyDBA==", + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/resend/-/resend-6.12.3.tgz", + "integrity": "sha512-FkEi6YPnVL96/LvH8+QP7NaeaBy5brYXwlRqUCqZZeNL0/iyKij18IPmyPXYauT/2ODn1JG04qKz+qlJfzqzTw==", "license": "MIT", "dependencies": { - "postal-mime": "2.7.3", - "svix": "1.84.1" + "postal-mime": "2.7.4", + "svix": "1.92.2" }, "engines": { "node": ">=20" @@ -17576,13 +16359,16 @@ } }, "node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", "dev": true, "license": "MIT", "dependencies": { + "es-errors": "^1.3.0", "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -17618,9 +16404,9 @@ } }, "node_modules/robust-predicates": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", - "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", "license": "Unlicense" }, "node_modules/router": { @@ -17670,15 +16456,15 @@ "license": "BSD-3-Clause" }, "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, @@ -17778,9 +16564,9 @@ } }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -17790,31 +16576,35 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -17824,6 +16614,10 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/set-function-length": { @@ -17882,16 +16676,16 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", - "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "optional": true, "dependencies": { "@img/colour": "^1.0.0", - "detect-libc": "^2.1.0", - "semver": "^7.7.2" + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -17900,34 +16694,37 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.4", - "@img/sharp-darwin-x64": "0.34.4", - "@img/sharp-libvips-darwin-arm64": "1.2.3", - "@img/sharp-libvips-darwin-x64": "1.2.3", - "@img/sharp-libvips-linux-arm": "1.2.3", - "@img/sharp-libvips-linux-arm64": "1.2.3", - "@img/sharp-libvips-linux-ppc64": "1.2.3", - "@img/sharp-libvips-linux-s390x": "1.2.3", - "@img/sharp-libvips-linux-x64": "1.2.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", - "@img/sharp-libvips-linuxmusl-x64": "1.2.3", - "@img/sharp-linux-arm": "0.34.4", - "@img/sharp-linux-arm64": "0.34.4", - "@img/sharp-linux-ppc64": "0.34.4", - "@img/sharp-linux-s390x": "0.34.4", - "@img/sharp-linux-x64": "0.34.4", - "@img/sharp-linuxmusl-arm64": "0.34.4", - "@img/sharp-linuxmusl-x64": "0.34.4", - "@img/sharp-wasm32": "0.34.4", - "@img/sharp-win32-arm64": "0.34.4", - "@img/sharp-win32-ia32": "0.34.4", - "@img/sharp-win32-x64": "0.34.4" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -17940,6 +16737,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -17965,13 +16763,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -18087,16 +16885,16 @@ } }, "node_modules/socket.io": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", - "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", - "debug": "~4.3.2", + "debug": "~4.4.1", "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" @@ -18106,38 +16904,20 @@ } }, "node_modules/socket.io-adapter": { - "version": "2.5.5", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", - "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", "dev": true, "license": "MIT", "dependencies": { - "debug": "~4.3.4", - "ws": "~8.17.1" - } - }, - "node_modules/socket.io-adapter/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "debug": "~4.4.1", + "ws": "~8.18.3" } }, "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "dev": true, "license": "MIT", "engines": { @@ -18157,37 +16937,19 @@ } }, "node_modules/socket.io-parser": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", - "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.6.tgz", + "integrity": "sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==", "dev": true, "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1" + "debug": "~4.4.1" }, "engines": { "node": ">=10.0.0" } }, - "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/socket.io/node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -18202,24 +16964,6 @@ "node": ">= 0.6" } }, - "node_modules/socket.io/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/socket.io/node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -18364,19 +17108,6 @@ "node": ">= 0.8" } }, - "node_modules/stdin-discarder": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/stop-iteration-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", @@ -18400,6 +17131,29 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -18514,12 +17268,12 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -18579,9 +17333,9 @@ } }, "node_modules/strnum": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", - "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", "funding": [ { "type": "github", @@ -18644,32 +17398,18 @@ } }, "node_modules/svix": { - "version": "1.84.1", - "resolved": "https://registry.npmjs.org/svix/-/svix-1.84.1.tgz", - "integrity": "sha512-K8DPPSZaW/XqXiz1kEyzSHYgmGLnhB43nQCMeKjWGCUpLIpAMMM8kx3rVVOSm6Bo6EHyK1RQLPT4R06skM/MlQ==", + "version": "1.92.2", + "resolved": "https://registry.npmjs.org/svix/-/svix-1.92.2.tgz", + "integrity": "sha512-ZmuA3UVvlnF9EgxlzmPtF7CKjQb64Z6OFlyfdDfU0sdcC7dJa+3aOYX5B9mA+RS6ch1AxBa4UP/l6KmqfGtWBQ==", "license": "MIT", "dependencies": { - "standardwebhooks": "1.0.0", - "uuid": "^10.0.0" - } - }, - "node_modules/svix/node_modules/uuid": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", - "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" + "standardwebhooks": "1.0.0" } }, "node_modules/swagger-ui-dist": { - "version": "5.30.3", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.3.tgz", - "integrity": "sha512-giQl7/ToPxCqnUAx2wpnSnDNGZtGzw1LyUw6ZitIpTmdrvpxKFY/94v1hihm0zYNpgp1/VY0jTDk//R0BBgnRQ==", + "version": "5.32.6", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.6.tgz", + "integrity": "sha512-75ttZNaYCLoFPnozPZcTUU6mS3wKT8l7WLjU5zJSHFeJa23i5vtnze6IiCl4jDMPeQTXVXIgovq4M11NNfQvSA==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -18691,9 +17431,9 @@ } }, "node_modules/tabbable": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.3.0.tgz", - "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, "node_modules/tagged-tag": { @@ -18710,9 +17450,9 @@ } }, "node_modules/tailwind-merge": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", - "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", "license": "MIT", "funding": { "type": "github", @@ -18720,16 +17460,15 @@ } }, "node_modules/tailwindcss": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", - "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", - "license": "MIT", - "peer": true + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", + "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", - "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { @@ -18781,18 +17520,18 @@ "license": "MIT" }, "node_modules/tiny-lru": { - "version": "11.4.7", - "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.4.7.tgz", - "integrity": "sha512-w/Te7uMUVeH0CR8vZIjr+XiN41V+30lkDdK+NRIDCUYKKuL9VcmaUEmaPISuwGhLlrTGh5yu18lENtR9axSxYw==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-13.0.0.tgz", + "integrity": "sha512-xDHxKKS1FdF0Tv2P+QT7IeSEg74K/8cEDzbv3Tv6UyHHUgBOjOiQiBp818MGj66dhurQus/IBcoAbwIKtSGc6Q==", "license": "BSD-3-Clause", "engines": { - "node": ">=12" + "node": ">=14" } }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", "dev": true, "license": "MIT", "engines": { @@ -18800,14 +17539,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -18820,6 +17559,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -18867,9 +17607,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -18880,9 +17620,9 @@ } }, "node_modules/tsc-alias": { - "version": "1.8.16", - "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", - "integrity": "sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==", + "version": "1.8.17", + "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.17.tgz", + "integrity": "sha512-EIduCZHqbNwPm8BZYfq1aD7BQ697A4h6uSGMOFQfYGoQwfrYFTKwYfy9Bv42YxHkduVBcn9Zx0DkX111DKskyg==", "dev": true, "license": "MIT", "dependencies": { @@ -19008,14 +17748,13 @@ "license": "0BSD" }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.0.tgz", + "integrity": "sha512-8ccZMPD69s1AbKXx0C5ddTNZfNjwV04iIKgjZmKfKxMynEtSYcK0Lh7iQFh53fI5Yu4pb9usgAiqyPmEONaALg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" @@ -19086,9 +17825,9 @@ } }, "node_modules/type-fest": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.0.tgz", - "integrity": "sha512-d9CwU93nN0IA1QL+GSNDdwLAu1Ew5ZjTwupvedwg3WdfoH6pIDvYQ2hV0Uc2nKBLPq7NB5apCx57MLS5qlmO5g==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.6.0.tgz", + "integrity": "sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==", "dev": true, "license": "(MIT OR CC0-1.0)", "dependencies": { @@ -19102,17 +17841,34 @@ } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typed-array-buffer": { @@ -19194,12 +17950,11 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -19209,16 +17964,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.56.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", - "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.56.1", - "@typescript-eslint/parser": "8.56.1", - "@typescript-eslint/typescript-estree": "8.56.1", - "@typescript-eslint/utils": "8.56.1" + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -19229,7 +17984,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/uint8array-extras": { @@ -19265,9 +18020,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "devOptional": true, "license": "MIT" }, @@ -19378,9 +18133,9 @@ } }, "node_modules/use-debounce": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.1.0.tgz", - "integrity": "sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.1.1.tgz", + "integrity": "sha512-kvds8BHR2k28cFsxW8k3nc/tRga2rs1RHYCqmmGqb90MEeE++oALwzh2COiuBLO1/QXiOuShXoSN2ZpWnMmvuQ==", "license": "MIT", "engines": { "node": ">= 16.0.0" @@ -19390,9 +18145,9 @@ } }, "node_modules/use-intl": { - "version": "4.8.3", - "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.3.tgz", - "integrity": "sha512-nLxlC/RH+le6g3amA508Itnn/00mE+J22ui21QhOWo5V9hCEC43+WtnRAITbJW0ztVZphev5X9gvOf2/Dk9PLA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.12.0.tgz", + "integrity": "sha512-r+qVb7UI1+kiOhjYsmsNUCY+jrnjVopwGeFlmMyQj4YInlwZzgMeMSv9n8MqnWWy77HL5BVM8K2WgX50SbtcpA==", "funding": [ { "type": "individual", @@ -19403,7 +18158,7 @@ "dependencies": { "@formatjs/fast-memoize": "^3.1.0", "@schummar/icu-type-parser": "1.21.5", - "icu-minify": "^4.8.3", + "icu-minify": "^4.12.0", "intl-messageformat": "^11.1.0" }, "peerDependencies": { @@ -19448,9 +18203,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -19483,9 +18238,9 @@ } }, "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", @@ -19601,9 +18356,9 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -19627,7 +18382,6 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", @@ -19687,6 +18441,35 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -19694,9 +18477,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -19714,6 +18497,21 @@ } } }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -19740,9 +18538,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" @@ -19780,29 +18578,6 @@ "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "license": "MIT" - }, - "node_modules/yargs/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", @@ -19816,6 +18591,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yocto-spinner": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/yocto-spinner/-/yocto-spinner-1.2.0.tgz", + "integrity": "sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18.19" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", @@ -19830,11 +18621,10 @@ } }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 1da507c0d..950402793 100644 --- a/package.json +++ b/package.json @@ -32,10 +32,10 @@ "format": "prettier --write ." }, "dependencies": { - "@asteasolutions/zod-to-openapi": "8.4.1", - "@aws-sdk/client-s3": "3.1011.0", - "@faker-js/faker": "10.3.0", - "@headlessui/react": "2.2.9", + "@asteasolutions/zod-to-openapi": "8.5.0", + "@aws-sdk/client-s3": "3.1047.0", + "@faker-js/faker": "10.4.0", + "@headlessui/react": "2.2.10", "@hookform/resolvers": "5.2.2", "@monaco-editor/react": "4.7.0", "@node-rs/argon2": "2.0.2", @@ -59,16 +59,17 @@ "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-tooltip": "1.2.8", - "@react-email/components": "1.0.8", - "@react-email/render": "2.0.4", - "@react-email/tailwind": "2.0.5", + "@react-email/body": "0.3.0", + "@react-email/components": "1.0.12", + "@react-email/render": "2.0.8", + "@react-email/tailwind": "2.0.7", "@simplewebauthn/browser": "13.3.0", "@simplewebauthn/server": "13.3.0", "@tailwindcss/forms": "0.5.11", - "@tanstack/react-query": "5.90.21", + "@tanstack/react-query": "5.100.10", "@tanstack/react-table": "8.21.3", "arctic": "3.7.0", - "axios": "1.15.0", + "axios": "1.16.1", "better-sqlite3": "11.9.1", "canvas-confetti": "1.9.4", "class-variance-authority": "0.7.1", @@ -80,76 +81,76 @@ "d3": "7.9.0", "drizzle-orm": "0.45.2", "express": "5.2.1", - "express-rate-limit": "8.3.0", + "express-rate-limit": "8.5.2", "glob": "13.0.6", "helmet": "8.1.0", "http-errors": "2.0.1", "input-otp": "1.4.2", - "ioredis": "5.10.0", + "ioredis": "5.10.1", "jmespath": "0.16.0", "js-yaml": "4.1.1", "jsonwebtoken": "9.0.3", "lucide-react": "0.577.0", - "maxmind": "5.0.5", + "maxmind": "5.0.6", "moment": "2.30.1", - "next": "15.5.15", - "next-intl": "4.8.3", + "next": "16.2.6", + "next-intl": "4.12.0", "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", - "nodemailer": "8.0.5", + "nodemailer": "8.0.7", "oslo": "1.2.1", "pg": "8.20.0", - "posthog-node": "5.28.0", + "posthog-node": "5.34.1", "qrcode.react": "4.2.0", - "react": "19.2.4", + "react": "19.2.6", "react-day-picker": "9.14.0", - "react-dom": "19.2.4", + "react-dom": "19.2.6", "react-easy-sort": "1.8.0", - "react-hook-form": "7.71.2", + "react-hook-form": "7.75.0", "react-icons": "5.6.0", - "recharts": "2.15.4", + "recharts": "3.8.1", "reodotdev": "1.1.0", - "resend": "6.9.2", - "semver": "7.7.4", + "resend": "6.12.3", + "semver": "7.8.0", "sshpk": "1.18.0", "stripe": "20.4.1", "swagger-ui-express": "5.0.1", - "tailwind-merge": "3.5.0", + "tailwind-merge": "3.6.0", "topojson-client": "3.1.0", "tw-animate-css": "1.4.0", - "use-debounce": "10.1.0", - "uuid": "13.0.0", + "use-debounce": "10.1.1", + "uuid": "14.0.0", "vaul": "1.1.2", "visionscarto-world-atlas": "1.0.0", "winston": "3.19.0", "winston-daily-rotate-file": "5.0.0", - "ws": "8.19.0", - "yaml": "2.8.3", + "ws": "8.20.1", + "yaml": "2.9.0", "yargs": "18.0.0", - "zod": "4.3.6", + "zod": "4.4.3", "zod-validation-error": "5.0.0" }, "devDependencies": { - "@dotenvx/dotenvx": "1.54.1", + "@dotenvx/dotenvx": "1.66.0", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@react-email/preview-server": "5.2.10", - "@tailwindcss/postcss": "4.2.2", - "@tanstack/react-query-devtools": "5.91.3", + "@react-email/ui": "^6.1.4", + "@tailwindcss/postcss": "4.3.0", + "@tanstack/react-query-devtools": "5.100.10", "@types/better-sqlite3": "7.6.13", "@types/cookie-parser": "1.4.10", "@types/cors": "2.8.19", "@types/crypto-js": "4.2.2", "@types/d3": "7.4.3", "@types/express": "5.0.6", - "@types/express-session": "1.18.2", + "@types/express-session": "1.19.0", "@types/jmespath": "0.15.2", "@types/js-yaml": "4.0.9", "@types/jsonwebtoken": "9.0.10", - "@types/node": "25.3.5", - "@types/nodemailer": "7.0.11", + "@types/node": "25.8.0", + "@types/nodemailer": "8.0.0", "@types/nprogress": "0.2.3", - "@types/pg": "8.18.0", + "@types/pg": "8.20.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@types/semver": "7.7.1", @@ -160,21 +161,22 @@ "@types/yargs": "17.0.35", "babel-plugin-react-compiler": "1.0.0", "drizzle-kit": "0.31.10", - "esbuild": "0.27.4", - "esbuild-node-externals": "1.20.1", - "eslint": "10.0.3", - "eslint-config-next": "16.1.7", - "postcss": "8.5.8", - "prettier": "3.8.1", - "react-email": "5.2.10", - "tailwindcss": "4.2.2", - "tsc-alias": "1.8.16", - "tsx": "4.21.0", - "typescript": "5.9.3", - "typescript-eslint": "8.56.1" + "esbuild": "0.28.0", + "esbuild-node-externals": "1.22.0", + "eslint": "10.3.0", + "eslint-config-next": "16.2.6", + "postcss": "8.5.14", + "prettier": "3.8.3", + "react-email": "6.1.4", + "tailwindcss": "4.3.0", + "tsc-alias": "1.8.17", + "tsx": "4.22.0", + "typescript": "6.0.3", + "typescript-eslint": "8.59.3" }, "overrides": { - "esbuild": "0.27.4", - "dompurify": "3.3.2" + "esbuild": "0.28.0", + "dompurify": "3.4.0", + "postcss": "8.5.14" } } diff --git a/server/index.ts b/server/index.ts index e3a6ba049..99fd20156 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,5 +1,5 @@ #! /usr/bin/env node -import "./extendZod.ts"; +import "./extendZod"; import { runSetupFunctions } from "./setup"; import { createApiServer } from "./apiServer"; diff --git a/server/lib/requestParams.ts b/server/lib/requestParams.ts new file mode 100644 index 000000000..faf337bba --- /dev/null +++ b/server/lib/requestParams.ts @@ -0,0 +1,11 @@ +export function getFirstString(value: unknown): string | undefined { + if (typeof value === "string") { + return value; + } + + if (Array.isArray(value) && typeof value[0] === "string") { + return value[0]; + } + + return undefined; +} diff --git a/server/middlewares/integration/verifyAccessTokenAccess.ts b/server/middlewares/integration/verifyAccessTokenAccess.ts index c9a84f188..93d986c0a 100644 --- a/server/middlewares/integration/verifyAccessTokenAccess.ts +++ b/server/middlewares/integration/verifyAccessTokenAccess.ts @@ -4,6 +4,7 @@ import { resourceAccessToken, resources, apiKeyOrg } from "@server/db"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyApiKeyAccessTokenAccess( req: Request, @@ -12,7 +13,7 @@ export async function verifyApiKeyAccessTokenAccess( ) { try { const apiKey = req.apiKey; - const accessTokenId = req.params.accessTokenId; + const accessTokenId = getFirstString(req.params.accessTokenId); if (!apiKey) { return next( @@ -20,6 +21,12 @@ export async function verifyApiKeyAccessTokenAccess( ); } + if (!accessTokenId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid access token ID") + ); + } + const [accessToken] = await db .select() .from(resourceAccessToken) diff --git a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts b/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts index 48fbbf872..8358c814d 100644 --- a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts +++ b/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts @@ -4,6 +4,7 @@ import { apiKeys, apiKeyOrg } from "@server/db"; import { and, eq, or } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyApiKeyApiKeyAccess( req: Request, @@ -14,8 +15,10 @@ export async function verifyApiKeyApiKeyAccess( const { apiKey: callerApiKey } = req; const apiKeyId = - req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId; - const orgId = req.params.orgId; + getFirstString(req.params.apiKeyId) || + getFirstString(req.body.apiKeyId) || + getFirstString(req.query.apiKeyId); + const orgId = getFirstString(req.params.orgId); if (!callerApiKey) { return next( diff --git a/server/middlewares/integration/verifyApiKeyDomainAccess.ts b/server/middlewares/integration/verifyApiKeyDomainAccess.ts index db0f5d95d..3c09cb15e 100644 --- a/server/middlewares/integration/verifyApiKeyDomainAccess.ts +++ b/server/middlewares/integration/verifyApiKeyDomainAccess.ts @@ -3,6 +3,7 @@ import { db, domains, orgDomains, apiKeyOrg } from "@server/db"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyApiKeyDomainAccess( req: Request, @@ -12,8 +13,10 @@ export async function verifyApiKeyDomainAccess( try { const apiKey = req.apiKey; const domainId = - req.params.domainId || req.body.domainId || req.query.domainId; - const orgId = req.params.orgId; + getFirstString(req.params.domainId) || + getFirstString(req.body.domainId) || + getFirstString(req.query.domainId); + const orgId = getFirstString(req.params.orgId); if (!apiKey) { return next( @@ -27,6 +30,12 @@ export async function verifyApiKeyDomainAccess( ); } + if (!orgId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") + ); + } + if (apiKey.isRoot) { // Root keys can access any domain in any org return next(); diff --git a/server/middlewares/integration/verifyApiKeyIdpAccess.ts b/server/middlewares/integration/verifyApiKeyIdpAccess.ts index 99b7e76bc..0721d691f 100644 --- a/server/middlewares/integration/verifyApiKeyIdpAccess.ts +++ b/server/middlewares/integration/verifyApiKeyIdpAccess.ts @@ -4,6 +4,7 @@ import { idp, idpOrg, apiKeyOrg } from "@server/db"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyApiKeyIdpAccess( req: Request, @@ -12,8 +13,12 @@ export async function verifyApiKeyIdpAccess( ) { try { const apiKey = req.apiKey; - const idpId = req.params.idpId || req.body.idpId || req.query.idpId; - const orgId = req.params.orgId; + const idpIdRaw = + getFirstString(req.params.idpId) || + getFirstString(req.body.idpId) || + getFirstString(req.query.idpId); + const idpId = Number.parseInt(idpIdRaw ?? "", 10); + const orgId = getFirstString(req.params.orgId); if (!apiKey) { return next( @@ -27,7 +32,7 @@ export async function verifyApiKeyIdpAccess( ); } - if (!idpId) { + if (Number.isNaN(idpId)) { return next( createHttpError(HttpCode.BAD_REQUEST, "Invalid IDP ID") ); diff --git a/server/middlewares/integration/verifyApiKeyOrgAccess.ts b/server/middlewares/integration/verifyApiKeyOrgAccess.ts index 978800038..7c93ceeb1 100644 --- a/server/middlewares/integration/verifyApiKeyOrgAccess.ts +++ b/server/middlewares/integration/verifyApiKeyOrgAccess.ts @@ -4,6 +4,7 @@ import { apiKeyOrg } from "@server/db"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyApiKeyOrgAccess( req: Request, @@ -12,7 +13,7 @@ export async function verifyApiKeyOrgAccess( ) { try { const apiKeyId = req.apiKey?.apiKeyId; - const orgId = req.params.orgId; + const orgId = getFirstString(req.params.orgId); if (!apiKeyId) { return next( @@ -45,7 +46,7 @@ export async function verifyApiKeyOrgAccess( } if (!req.apiKeyOrg) { - next( + return next( createHttpError( HttpCode.FORBIDDEN, "Key does not have access to this organization" diff --git a/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts b/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts index 1fc11c314..849c1c66c 100644 --- a/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts +++ b/server/middlewares/integration/verifyApiKeySiteResourceAccess.ts @@ -4,6 +4,7 @@ import { siteResources, apiKeyOrg } from "@server/db"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyApiKeySiteResourceAccess( req: Request, @@ -12,7 +13,8 @@ export async function verifyApiKeySiteResourceAccess( ) { try { const apiKey = req.apiKey; - const siteResourceId = parseInt(req.params.siteResourceId); + const siteResourceIdRaw = getFirstString(req.params.siteResourceId); + const siteResourceId = Number.parseInt(siteResourceIdRaw ?? "", 10); if (!apiKey) { return next( @@ -20,7 +22,7 @@ export async function verifyApiKeySiteResourceAccess( ); } - if (!siteResourceId) { + if (Number.isNaN(siteResourceId)) { return next( createHttpError( HttpCode.BAD_REQUEST, diff --git a/server/middlewares/integration/verifyApiKeyTargetAccess.ts b/server/middlewares/integration/verifyApiKeyTargetAccess.ts index 71146c150..dac36b1e2 100644 --- a/server/middlewares/integration/verifyApiKeyTargetAccess.ts +++ b/server/middlewares/integration/verifyApiKeyTargetAccess.ts @@ -4,6 +4,7 @@ import { resources, targets, apiKeyOrg } from "@server/db"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyApiKeyTargetAccess( req: Request, @@ -12,7 +13,8 @@ export async function verifyApiKeyTargetAccess( ) { try { const apiKey = req.apiKey; - const targetId = parseInt(req.params.targetId); + const targetIdRaw = getFirstString(req.params.targetId); + const targetId = Number.parseInt(targetIdRaw ?? "", 10); if (!apiKey) { return next( @@ -20,7 +22,7 @@ export async function verifyApiKeyTargetAccess( ); } - if (isNaN(targetId)) { + if (Number.isNaN(targetId)) { return next( createHttpError(HttpCode.BAD_REQUEST, "Invalid target ID") ); diff --git a/server/middlewares/verifyAccessTokenAccess.ts b/server/middlewares/verifyAccessTokenAccess.ts index f1f2ca52e..528298727 100644 --- a/server/middlewares/verifyAccessTokenAccess.ts +++ b/server/middlewares/verifyAccessTokenAccess.ts @@ -7,6 +7,7 @@ import HttpCode from "@server/types/HttpCode"; import { canUserAccessResource } from "@server/auth/canUserAccessResource"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyAccessTokenAccess( req: Request, @@ -14,7 +15,7 @@ export async function verifyAccessTokenAccess( next: NextFunction ) { const userId = req.user!.userId; - const accessTokenId = req.params.accessTokenId; + const accessTokenId = getFirstString(req.params.accessTokenId); if (!userId) { return next( @@ -22,6 +23,12 @@ export async function verifyAccessTokenAccess( ); } + if (!accessTokenId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid access token ID") + ); + } + const [accessToken] = await db .select() .from(resourceAccessToken) @@ -87,7 +94,7 @@ export async function verifyAccessTokenAccess( } if (!req.userOrg) { - next( + return next( createHttpError( HttpCode.FORBIDDEN, "User does not have access to this organization" diff --git a/server/middlewares/verifyApiKeyAccess.ts b/server/middlewares/verifyApiKeyAccess.ts index b497892c8..2522a1e8b 100644 --- a/server/middlewares/verifyApiKeyAccess.ts +++ b/server/middlewares/verifyApiKeyAccess.ts @@ -6,6 +6,7 @@ import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyApiKeyAccess( req: Request, @@ -14,9 +15,24 @@ export async function verifyApiKeyAccess( ) { try { const userId = req.user!.userId; - const apiKeyId = - req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId; - const orgId = req.params.orgId; + const apiKeyIdFromParams = getFirstString(req.params?.apiKeyId); + const apiKeyIdFromBody = getFirstString(req.body?.apiKeyId); + + if ( + apiKeyIdFromParams && + apiKeyIdFromBody && + apiKeyIdFromParams !== apiKeyIdFromBody + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "API key ID provided in both URL and body with different values" + ) + ); + } + + const apiKeyId = apiKeyIdFromParams || apiKeyIdFromBody; + const orgId = getFirstString(req.params.orgId); if (!userId) { return next( @@ -104,10 +120,7 @@ export async function verifyApiKeyAccess( } } - req.userOrgRoleIds = await getUserOrgRoleIds( - req.userOrg.userId, - orgId - ); + req.userOrgRoleIds = await getUserOrgRoleIds(req.userOrg.userId, orgId); return next(); } catch (error) { diff --git a/server/middlewares/verifyDomainAccess.ts b/server/middlewares/verifyDomainAccess.ts index d37f6725d..783132a1a 100644 --- a/server/middlewares/verifyDomainAccess.ts +++ b/server/middlewares/verifyDomainAccess.ts @@ -6,6 +6,7 @@ import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyDomainAccess( req: Request, @@ -14,9 +15,8 @@ export async function verifyDomainAccess( ) { try { const userId = req.user!.userId; - const domainId = - req.params.domainId; - const orgId = req.params.orgId; + const domainId = getFirstString(req.params.domainId); + const orgId = getFirstString(req.params.orgId); if (!userId) { return next( @@ -62,10 +62,7 @@ export async function verifyDomainAccess( .select() .from(userOrgs) .where( - and( - eq(userOrgs.userId, userId), - eq(userOrgs.orgId, orgId) - ) + and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)) ) .limit(1); req.userOrg = userOrgRole[0]; diff --git a/server/middlewares/verifyLimits.ts b/server/middlewares/verifyLimits.ts index 667895309..94f645489 100644 --- a/server/middlewares/verifyLimits.ts +++ b/server/middlewares/verifyLimits.ts @@ -3,6 +3,7 @@ import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { usageService } from "@server/lib/billing/usageService"; import { build } from "@server/build"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyLimits( req: Request, @@ -13,7 +14,10 @@ export async function verifyLimits( return next(); } - const orgId = req.userOrgId || req.apiKeyOrg?.orgId || req.params.orgId; + const orgId = + req.userOrgId || + req.apiKeyOrg?.orgId || + getFirstString(req.params.orgId); if (!orgId) { return next(); // its fine if we silently fail here because this is not critical to operation or security and its better user experience if we dont fail diff --git a/server/middlewares/verifyOrgAccess.ts b/server/middlewares/verifyOrgAccess.ts index cb797afb0..e464f7b89 100644 --- a/server/middlewares/verifyOrgAccess.ts +++ b/server/middlewares/verifyOrgAccess.ts @@ -6,6 +6,7 @@ import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyOrgAccess( req: Request, @@ -13,7 +14,7 @@ export async function verifyOrgAccess( next: NextFunction ) { const userId = req.user!.userId; - const orgId = req.params.orgId; + const orgId = getFirstString(req.params.orgId); if (!userId) { return next( diff --git a/server/middlewares/verifySiteProvisioningKeyAccess.ts b/server/middlewares/verifySiteProvisioningKeyAccess.ts index bdf12c821..73393e1e9 100644 --- a/server/middlewares/verifySiteProvisioningKeyAccess.ts +++ b/server/middlewares/verifySiteProvisioningKeyAccess.ts @@ -1,10 +1,16 @@ import { Request, Response, NextFunction } from "express"; -import { db, userOrgs, siteProvisioningKeys, siteProvisioningKeyOrg } from "@server/db"; +import { + db, + userOrgs, + siteProvisioningKeys, + siteProvisioningKeyOrg +} from "@server/db"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifySiteProvisioningKeyAccess( req: Request, @@ -13,8 +19,10 @@ export async function verifySiteProvisioningKeyAccess( ) { try { const userId = req.user!.userId; - const siteProvisioningKeyId = req.params.siteProvisioningKeyId; - const orgId = req.params.orgId; + const siteProvisioningKeyId = getFirstString( + req.params.siteProvisioningKeyId + ); + const orgId = getFirstString(req.params.orgId); if (!userId) { return next( @@ -80,10 +88,7 @@ export async function verifySiteProvisioningKeyAccess( .where( and( eq(userOrgs.userId, userId), - eq( - userOrgs.orgId, - row.siteProvisioningKeyOrg.orgId - ) + eq(userOrgs.orgId, row.siteProvisioningKeyOrg.orgId) ) ) .limit(1); diff --git a/server/middlewares/verifyTargetAccess.ts b/server/middlewares/verifyTargetAccess.ts index 141a04549..8bbed6fca 100644 --- a/server/middlewares/verifyTargetAccess.ts +++ b/server/middlewares/verifyTargetAccess.ts @@ -7,6 +7,7 @@ import HttpCode from "@server/types/HttpCode"; import { canUserAccessResource } from "../auth/canUserAccessResource"; import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyTargetAccess( req: Request, @@ -14,7 +15,8 @@ export async function verifyTargetAccess( next: NextFunction ) { const userId = req.user!.userId; - const targetId = parseInt(req.params.targetId); + const targetIdRaw = getFirstString(req.params.targetId); + const targetId = Number.parseInt(targetIdRaw ?? "", 10); if (!userId) { return next( diff --git a/server/middlewares/verifyUserIsOrgOwner.ts b/server/middlewares/verifyUserIsOrgOwner.ts index 25ccf047c..5e3596374 100644 --- a/server/middlewares/verifyUserIsOrgOwner.ts +++ b/server/middlewares/verifyUserIsOrgOwner.ts @@ -4,6 +4,7 @@ import { userOrgs } from "@server/db"; import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyUserIsOrgOwner( req: Request, @@ -11,7 +12,7 @@ export async function verifyUserIsOrgOwner( next: NextFunction ) { const userId = req.user!.userId; - const orgId = req.params.orgId; + const orgId = getFirstString(req.params.orgId); if (!userId) { return next( diff --git a/server/private/middlewares/verifyCertificateAccess.ts b/server/private/middlewares/verifyCertificateAccess.ts index 3cd2e03be..3d86db0ef 100644 --- a/server/private/middlewares/verifyCertificateAccess.ts +++ b/server/private/middlewares/verifyCertificateAccess.ts @@ -19,6 +19,7 @@ import { eq, and } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import logger from "@server/logger"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyCertificateAccess( req: Request, @@ -27,11 +28,43 @@ export async function verifyCertificateAccess( ) { try { // Assume user/org access is already verified - const orgId = req.params.orgId; - const certId = - req.params.certId || req.body?.certId || req.query?.certId; - let domainId = - req.params.domainId || req.body?.domainId || req.query?.domainId; + const orgId = getFirstString(req.params.orgId); + + const certIdFromParams = getFirstString(req.params?.certId); + const certIdFromBody = getFirstString(req.body?.certId); + + if ( + certIdFromParams && + certIdFromBody && + certIdFromParams !== certIdFromBody + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Certificate ID provided in both URL and body with different values" + ) + ); + } + + const certId = certIdFromParams || certIdFromBody; + + const domainIdFromParams = getFirstString(req.params?.domainId); + const domainIdFromBody = getFirstString(req.body?.domainId); + + if ( + domainIdFromParams && + domainIdFromBody && + domainIdFromParams !== domainIdFromBody + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Domain ID provided in both URL and body with different values" + ) + ); + } + + let domainId = domainIdFromParams || domainIdFromBody; if (!orgId) { return next( @@ -65,7 +98,7 @@ export async function verifyCertificateAccess( ); } - domainId = cert.domainId; + domainId = cert.domainId ?? undefined; if (!domainId) { return next( createHttpError( diff --git a/server/private/middlewares/verifyIdpAccess.ts b/server/private/middlewares/verifyIdpAccess.ts index 29d997d3f..11906cb64 100644 --- a/server/private/middlewares/verifyIdpAccess.ts +++ b/server/private/middlewares/verifyIdpAccess.ts @@ -17,6 +17,7 @@ import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyIdpAccess( req: Request, @@ -25,8 +26,12 @@ export async function verifyIdpAccess( ) { try { const userId = req.user!.userId; - const idpId = req.params.idpId || req.body.idpId || req.query.idpId; - const orgId = req.params.orgId; + const idpIdRaw = + getFirstString(req.params.idpId) || + getFirstString(req.body?.idpId) || + getFirstString(req.query?.idpId); + const idpId = Number.parseInt(idpIdRaw ?? "", 10); + const orgId = getFirstString(req.params.orgId); if (!userId) { return next( @@ -40,7 +45,7 @@ export async function verifyIdpAccess( ); } - if (!idpId) { + if (Number.isNaN(idpId)) { return next( createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID") ); diff --git a/server/private/middlewares/verifyRemoteExitNodeAccess.ts b/server/private/middlewares/verifyRemoteExitNodeAccess.ts index 8c1a51e4c..8585759fd 100644 --- a/server/private/middlewares/verifyRemoteExitNodeAccess.ts +++ b/server/private/middlewares/verifyRemoteExitNodeAccess.ts @@ -18,6 +18,7 @@ import { and, eq } from "drizzle-orm"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { getUserOrgRoleIds } from "@server/lib/userOrgRoles"; +import { getFirstString } from "@server/lib/requestParams"; export async function verifyRemoteExitNodeAccess( req: Request, @@ -25,11 +26,11 @@ export async function verifyRemoteExitNodeAccess( next: NextFunction ) { const userId = req.user!.userId; // Assuming you have user information in the request - const orgId = req.params.orgId; + const orgId = getFirstString(req.params.orgId); const remoteExitNodeId = - req.params.remoteExitNodeId || - req.body.remoteExitNodeId || - req.query.remoteExitNodeId; + getFirstString(req.params.remoteExitNodeId) || + getFirstString(req.body?.remoteExitNodeId) || + getFirstString(req.query?.remoteExitNodeId); if (!userId) { return next( @@ -37,6 +38,15 @@ export async function verifyRemoteExitNodeAccess( ); } + if (!orgId || !remoteExitNodeId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid organization or remote exit node ID" + ) + ); + } + try { const [remoteExitNode] = await db .select() diff --git a/server/private/routers/generatedLicense/generateNewLicense.ts b/server/private/routers/generatedLicense/generateNewLicense.ts index f9349fc46..a97535d8a 100644 --- a/server/private/routers/generatedLicense/generateNewLicense.ts +++ b/server/private/routers/generatedLicense/generateNewLicense.ts @@ -16,40 +16,44 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { response as sendResponse } from "@server/lib/response"; +import { getFirstString } from "@server/lib/requestParams"; import privateConfig from "#private/lib/config"; import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types"; export interface CreateNewLicenseResponse { - data: Data - success: boolean - error: boolean - message: string - status: number + data: Data; + success: boolean; + error: boolean; + message: string; + status: number; } export interface Data { - licenseKey: LicenseKey + licenseKey: LicenseKey; } export interface LicenseKey { - id: number - instanceName: any - instanceId: string - licenseKey: string - tier: string - type: string - quantity: number - quantity_2: number - isValid: boolean - updatedAt: string - createdAt: string - expiresAt: string - paidFor: boolean - orgId: string - metadata: string + id: number; + instanceName: any; + instanceId: string; + licenseKey: string; + tier: string; + type: string; + quantity: number; + quantity_2: number; + isValid: boolean; + updatedAt: string; + createdAt: string; + expiresAt: string; + paidFor: boolean; + orgId: string; + metadata: string; } -export async function createNewLicense(orgId: string, licenseData: any): Promise { +export async function createNewLicense( + orgId: string, + licenseData: any +): Promise { try { const response = await fetch( `${privateConfig.getRawPrivateConfig().server.fossorial_api}/api/v1/license-internal/enterprise/${orgId}/create`, // this says enterprise but it does both @@ -80,7 +84,7 @@ export async function generateNewLicense( next: NextFunction ): Promise { try { - const { orgId } = req.params; + const orgId = getFirstString(req.params.orgId); if (!orgId) { return next( diff --git a/server/private/routers/generatedLicense/listGeneratedLicenses.ts b/server/private/routers/generatedLicense/listGeneratedLicenses.ts index 61b9d04f2..3b0d4818f 100644 --- a/server/private/routers/generatedLicense/listGeneratedLicenses.ts +++ b/server/private/routers/generatedLicense/listGeneratedLicenses.ts @@ -16,6 +16,7 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { response as sendResponse } from "@server/lib/response"; +import { getFirstString } from "@server/lib/requestParams"; import privateConfig from "#private/lib/config"; import { GeneratedLicenseKey, @@ -55,7 +56,7 @@ export async function listSaasLicenseKeys( next: NextFunction ): Promise { try { - const { orgId } = req.params; + const orgId = getFirstString(req.params.orgId); if (!orgId) { return next( diff --git a/server/routers/auth/securityKey.ts b/server/routers/auth/securityKey.ts index 9a1ee2cdc..2480ddd08 100644 --- a/server/routers/auth/securityKey.ts +++ b/server/routers/auth/securityKey.ts @@ -25,6 +25,7 @@ import { UserType } from "@server/types/UserTypes"; import { verifyPassword } from "@server/auth/password"; import { unauthorized } from "@server/auth/unauthorizedResponse"; import { verifyTotpCode } from "@server/auth/totp"; +import { getFirstString } from "@server/lib/requestParams"; // The RP ID is the domain name of your application const rpID = (() => { @@ -406,7 +407,12 @@ export async function deleteSecurityKey( res: Response, next: NextFunction ): Promise { - const { credentialId: encodedCredentialId } = req.params; + const encodedCredentialId = getFirstString(req.params.credentialId); + if (!encodedCredentialId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid credential ID") + ); + } const credentialId = decodeURIComponent(encodedCredentialId); const user = req.user as User; diff --git a/server/routers/domain/getDomain.ts b/server/routers/domain/getDomain.ts index 3e5565f92..1a5b1a369 100644 --- a/server/routers/domain/getDomain.ts +++ b/server/routers/domain/getDomain.ts @@ -8,7 +8,6 @@ import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import { domain } from "zod/v4/core/regexes"; const getDomainSchema = z.strictObject({ domainId: z.string().optional(), diff --git a/server/routers/resource/getUserResources.ts b/server/routers/resource/getUserResources.ts index 48df9735e..c26d819c4 100644 --- a/server/routers/resource/getUserResources.ts +++ b/server/routers/resource/getUserResources.ts @@ -19,6 +19,7 @@ import { import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; import { response } from "@server/lib/response"; +import { getFirstString } from "@server/lib/requestParams"; export async function getUserResources( req: Request, @@ -26,7 +27,7 @@ export async function getUserResources( next: NextFunction ): Promise { try { - const { orgId } = req.params; + const orgId = getFirstString(req.params.orgId); const userId = req.user?.userId; if (!userId) { @@ -35,6 +36,12 @@ export async function getUserResources( ); } + if (!orgId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID") + ); + } + // Check user is in organization and get their role IDs const [userOrg] = await db .select() diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index 9cf34b377..3a184a511 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -49,7 +49,7 @@ import { build } from "@server/build"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; import { UserType } from "@server/types/UserTypes"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import SetResourcePasswordForm from "components/SetResourcePasswordForm"; +import SetResourcePasswordForm from "@app/components/SetResourcePasswordForm"; import { Binary, Bot, InfoIcon, Key } from "lucide-react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx index 58d6a2704..7f324cb2a 100644 --- a/src/components/ui/chart.tsx +++ b/src/components/ui/chart.tsx @@ -101,16 +101,41 @@ ${colorConfig const ChartTooltip = RechartsPrimitive.Tooltip; +type ChartTooltipValue = number | string | Array; +type ChartTooltipName = number | string; +type ChartTooltipPayload = RechartsPrimitive.DefaultTooltipContentProps< + ChartTooltipValue, + ChartTooltipName +>["payload"]; +type ChartTooltipPayloadItem = NonNullable[number]; + +type ChartTooltipContentProps = React.ComponentProps<"div"> & { + active?: boolean; + payload?: ChartTooltipPayload; + label?: RechartsPrimitive.DefaultTooltipContentProps< + ChartTooltipValue, + ChartTooltipName + >["label"]; + labelFormatter?: RechartsPrimitive.DefaultTooltipContentProps< + ChartTooltipValue, + ChartTooltipName + >["labelFormatter"]; + formatter?: RechartsPrimitive.DefaultTooltipContentProps< + ChartTooltipValue, + ChartTooltipName + >["formatter"]; + hideLabel?: boolean; + hideIndicator?: boolean; + indicator?: "line" | "dot" | "dashed"; + nameKey?: string; + labelKey?: string; + color?: string; + labelClassName?: string; +}; + const ChartTooltipContent = React.forwardRef< HTMLDivElement, - React.ComponentProps & - React.ComponentProps<"div"> & { - hideLabel?: boolean; - hideIndicator?: boolean; - indicator?: "line" | "dot" | "dashed"; - nameKey?: string; - labelKey?: string; - } + ChartTooltipContentProps >( ( { @@ -187,8 +212,11 @@ const ChartTooltipContent = React.forwardRef< {!nestLabel ? tooltipLabel : null}
{payload - .filter((item) => item.type !== "none") - .map((item, index) => { + .filter( + (item: ChartTooltipPayloadItem) => + item.type !== "none" + ) + .map((item: ChartTooltipPayloadItem, index: number) => { const key = `${nameKey || item.name || item.dataKey || "value"}`; const itemConfig = getPayloadConfigFromPayload( @@ -201,7 +229,7 @@ const ChartTooltipContent = React.forwardRef< return (
svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground", indicator === "dot" && "items-center" @@ -301,13 +329,20 @@ ChartTooltipContent.displayName = "ChartTooltip"; const ChartLegend = RechartsPrimitive.Legend; +type ChartLegendPayload = + RechartsPrimitive.DefaultLegendContentProps["payload"]; +type ChartLegendPayloadItem = NonNullable[number]; + +type ChartLegendContentProps = React.ComponentProps<"div"> & { + payload?: ChartLegendPayload; + verticalAlign?: RechartsPrimitive.DefaultLegendContentProps["verticalAlign"]; + hideIcon?: boolean; + nameKey?: string; +}; + const ChartLegendContent = React.forwardRef< HTMLDivElement, - React.ComponentProps<"div"> & - Pick & { - hideIcon?: boolean; - nameKey?: string; - } + ChartLegendContentProps >( ( { @@ -335,8 +370,10 @@ const ChartLegendContent = React.forwardRef< )} > {payload - .filter((item) => item.type !== "none") - .map((item) => { + .filter( + (item: ChartLegendPayloadItem) => item.type !== "none" + ) + .map((item: ChartLegendPayloadItem) => { const key = `${nameKey || item.dataKey || "value"}`; const itemConfig = getPayloadConfigFromPayload( config, diff --git a/src/types/css.d.ts b/src/types/css.d.ts new file mode 100644 index 000000000..cbe652dbe --- /dev/null +++ b/src/types/css.d.ts @@ -0,0 +1 @@ +declare module "*.css"; diff --git a/tsconfig.enterprise.json b/tsconfig.enterprise.json index 60ed7c09b..fbb502f12 100644 --- a/tsconfig.enterprise.json +++ b/tsconfig.enterprise.json @@ -1,28 +1,49 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, - "baseUrl": "src", "paths": { - "@server/*": ["../server/*"], - "@test/*": ["../test/*"], - "@app/*": ["*"], - "@cli/*": ["../cli/*"], - "@/*": ["./*"], - "#private/*": ["../server/private/*"], - "#open/*": ["../server/*"], - "#closed/*": ["../server/private/*"], - "#dynamic/*": ["../server/private/*"] + "@server/*": [ + "./server/*" + ], + "@test/*": [ + "./test/*" + ], + "@app/*": [ + "./src/*" + ], + "@cli/*": [ + "./cli/*" + ], + "@/*": [ + "./src/*" + ], + "#private/*": [ + "./server/private/*" + ], + "#open/*": [ + "./server/*" + ], + "#closed/*": [ + "./server/private/*" + ], + "#dynamic/*": [ + "./server/private/*" + ] }, "plugins": [ { @@ -31,6 +52,14 @@ ], "target": "ES2024" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.d.ts", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } diff --git a/tsconfig.oss.json b/tsconfig.oss.json index f2157b29c..cc4f30b61 100644 --- a/tsconfig.oss.json +++ b/tsconfig.oss.json @@ -1,28 +1,49 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, - "baseUrl": "src", "paths": { - "@server/*": ["../server/*"], - "@test/*": ["../test/*"], - "@app/*": ["*"], - "@cli/*": ["../cli/*"], - "@/*": ["./*"], - "#private/*": ["../server/private/*"], - "#open/*": ["../server/*"], - "#closed/*": ["../server/private/*"], - "#dynamic/*": ["../server/*"] + "@server/*": [ + "./server/*" + ], + "@test/*": [ + "./test/*" + ], + "@app/*": [ + "./src/*" + ], + "@cli/*": [ + "./cli/*" + ], + "@/*": [ + "./src/*" + ], + "#private/*": [ + "./server/private/*" + ], + "#open/*": [ + "./server/*" + ], + "#closed/*": [ + "./server/private/*" + ], + "#dynamic/*": [ + "./server/*" + ] }, "plugins": [ { @@ -31,6 +52,14 @@ ], "target": "ES2024" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.d.ts", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } diff --git a/tsconfig.saas.json b/tsconfig.saas.json index 60ed7c09b..fbb502f12 100644 --- a/tsconfig.saas.json +++ b/tsconfig.saas.json @@ -1,28 +1,49 @@ { "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], "allowJs": true, "skipLibCheck": true, "strict": true, "noEmit": true, "esModuleInterop": true, "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, - "baseUrl": "src", "paths": { - "@server/*": ["../server/*"], - "@test/*": ["../test/*"], - "@app/*": ["*"], - "@cli/*": ["../cli/*"], - "@/*": ["./*"], - "#private/*": ["../server/private/*"], - "#open/*": ["../server/*"], - "#closed/*": ["../server/private/*"], - "#dynamic/*": ["../server/private/*"] + "@server/*": [ + "./server/*" + ], + "@test/*": [ + "./test/*" + ], + "@app/*": [ + "./src/*" + ], + "@cli/*": [ + "./cli/*" + ], + "@/*": [ + "./src/*" + ], + "#private/*": [ + "./server/private/*" + ], + "#open/*": [ + "./server/*" + ], + "#closed/*": [ + "./server/private/*" + ], + "#dynamic/*": [ + "./server/private/*" + ] }, "plugins": [ { @@ -31,6 +52,14 @@ ], "target": "ES2024" }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + "**/*.d.ts", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] } From 4c8f0cc9ec068d76658d7e36caae62da42d39b8b Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 15 May 2026 11:48:13 -0700 Subject: [PATCH 159/771] Pull in the destination from the api --- server/private/lib/acmeCertSync.ts | 6 +++--- server/routers/resource/getBrowserTarget.ts | 15 ++++++++++----- src/app/rdp/RdpClient.tsx | 6 +++--- src/app/rdp/page.tsx | 8 +++++--- src/app/ssh/page.tsx | 7 ++++--- src/app/vnc/page.tsx | 7 ++++--- 6 files changed, 29 insertions(+), 20 deletions(-) diff --git a/server/private/lib/acmeCertSync.ts b/server/private/lib/acmeCertSync.ts index b69c2ae89..6cb784736 100644 --- a/server/private/lib/acmeCertSync.ts +++ b/server/private/lib/acmeCertSync.ts @@ -780,9 +780,9 @@ async function syncAcmeCerts(acmeJsonPath: string): Promise { } } - logger.debug( - `acmeCertSync: cert for ${mainDomain} covers ${allDomains.size} domain(s): ${[...allDomains].join(", ")}` - ); + // logger.debug( + // `acmeCertSync: cert for ${mainDomain} covers ${allDomains.size} domain(s): ${[...allDomains].join(", ")}` + // ); for (const domain of allDomains) { try { diff --git a/server/routers/resource/getBrowserTarget.ts b/server/routers/resource/getBrowserTarget.ts index 3baab4540..b8ddd43dc 100644 --- a/server/routers/resource/getBrowserTarget.ts +++ b/server/routers/resource/getBrowserTarget.ts @@ -38,10 +38,12 @@ export async function getBrowserTarget( const { fullDomain } = parsed.data; - const [row] = await db + logger.info(`Retrieving browser target for domain: ${fullDomain}`); + + const [browserTarget] = await db .select({ - ip: browserGatewayTarget.destination, - port: browserGatewayTarget.destinationPort + destination: browserGatewayTarget.destination, + destinationPort: browserGatewayTarget.destinationPort }) .from(browserGatewayTarget) .innerJoin( @@ -51,7 +53,7 @@ export async function getBrowserTarget( .where(eq(resources.fullDomain, fullDomain)) .limit(1); - if (!row) { + if (!browserTarget) { return next( createHttpError( HttpCode.NOT_FOUND, @@ -61,7 +63,10 @@ export async function getBrowserTarget( } return response(res, { - data: { ip: row.ip, port: row.port }, + data: { + ip: browserTarget.destination, + port: browserTarget.destinationPort + }, success: true, error: false, message: "Browser target retrieved successfully", diff --git a/src/app/rdp/RdpClient.tsx b/src/app/rdp/RdpClient.tsx index 5b61e527d..5114646cb 100644 --- a/src/app/rdp/RdpClient.tsx +++ b/src/app/rdp/RdpClient.tsx @@ -223,9 +223,9 @@ export default function RdpClient({ ); } - const destination = target - ? `${target.ip}:${target.port}` - : ""; + const destination = target ? `${target.ip}:${target.port}` : ""; + + console.log("Starting RDP session with destination:", destination); const builder = userInteraction .configBuilder() diff --git a/src/app/rdp/page.tsx b/src/app/rdp/page.tsx index 75cc5a852..7c2a2d47c 100644 --- a/src/app/rdp/page.tsx +++ b/src/app/rdp/page.tsx @@ -1,5 +1,5 @@ import { headers } from "next/headers"; -import { internal } from "@app/lib/api"; +import { priv } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { GetBrowserTargetResponse } from "@server/routers/resource"; import RdpClient from "./RdpClient"; @@ -19,11 +19,13 @@ export default async function RdpPage() { let error: string | null = null; try { - const res = await internal.get>( + const res = await priv.get>( `/resource/browser-target?fullDomain=${encodeURIComponent(hostname)}` ); target = res.data.data; - } catch { + console.log("Fetched browser target:", target); + } catch (error) { + console.error("Error fetching browser target:", error); error = "No resource found for this domain"; } diff --git a/src/app/ssh/page.tsx b/src/app/ssh/page.tsx index b0e0b37b2..94fa8ce58 100644 --- a/src/app/ssh/page.tsx +++ b/src/app/ssh/page.tsx @@ -1,5 +1,5 @@ import { headers } from "next/headers"; -import { internal } from "@app/lib/api"; +import { priv } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { GetBrowserTargetResponse } from "@server/routers/resource"; import SshClient from "./SshClient"; @@ -19,11 +19,12 @@ export default async function SshPage() { let error: string | null = null; try { - const res = await internal.get>( + const res = await priv.get>( `/resource/browser-target?fullDomain=${encodeURIComponent(hostname)}` ); target = res.data.data; - } catch { + } catch (error) { + console.error("Error fetching browser target:", error); error = "No resource found for this domain"; } diff --git a/src/app/vnc/page.tsx b/src/app/vnc/page.tsx index 6c1827e68..16a07b214 100644 --- a/src/app/vnc/page.tsx +++ b/src/app/vnc/page.tsx @@ -1,5 +1,5 @@ import { headers } from "next/headers"; -import { internal } from "@app/lib/api"; +import { priv } from "@app/lib/api"; import { AxiosResponse } from "axios"; import { GetBrowserTargetResponse } from "@server/routers/resource"; import VncClient from "./VncClient"; @@ -19,11 +19,12 @@ export default async function VncPage() { let error: string | null = null; try { - const res = await internal.get>( + const res = await priv.get>( `/resource/browser-target?fullDomain=${encodeURIComponent(hostname)}` ); target = res.data.data; - } catch { + } catch (error) { + console.error("Error fetching browser target:", error); error = "No resource found for this domain"; } From 2ddbdf977bebe93f6adceb8d77092ce246cc1282 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 15 May 2026 12:06:05 -0700 Subject: [PATCH 160/771] Standardize the ui --- src/app/ssh/SshClient.tsx | 92 +++++++++++++++++++++------------------ src/app/vnc/VncClient.tsx | 4 +- 2 files changed, 51 insertions(+), 45 deletions(-) diff --git a/src/app/ssh/SshClient.tsx b/src/app/ssh/SshClient.tsx index 0c96f1355..86f5bdda4 100644 --- a/src/app/ssh/SshClient.tsx +++ b/src/app/ssh/SshClient.tsx @@ -190,26 +190,22 @@ export default function SshClient({ if (error) { return ( -
-

{error}

+
+

{error}

); } return ( -
-

SSH Terminal

- +
{!connected && ( -
-
-
- +
+

+ SSH Terminal +

+ +
+ -
- -
- + + -
+ + + {connectError && ( +

+ {connectError} +

+ )} + +
- - {connectError && ( -

{connectError}

- )} - -
)} {connected && ( -
-
+
+
@@ -281,3 +270,20 @@ export default function SshClient({
); } + +function Field({ + label, + id, + children +}: { + label: string; + id: string; + children: React.ReactNode; +}) { + return ( +
+ + {children} +
+ ); +} diff --git a/src/app/vnc/VncClient.tsx b/src/app/vnc/VncClient.tsx index 373eb7763..174921eed 100644 --- a/src/app/vnc/VncClient.tsx +++ b/src/app/vnc/VncClient.tsx @@ -181,7 +181,7 @@ export default function VncClient({ className="flex h-screen flex-col bg-neutral-900" style={{ display: connected ? "flex" : "none" }} > -
+
From 00e1675f7bc58b8aeb07753c0631761f756646b5 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 15 May 2026 12:08:40 -0700 Subject: [PATCH 161/771] Remove extra fields --- src/app/rdp/RdpClient.tsx | 42 +++++---------------------------------- src/app/rdp/page.tsx | 2 +- src/app/ssh/page.tsx | 2 +- src/app/vnc/page.tsx | 2 +- 4 files changed, 8 insertions(+), 40 deletions(-) diff --git a/src/app/rdp/RdpClient.tsx b/src/app/rdp/RdpClient.tsx index 5114646cb..75f351548 100644 --- a/src/app/rdp/RdpClient.tsx +++ b/src/app/rdp/RdpClient.tsx @@ -43,8 +43,6 @@ type FormState = { domain: string; kdcProxyUrl: string; pcb: string; - desktopWidth: number; - desktopHeight: number; enableClipboard: boolean; }; @@ -70,8 +68,6 @@ export default function RdpClient({ domain: "", kdcProxyUrl: "", pcb: "", - desktopWidth: 1280, - desktopHeight: 720, enableClipboard: true }); @@ -238,8 +234,8 @@ export default function RdpClient({ .withServerDomain(form.domain) .withAuthToken("test-token") .withDesktopSize({ - width: form.desktopWidth, - height: form.desktopHeight + width: window.innerWidth, + height: window.innerHeight }) .withExtension(exts.displayControl(true)); @@ -349,34 +345,7 @@ export default function RdpClient({ onChange={(e) => update("pcb", e.target.value)} /> */} -
- - - update( - "desktopWidth", - Number(e.target.value) || 0 - ) - } - /> - - - - update( - "desktopHeight", - Number(e.target.value) || 0 - ) - } - /> - -
+ {/* */} -
+ {/*
Enable Clipboard -
- +
*/}
diff --git a/src/app/vnc/VncClient.tsx b/src/app/vnc/VncClient.tsx index 7718fd665..9df0d97b9 100644 --- a/src/app/vnc/VncClient.tsx +++ b/src/app/vnc/VncClient.tsx @@ -96,8 +96,6 @@ export default function VncClient({ }); const wsUrl = `${base}?${params.toString()}`; - toast({ title: "Connecting…", description: wsUrl }); - // Clear the container so noVNC gets a clean mount point. screenRef.current.innerHTML = ""; @@ -113,7 +111,6 @@ export default function VncClient({ rfb.resizeSession = true; rfb.addEventListener("connect", () => { - toast({ title: "Connected" }); setConnected(true); }); @@ -122,10 +119,6 @@ export default function VncClient({ (e: { detail: { clean: boolean } }) => { rfbRef.current = null; setConnected(false); - toast({ - title: e.detail.clean ? "Disconnected" : "Connection lost", - variant: e.detail.clean ? "default" : "destructive" - }); } ); From 0f9a6fd968aed2c75e0bbd88d786962441f6611c Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 15 May 2026 16:07:14 -0700 Subject: [PATCH 164/771] Support private key --- src/app/ssh/SshClient.tsx | 94 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 5 deletions(-) diff --git a/src/app/ssh/SshClient.tsx b/src/app/ssh/SshClient.tsx index 817bb1f30..a84b1dacc 100644 --- a/src/app/ssh/SshClient.tsx +++ b/src/app/ssh/SshClient.tsx @@ -5,6 +5,7 @@ import { useEffect, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; type Target = { ip: string; @@ -15,6 +16,7 @@ type Target = { type FormState = { username: string; password: string; + privateKey: string; }; export default function SshClient({ @@ -26,9 +28,27 @@ export default function SshClient({ }) { const [form, setForm] = useState({ username: "", - password: "" + password: "", + privateKey: "" }); + const fileInputRef = useRef(null); + + function handleKeyFile(e: React.ChangeEvent) { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + const text = ev.target?.result; + if (typeof text === "string") { + setForm((prev) => ({ ...prev, privateKey: text })); + } + }; + reader.readAsText(file); + // Reset input so the same file can be re-selected if needed. + e.target.value = ""; + } + const [connected, setConnected] = useState(false); const [connecting, setConnecting] = useState(false); const [connectError, setConnectError] = useState(null); @@ -143,9 +163,15 @@ export default function SshClient({ wsRef.current = ws; ws.onopen = () => { - // Send the password (or empty string) as the first frame so the - // proxy can complete SSH authentication before piping pty data. - ws.send(JSON.stringify({ type: "auth", password: form.password })); + // Send credentials as the first frame so the proxy can complete + // SSH authentication before piping pty data. + ws.send( + JSON.stringify({ + type: "auth", + password: form.password, + privateKey: form.privateKey + }) + ); setConnecting(false); setConnected(true); }; @@ -236,6 +262,60 @@ export default function SshClient({ password: e.target.value }) } + placeholder={ + form.privateKey + ? "Optional with key auth" + : "" + } + /> + + + +