From 0abc561bb843d07b19ef44456846525af672bb09 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 11 Nov 2025 02:22:26 +0100 Subject: [PATCH 001/153] =?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/app/auth/resource/[resourceGuid]/page.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/auth/resource/[resourceGuid]/page.tsx b/src/app/auth/resource/[resourceGuid]/page.tsx index 26d8bbea..d51f2210 100644 --- a/src/app/auth/resource/[resourceGuid]/page.tsx +++ b/src/app/auth/resource/[resourceGuid]/page.tsx @@ -50,7 +50,9 @@ export default async function ResourceAuthPage(props: { if (res && res.status === 200) { authInfo = res.data.data; } - } catch (e) {} + } catch (e) { + console.error(e); + } const getUser = cache(verifySession); const user = await getUser({ skipCheckVerifyEmail: true }); From 5641a2aa316589d35282380cac19f11a1a820ebf Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 11 Nov 2025 17:08:27 +0100 Subject: [PATCH 002/153] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20add=20org=20aut?= =?UTF-8?q?h=20page=20model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/schema.ts | 15 ++++++++++++ server/db/sqlite/schema/schema.ts | 24 +++++++++++++++---- .../routers/resource/getResourceAuthInfo.ts | 1 - 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index ffbe820c..15c1942b 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -64,6 +64,20 @@ export const orgDomains = pgTable("orgDomains", { .references(() => domains.domainId, { onDelete: "cascade" }) }); +export const orgAuthPages = pgTable("orgAuthPages", { + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + orgAuthPageId: serial("orgAuthPageId").primaryKey(), + logoUrl: text("logoUrl"), + logoWidth: integer("logoWidth"), + logoHeight: integer("logoHeight"), + title: text("title"), + subtitle: text("subtitle"), + resourceTitle: text("resourceTitle"), + resourceSubtitle: text("resourceSubtitle") +}); + export const sites = pgTable("sites", { siteId: serial("siteId").primaryKey(), orgId: varchar("orgId") @@ -809,3 +823,4 @@ export type LicenseKey = InferSelectModel; export type SecurityKey = InferSelectModel; export type WebauthnChallenge = InferSelectModel; export type RequestAuditLog = InferSelectModel; +export type OrgAuthPage = InferSelectModel; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 13453d2e..6e5e49e7 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -25,11 +25,10 @@ export const dnsRecords = sqliteTable("dnsRecords", { recordType: text("recordType").notNull(), // "NS" | "CNAME" | "A" | "TXT" baseDomain: text("baseDomain"), - value: text("value").notNull(), - verified: integer("verified", { mode: "boolean" }).notNull().default(false), + value: text("value").notNull(), + verified: integer("verified", { mode: "boolean" }).notNull().default(false) }); - export const orgs = sqliteTable("orgs", { orgId: text("orgId").primaryKey(), name: text("name").notNull(), @@ -67,6 +66,20 @@ export const orgDomains = sqliteTable("orgDomains", { .references(() => domains.domainId, { onDelete: "cascade" }) }); +export const orgAuthPages = sqliteTable("orgAuthPages", { + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + orgAuthPageId: integer("orgAuthPageId").primaryKey({ autoIncrement: true }), + logoUrl: text("logoUrl"), + logoWidth: integer("logoWidth"), + logoHeight: integer("logoHeight"), + title: text("title"), + subtitle: text("subtitle"), + resourceTitle: text("resourceTitle"), + resourceSubtitle: text("resourceSubtitle") +}); + export const sites = sqliteTable("sites", { siteId: integer("siteId").primaryKey({ autoIncrement: true }), orgId: text("orgId") @@ -142,9 +155,10 @@ export const resources = sqliteTable("resources", { onDelete: "set null" }), headers: text("headers"), // comma-separated list of headers to add to the request - proxyProtocol: integer("proxyProtocol", { mode: "boolean" }).notNull().default(false), + proxyProtocol: integer("proxyProtocol", { mode: "boolean" }) + .notNull() + .default(false), proxyProtocolVersion: integer("proxyProtocolVersion").default(1) - }); export const targets = sqliteTable("targets", { diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index 834da7b3..223fcaa4 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -91,7 +91,6 @@ export async function getResourceAuthInfo( resourcePassword, eq(resourcePassword.resourceId, resources.resourceId) ) - .leftJoin( resourceHeaderAuth, eq( From 46d60bd0900ba1ed3a693294c001bfb4b288d050 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 11 Nov 2025 17:08:52 +0100 Subject: [PATCH 003/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20add=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/sqlite/schema/schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 6e5e49e7..e259835d 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -873,3 +873,4 @@ export type LicenseKey = InferSelectModel; export type SecurityKey = InferSelectModel; export type WebauthnChallenge = InferSelectModel; export type RequestAuditLog = InferSelectModel; +export type OrgAuthPage = InferSelectModel; From 08e43400e40c6406d62166fc4b58d991ed1ae3de Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 11 Nov 2025 21:14:10 +0100 Subject: [PATCH 004/153] =?UTF-8?q?=F0=9F=9A=A7=20frontend=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + server/db/pg/migrate.ts | 3 +- server/db/pg/schema/schema.ts | 33 ++++++----- server/db/sqlite/schema/schema.ts | 40 ++++++++----- .../settings/general/auth-pages/page.tsx | 15 +++++ src/app/[orgId]/settings/general/layout.tsx | 31 +++++----- src/components/AuthPagesCustomizationForm.tsx | 56 +++++++++++++++++++ src/components/HorizontalTabs.tsx | 14 +++-- 8 files changed, 145 insertions(+), 48 deletions(-) create mode 100644 src/app/[orgId]/settings/general/auth-pages/page.tsx create mode 100644 src/components/AuthPagesCustomizationForm.tsx diff --git a/messages/en-US.json b/messages/en-US.json index 178a9bb9..0a45b32e 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -150,6 +150,7 @@ "resourceAdd": "Add Resource", "resourceErrorDelte": "Error deleting resource", "authentication": "Authentication", + "authPages": "Auth Page", "protected": "Protected", "notProtected": "Not Protected", "resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.", diff --git a/server/db/pg/migrate.ts b/server/db/pg/migrate.ts index 70b2ef54..2d2abca3 100644 --- a/server/db/pg/migrate.ts +++ b/server/db/pg/migrate.ts @@ -10,7 +10,8 @@ const runMigrations = async () => { await migrate(db as any, { migrationsFolder: migrationsFolder }); - console.log("Migrations completed successfully."); + console.log("Migrations completed successfully. ✅"); + process.exit(0); } catch (error) { console.error("Error running migrations:", error); process.exit(1); diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 15c1942b..90cd1984 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -7,7 +7,8 @@ import { bigint, real, text, - index + index, + uniqueIndex } from "drizzle-orm/pg-core"; import { InferSelectModel } from "drizzle-orm"; import { randomUUID } from "crypto"; @@ -64,19 +65,23 @@ export const orgDomains = pgTable("orgDomains", { .references(() => domains.domainId, { onDelete: "cascade" }) }); -export const orgAuthPages = pgTable("orgAuthPages", { - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - orgAuthPageId: serial("orgAuthPageId").primaryKey(), - logoUrl: text("logoUrl"), - logoWidth: integer("logoWidth"), - logoHeight: integer("logoHeight"), - title: text("title"), - subtitle: text("subtitle"), - resourceTitle: text("resourceTitle"), - resourceSubtitle: text("resourceSubtitle") -}); +export const orgAuthPages = pgTable( + "orgAuthPages", + { + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + orgAuthPageId: serial("orgAuthPageId").primaryKey(), + logoUrl: text("logoUrl").notNull(), + logoWidth: integer("logoWidth").notNull(), + logoHeight: integer("logoHeight").notNull(), + title: text("title").notNull(), + subtitle: text("subtitle"), + resourceTitle: text("resourceTitle").notNull(), + resourceSubtitle: text("resourceSubtitle") + }, + (t) => [uniqueIndex("uniqueAuthPagePerOrgIdx").on(t.orgId)] +); export const sites = pgTable("sites", { siteId: serial("siteId").primaryKey(), diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index e259835d..5c293ffd 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -1,6 +1,12 @@ import { randomUUID } from "crypto"; import { InferSelectModel } from "drizzle-orm"; -import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; +import { + sqliteTable, + text, + integer, + index, + uniqueIndex +} from "drizzle-orm/sqlite-core"; import { boolean } from "yargs"; export const domains = sqliteTable("domains", { @@ -66,19 +72,25 @@ export const orgDomains = sqliteTable("orgDomains", { .references(() => domains.domainId, { onDelete: "cascade" }) }); -export const orgAuthPages = sqliteTable("orgAuthPages", { - orgId: text("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - orgAuthPageId: integer("orgAuthPageId").primaryKey({ autoIncrement: true }), - logoUrl: text("logoUrl"), - logoWidth: integer("logoWidth"), - logoHeight: integer("logoHeight"), - title: text("title"), - subtitle: text("subtitle"), - resourceTitle: text("resourceTitle"), - resourceSubtitle: text("resourceSubtitle") -}); +export const orgAuthPages = sqliteTable( + "orgAuthPages", + { + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + orgAuthPageId: integer("orgAuthPageId").primaryKey({ + autoIncrement: true + }), + logoUrl: text("logoUrl").notNull(), + logoWidth: integer("logoWidth").notNull(), + logoHeight: integer("logoHeight").notNull(), + title: text("title").notNull(), + subtitle: text("subtitle"), + resourceTitle: text("resourceTitle").notNull(), + resourceSubtitle: text("resourceSubtitle") + }, + (t) => [uniqueIndex("uniqueAuthPagePerOrgIdx").on(t.orgId)] +); export const sites = sqliteTable("sites", { siteId: integer("siteId").primaryKey({ autoIncrement: true }), diff --git a/src/app/[orgId]/settings/general/auth-pages/page.tsx b/src/app/[orgId]/settings/general/auth-pages/page.tsx new file mode 100644 index 00000000..2ab90bdb --- /dev/null +++ b/src/app/[orgId]/settings/general/auth-pages/page.tsx @@ -0,0 +1,15 @@ +import AuthPageCustomizationForm from "@app/components/AuthPagesCustomizationForm"; +import { SettingsContainer } from "@app/components/Settings"; + +export interface AuthPageProps { + params: Promise<{ orgId: string }>; +} + +export default async function AuthPage(props: AuthPageProps) { + const orgId = (await props.params).orgId; + return ( + + + + ); +} diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 82b2c999..fc501d82 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -1,7 +1,7 @@ import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import { HorizontalTabs, type TabItem } from "@app/components/HorizontalTabs"; import { verifySession } from "@app/lib/auth/verifySession"; import OrgProvider from "@app/providers/OrgProvider"; import OrgUserProvider from "@app/providers/OrgUserProvider"; @@ -10,7 +10,7 @@ import { GetOrgUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { cache } from "react"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; type GeneralSettingsProps = { children: React.ReactNode; @@ -19,7 +19,7 @@ type GeneralSettingsProps = { export default async function GeneralSettingsPage({ children, - params, + params }: GeneralSettingsProps) { const { orgId } = await params; @@ -35,8 +35,8 @@ export default async function GeneralSettingsPage({ const getOrgUser = cache(async () => internal.get>( `/org/${orgId}/user/${user.userId}`, - await authCookieHeader(), - ), + await authCookieHeader() + ) ); const res = await getOrgUser(); orgUser = res.data.data; @@ -49,8 +49,8 @@ export default async function GeneralSettingsPage({ const getOrg = cache(async () => internal.get>( `/org/${orgId}`, - await authCookieHeader(), - ), + await authCookieHeader() + ) ); const res = await getOrg(); org = res.data.data; @@ -60,11 +60,16 @@ export default async function GeneralSettingsPage({ const t = await getTranslations(); - const navItems = [ + const navItems: TabItem[] = [ { - title: t('general'), + title: t("general"), href: `/{orgId}/settings/general`, + exact: true }, + { + title: t("authPages"), + href: `/{orgId}/settings/general/auth-pages` + } ]; return ( @@ -72,13 +77,11 @@ export default async function GeneralSettingsPage({ - - {children} - + {children} diff --git a/src/components/AuthPagesCustomizationForm.tsx b/src/components/AuthPagesCustomizationForm.tsx new file mode 100644 index 00000000..fc695eeb --- /dev/null +++ b/src/components/AuthPagesCustomizationForm.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import * as React 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"; + +export type AuthPageCustomizationProps = { + orgId: string; +}; + +const AuthPageFormSchema = z.object({ + logoUrl: z.string().url(), + logoWidth: z.number().min(1), + logoHeight: z.number().min(1), + title: z.string(), + subtitle: z.string().optional(), + resourceTitle: z.string(), + resourceSubtitle: z.string().optional() +}); + +export default function AuthPageCustomizationForm({ + orgId +}: AuthPageCustomizationProps) { + const [, formAction, isSubmitting] = React.useActionState(onSubmit, null); + + const form = useForm({ + resolver: zodResolver(AuthPageFormSchema), + defaultValues: { + title: `Log in to {{orgName}}`, + resourceTitle: `Authenticate to access {{resourceName}}` + } + }); + + async function onSubmit() { + const isValid = await form.trigger(); + + if (!isValid) return; + // ... + } + + return ( +
+ +
+ ); +} diff --git a/src/components/HorizontalTabs.tsx b/src/components/HorizontalTabs.tsx index 078cc660..7cbac226 100644 --- a/src/components/HorizontalTabs.tsx +++ b/src/components/HorizontalTabs.tsx @@ -8,16 +8,17 @@ import { Badge } from "@app/components/ui/badge"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useTranslations } from "next-intl"; -export type HorizontalTabs = Array<{ +export type TabItem = { title: string; href: string; icon?: React.ReactNode; showProfessional?: boolean; -}>; + exact?: boolean; +}; interface HorizontalTabsProps { children: React.ReactNode; - items: HorizontalTabs; + items: TabItem[]; disabled?: boolean; } @@ -49,8 +50,11 @@ export function HorizontalTabs({ {items.map((item) => { const hydratedHref = hydrateHref(item.href); const isActive = - pathname.startsWith(hydratedHref) && + (item.exact + ? pathname === hydratedHref + : pathname.startsWith(hydratedHref)) && !pathname.includes("create"); + const isProfessional = item.showProfessional && !isUnlocked(); const isDisabled = @@ -88,7 +92,7 @@ export function HorizontalTabs({ variant="outlinePrimary" className="ml-2" > - {t('licenseBadge')} + {t("licenseBadge")} )} From f58cf68f7c20fb332d237b0c8b01b5cea5f3a94d Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 11 Nov 2025 23:35:20 +0100 Subject: [PATCH 005/153] =?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 | 1 - server/auth/actions.ts | 4 +- server/lib/createResponseBodySchema.ts | 13 ++ .../routers/authPage/getOrgAuthPage.ts | 107 +++++++++++++ server/private/routers/authPage/index.ts | 15 ++ .../routers/authPage/updateOrgAuthPage.ts | 141 ++++++++++++++++++ server/private/routers/external.ts | 21 +++ server/routers/external.ts | 88 +++++------ .../settings/(private)/billing/layout.tsx | 38 ++--- .../settings/general/auth-pages/page.tsx | 21 +++ src/app/[orgId]/settings/general/layout.tsx | 47 +++--- src/lib/api/getCachedOrgUser.ts | 13 ++ src/lib/api/getCachedSubscription.ts | 8 + src/lib/api/index.ts | 1 - src/lib/auth/verifySession.ts | 9 +- 15 files changed, 427 insertions(+), 100 deletions(-) create mode 100644 server/lib/createResponseBodySchema.ts create mode 100644 server/private/routers/authPage/getOrgAuthPage.ts create mode 100644 server/private/routers/authPage/index.ts create mode 100644 server/private/routers/authPage/updateOrgAuthPage.ts create mode 100644 src/lib/api/getCachedOrgUser.ts create mode 100644 src/lib/api/getCachedSubscription.ts diff --git a/messages/en-US.json b/messages/en-US.json index 0a45b32e..178a9bb9 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -150,7 +150,6 @@ "resourceAdd": "Add Resource", "resourceErrorDelte": "Error deleting resource", "authentication": "Authentication", - "authPages": "Auth Page", "protected": "Protected", "notProtected": "Not Protected", "resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index d08457e5..411ada44 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -123,7 +123,9 @@ export enum ActionsEnum { getBlueprint = "getBlueprint", applyBlueprint = "applyBlueprint", viewLogs = "viewLogs", - exportLogs = "exportLogs" + exportLogs = "exportLogs", + updateOrgAuthPage = "updateOrgAuthPage", + getOrgAuthPage = "getOrgAuthPage" } export async function checkUserActionPermission( diff --git a/server/lib/createResponseBodySchema.ts b/server/lib/createResponseBodySchema.ts new file mode 100644 index 00000000..478cc0c3 --- /dev/null +++ b/server/lib/createResponseBodySchema.ts @@ -0,0 +1,13 @@ +import z, { type ZodSchema } from "zod"; + +export function createResponseBodySchema(dataSchema: T) { + return z.object({ + data: dataSchema.nullable(), + success: z.boolean(), + error: z.boolean(), + message: z.string(), + status: z.number() + }); +} + +export default createResponseBodySchema; diff --git a/server/private/routers/authPage/getOrgAuthPage.ts b/server/private/routers/authPage/getOrgAuthPage.ts new file mode 100644 index 00000000..03c03ac9 --- /dev/null +++ b/server/private/routers/authPage/getOrgAuthPage.ts @@ -0,0 +1,107 @@ +import { eq } from "drizzle-orm"; +/* + * 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, orgAuthPages } from "@server/db"; +import { orgs } 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 { OpenAPITags, registry } from "@server/openApi"; +import createResponseBodySchema from "@server/lib/createResponseBodySchema"; + +const getOrgAuthPageParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +const reponseSchema = createResponseBodySchema( + z + .object({ + logoUrl: z.string().url(), + logoWidth: z.number().min(1), + logoHeight: z.number().min(1), + title: z.string(), + subtitle: z.string().optional(), + resourceTitle: z.string(), + resourceSubtitle: z.string().optional() + }) + .strict() +); + +export type GetOrgAuthPageResponse = z.infer; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/auth-page", + description: "Get an organization auth page", + tags: [OpenAPITags.Org], + request: { + params: getOrgAuthPageParamsSchema + }, + responses: { + 200: { + description: "", + content: { + "application/json": { + schema: reponseSchema + } + } + } + } +}); + +export async function getOrgAuthPage( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = getOrgAuthPageParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + const [orgAuthPage] = await db + .select() + .from(orgAuthPages) + .leftJoin(orgs, eq(orgs.orgId, orgAuthPages.orgId)) + .where(eq(orgs.orgId, orgId)) + .limit(1); + + return response(res, { + data: orgAuthPage?.orgAuthPages ?? null, + success: true, + error: false, + message: "Organization auth page 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/authPage/index.ts b/server/private/routers/authPage/index.ts new file mode 100644 index 00000000..2a01a879 --- /dev/null +++ b/server/private/routers/authPage/index.ts @@ -0,0 +1,15 @@ +/* + * 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. + */ + +export * from "./updateOrgAuthPage"; +export * from "./getOrgAuthPage"; diff --git a/server/private/routers/authPage/updateOrgAuthPage.ts b/server/private/routers/authPage/updateOrgAuthPage.ts new file mode 100644 index 00000000..e6fa04f4 --- /dev/null +++ b/server/private/routers/authPage/updateOrgAuthPage.ts @@ -0,0 +1,141 @@ +/* + * 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, orgAuthPages } 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 { OpenAPITags, registry } from "@server/openApi"; +import createResponseBodySchema from "@server/lib/createResponseBodySchema"; + +const updateOrgAuthPageParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +const updateOrgAuthPageBodySchema = z + .object({ + logoUrl: z.string().url(), + logoWidth: z.number().min(1), + logoHeight: z.number().min(1), + title: z.string(), + subtitle: z.string().optional(), + resourceTitle: z.string(), + resourceSubtitle: z.string().optional() + }) + .strict(); + +const reponseSchema = createResponseBodySchema(updateOrgAuthPageBodySchema); + +export type UpdateOrgAuthPageResponse = z.infer; + +registry.registerPath({ + method: "put", + path: "/org/{orgId}/auth-page", + description: "Update an organization auth page", + tags: [OpenAPITags.Org], + request: { + params: updateOrgAuthPageParamsSchema, + body: { + content: { + "application/json": { + schema: updateOrgAuthPageBodySchema + } + } + } + }, + responses: { + 200: { + description: "", + content: { + "application/json": { + schema: reponseSchema + } + } + } + } +}); + +export async function updateOrgAuthPage( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = updateOrgAuthPageParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = updateOrgAuthPageBodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + const body = parsedBody.data; + + const updatedOrgAuthPages = await db + .insert(orgAuthPages) + .values({ + ...body, + orgId + }) + .onConflictDoUpdate({ + target: orgAuthPages.orgId, + set: { + ...body + } + }) + .returning(); + + if (updatedOrgAuthPages.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Organization with ID ${orgId} not found` + ) + ); + } + + return response(res, { + data: updatedOrgAuthPages[0], + success: true, + error: false, + message: "Organization auth page updated 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/external.ts b/server/private/routers/external.ts index 00ad117f..6d1cf6df 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -17,6 +17,7 @@ import * as billing from "#private/routers/billing"; import * as remoteExitNode from "#private/routers/remoteExitNode"; import * as loginPage from "#private/routers/loginPage"; import * as orgIdp from "#private/routers/orgIdp"; +import * as authPage from "#private/routers/authPage"; import * as domain from "#private/routers/domain"; import * as auth from "#private/routers/auth"; import * as license from "#private/routers/license"; @@ -403,3 +404,23 @@ authenticated.get( logActionAudit(ActionsEnum.exportLogs), logs.exportAccessAuditLogs ); + +authenticated.put( + "/org/:orgId/auth-page", + verifyValidLicense, + verifyValidSubscription, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.updateOrgAuthPage), + logActionAudit(ActionsEnum.updateOrgAuthPage), + authPage.updateOrgAuthPage +); + +authenticated.get( + "/org/:orgId/auth-page", + verifyValidLicense, + verifyValidSubscription, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.getOrgAuthPage), + logActionAudit(ActionsEnum.getOrgAuthPage), + authPage.getOrgAuthPage +); diff --git a/server/routers/external.ts b/server/routers/external.ts index 5c235902..0099aeea 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -80,7 +80,7 @@ authenticated.post( verifyOrgAccess, verifyUserHasAction(ActionsEnum.updateOrg), logActionAudit(ActionsEnum.updateOrg), - org.updateOrg, + org.updateOrg ); if (build !== "saas") { @@ -90,7 +90,7 @@ if (build !== "saas") { verifyUserIsOrgOwner, verifyUserHasAction(ActionsEnum.deleteOrg), logActionAudit(ActionsEnum.deleteOrg), - org.deleteOrg, + org.deleteOrg ); } @@ -157,7 +157,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createClient), logActionAudit(ActionsEnum.createClient), - client.createClient, + client.createClient ); authenticated.delete( @@ -166,7 +166,7 @@ authenticated.delete( verifyClientAccess, verifyUserHasAction(ActionsEnum.deleteClient), logActionAudit(ActionsEnum.deleteClient), - client.deleteClient, + client.deleteClient ); authenticated.post( @@ -175,7 +175,7 @@ authenticated.post( verifyClientAccess, // this will check if the user has access to the client verifyUserHasAction(ActionsEnum.updateClient), // this will check if the user has permission to update the client logActionAudit(ActionsEnum.updateClient), - client.updateClient, + client.updateClient ); // authenticated.get( @@ -189,14 +189,14 @@ authenticated.post( verifySiteAccess, verifyUserHasAction(ActionsEnum.updateSite), logActionAudit(ActionsEnum.updateSite), - site.updateSite, + site.updateSite ); authenticated.delete( "/site/:siteId", verifySiteAccess, verifyUserHasAction(ActionsEnum.deleteSite), logActionAudit(ActionsEnum.deleteSite), - site.deleteSite, + site.deleteSite ); // TODO: BREAK OUT THESE ACTIONS SO THEY ARE NOT ALL "getSite" @@ -216,13 +216,13 @@ authenticated.post( "/site/:siteId/docker/check", verifySiteAccess, verifyUserHasAction(ActionsEnum.getSite), - site.checkDockerSocket, + site.checkDockerSocket ); authenticated.post( "/site/:siteId/docker/trigger", verifySiteAccess, verifyUserHasAction(ActionsEnum.getSite), - site.triggerFetchContainers, + site.triggerFetchContainers ); authenticated.get( "/site/:siteId/docker/containers", @@ -238,7 +238,7 @@ authenticated.put( verifySiteAccess, verifyUserHasAction(ActionsEnum.createSiteResource), logActionAudit(ActionsEnum.createSiteResource), - siteResource.createSiteResource, + siteResource.createSiteResource ); authenticated.get( @@ -272,7 +272,7 @@ authenticated.post( verifySiteResourceAccess, verifyUserHasAction(ActionsEnum.updateSiteResource), logActionAudit(ActionsEnum.updateSiteResource), - siteResource.updateSiteResource, + siteResource.updateSiteResource ); authenticated.delete( @@ -282,7 +282,7 @@ authenticated.delete( verifySiteResourceAccess, verifyUserHasAction(ActionsEnum.deleteSiteResource), logActionAudit(ActionsEnum.deleteSiteResource), - siteResource.deleteSiteResource, + siteResource.deleteSiteResource ); authenticated.put( @@ -290,7 +290,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createResource), logActionAudit(ActionsEnum.createResource), - resource.createResource, + resource.createResource ); authenticated.get( @@ -352,7 +352,7 @@ authenticated.delete( verifyOrgAccess, verifyUserHasAction(ActionsEnum.removeInvitation), logActionAudit(ActionsEnum.removeInvitation), - user.removeInvitation, + user.removeInvitation ); authenticated.post( @@ -360,7 +360,7 @@ authenticated.post( verifyOrgAccess, verifyUserHasAction(ActionsEnum.inviteUser), logActionAudit(ActionsEnum.inviteUser), - user.inviteUser, + user.inviteUser ); // maybe make this /invite/create instead unauthenticated.post("/invite/accept", user.acceptInvite); // this is supposed to be unauthenticated @@ -396,14 +396,14 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.updateResource), logActionAudit(ActionsEnum.updateResource), - resource.updateResource, + resource.updateResource ); authenticated.delete( "/resource/:resourceId", verifyResourceAccess, verifyUserHasAction(ActionsEnum.deleteResource), logActionAudit(ActionsEnum.deleteResource), - resource.deleteResource, + resource.deleteResource ); authenticated.put( @@ -411,7 +411,7 @@ authenticated.put( verifyResourceAccess, verifyUserHasAction(ActionsEnum.createTarget), logActionAudit(ActionsEnum.createTarget), - target.createTarget, + target.createTarget ); authenticated.get( "/resource/:resourceId/targets", @@ -425,7 +425,7 @@ authenticated.put( verifyResourceAccess, verifyUserHasAction(ActionsEnum.createResourceRule), logActionAudit(ActionsEnum.createResourceRule), - resource.createResourceRule, + resource.createResourceRule ); authenticated.get( "/resource/:resourceId/rules", @@ -438,14 +438,14 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.updateResourceRule), logActionAudit(ActionsEnum.updateResourceRule), - resource.updateResourceRule, + resource.updateResourceRule ); authenticated.delete( "/resource/:resourceId/rule/:ruleId", verifyResourceAccess, verifyUserHasAction(ActionsEnum.deleteResourceRule), logActionAudit(ActionsEnum.deleteResourceRule), - resource.deleteResourceRule, + resource.deleteResourceRule ); authenticated.get( @@ -459,14 +459,14 @@ authenticated.post( verifyTargetAccess, verifyUserHasAction(ActionsEnum.updateTarget), logActionAudit(ActionsEnum.updateTarget), - target.updateTarget, + target.updateTarget ); authenticated.delete( "/target/:targetId", verifyTargetAccess, verifyUserHasAction(ActionsEnum.deleteTarget), logActionAudit(ActionsEnum.deleteTarget), - target.deleteTarget, + target.deleteTarget ); authenticated.put( @@ -474,7 +474,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createRole), logActionAudit(ActionsEnum.createRole), - role.createRole, + role.createRole ); authenticated.get( "/org/:orgId/roles", @@ -500,7 +500,7 @@ authenticated.delete( verifyRoleAccess, verifyUserHasAction(ActionsEnum.deleteRole), logActionAudit(ActionsEnum.deleteRole), - role.deleteRole, + role.deleteRole ); authenticated.post( "/role/:roleId/add/:userId", @@ -508,7 +508,7 @@ authenticated.post( verifyUserAccess, verifyUserHasAction(ActionsEnum.addUserRole), logActionAudit(ActionsEnum.addUserRole), - user.addUserRole, + user.addUserRole ); authenticated.post( @@ -517,7 +517,7 @@ authenticated.post( verifyRoleAccess, verifyUserHasAction(ActionsEnum.setResourceRoles), logActionAudit(ActionsEnum.setResourceRoles), - resource.setResourceRoles, + resource.setResourceRoles ); authenticated.post( @@ -526,7 +526,7 @@ authenticated.post( verifySetResourceUsers, verifyUserHasAction(ActionsEnum.setResourceUsers), logActionAudit(ActionsEnum.setResourceUsers), - resource.setResourceUsers, + resource.setResourceUsers ); authenticated.post( @@ -534,7 +534,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.setResourcePassword), logActionAudit(ActionsEnum.setResourcePassword), - resource.setResourcePassword, + resource.setResourcePassword ); authenticated.post( @@ -542,7 +542,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.setResourcePincode), logActionAudit(ActionsEnum.setResourcePincode), - resource.setResourcePincode, + resource.setResourcePincode ); authenticated.post( @@ -550,7 +550,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.setResourceHeaderAuth), logActionAudit(ActionsEnum.setResourceHeaderAuth), - resource.setResourceHeaderAuth, + resource.setResourceHeaderAuth ); authenticated.post( @@ -558,7 +558,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.setResourceWhitelist), logActionAudit(ActionsEnum.setResourceWhitelist), - resource.setResourceWhitelist, + resource.setResourceWhitelist ); authenticated.get( @@ -573,7 +573,7 @@ authenticated.post( verifyResourceAccess, verifyUserHasAction(ActionsEnum.generateAccessToken), logActionAudit(ActionsEnum.generateAccessToken), - accessToken.generateAccessToken, + accessToken.generateAccessToken ); authenticated.delete( @@ -581,7 +581,7 @@ authenticated.delete( verifyAccessTokenAccess, verifyUserHasAction(ActionsEnum.deleteAcessToken), logActionAudit(ActionsEnum.deleteAcessToken), - accessToken.deleteAccessToken, + accessToken.deleteAccessToken ); authenticated.get( @@ -655,7 +655,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createOrgUser), logActionAudit(ActionsEnum.createOrgUser), - user.createOrgUser, + user.createOrgUser ); authenticated.post( @@ -664,7 +664,7 @@ authenticated.post( verifyUserAccess, verifyUserHasAction(ActionsEnum.updateOrgUser), logActionAudit(ActionsEnum.updateOrgUser), - user.updateOrgUser, + user.updateOrgUser ); authenticated.get("/org/:orgId/user/:userId", verifyOrgAccess, user.getOrgUser); @@ -688,7 +688,7 @@ authenticated.delete( verifyUserAccess, verifyUserHasAction(ActionsEnum.removeUser), logActionAudit(ActionsEnum.removeUser), - user.removeUserOrg, + user.removeUserOrg ); // authenticated.put( @@ -819,7 +819,7 @@ authenticated.post( verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.setApiKeyActions), logActionAudit(ActionsEnum.setApiKeyActions), - apiKeys.setApiKeyActions, + apiKeys.setApiKeyActions ); authenticated.get( @@ -835,7 +835,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createApiKey), logActionAudit(ActionsEnum.createApiKey), - apiKeys.createOrgApiKey, + apiKeys.createOrgApiKey ); authenticated.delete( @@ -844,7 +844,7 @@ authenticated.delete( verifyApiKeyAccess, verifyUserHasAction(ActionsEnum.deleteApiKey), logActionAudit(ActionsEnum.deleteApiKey), - apiKeys.deleteOrgApiKey, + apiKeys.deleteOrgApiKey ); authenticated.get( @@ -860,7 +860,7 @@ authenticated.put( verifyOrgAccess, verifyUserHasAction(ActionsEnum.createOrgDomain), logActionAudit(ActionsEnum.createOrgDomain), - domain.createOrgDomain, + domain.createOrgDomain ); authenticated.post( @@ -869,7 +869,7 @@ authenticated.post( verifyDomainAccess, verifyUserHasAction(ActionsEnum.restartOrgDomain), logActionAudit(ActionsEnum.restartOrgDomain), - domain.restartOrgDomain, + domain.restartOrgDomain ); authenticated.delete( @@ -878,7 +878,7 @@ authenticated.delete( verifyDomainAccess, verifyUserHasAction(ActionsEnum.deleteOrgDomain), logActionAudit(ActionsEnum.deleteOrgDomain), - domain.deleteAccountDomain, + domain.deleteAccountDomain ); authenticated.get( @@ -1237,4 +1237,4 @@ authRouter.delete( store: createStore() }), auth.deleteSecurityKey -); \ No newline at end of file +); diff --git a/src/app/[orgId]/settings/(private)/billing/layout.tsx b/src/app/[orgId]/settings/(private)/billing/layout.tsx index 538c7fde..c4048bcc 100644 --- a/src/app/[orgId]/settings/(private)/billing/layout.tsx +++ b/src/app/[orgId]/settings/(private)/billing/layout.tsx @@ -1,16 +1,11 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; import { verifySession } from "@app/lib/auth/verifySession"; import OrgProvider from "@app/providers/OrgProvider"; import OrgUserProvider from "@app/providers/OrgUserProvider"; -import { GetOrgResponse } from "@server/routers/org"; -import { GetOrgUserResponse } from "@server/routers/user"; -import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; -import { cache } from "react"; -import { getTranslations } from 'next-intl/server'; +import { getTranslations } from "next-intl/server"; +import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; +import { getCachedOrg } from "@app/lib/api/getCachedOrg"; type BillingSettingsProps = { children: React.ReactNode; @@ -19,12 +14,11 @@ type BillingSettingsProps = { export default async function BillingSettingsPage({ children, - params, + params }: BillingSettingsProps) { const { orgId } = await params; - const getUser = cache(verifySession); - const user = await getUser(); + const user = await verifySession(); if (!user) { redirect(`/`); @@ -32,13 +26,7 @@ export default async function BillingSettingsPage({ let orgUser = null; try { - const getOrgUser = cache(async () => - internal.get>( - `/org/${orgId}/user/${user.userId}`, - await authCookieHeader(), - ), - ); - const res = await getOrgUser(); + const res = await getCachedOrgUser(orgId, user.userId); orgUser = res.data.data; } catch { redirect(`/${orgId}`); @@ -46,13 +34,7 @@ export default async function BillingSettingsPage({ let org = null; try { - const getOrg = cache(async () => - internal.get>( - `/org/${orgId}`, - await authCookieHeader(), - ), - ); - const res = await getOrg(); + const res = await getCachedOrg(orgId); org = res.data.data; } catch { redirect(`/${orgId}`); @@ -65,11 +47,11 @@ export default async function BillingSettingsPage({ - {children} + {children} diff --git a/src/app/[orgId]/settings/general/auth-pages/page.tsx b/src/app/[orgId]/settings/general/auth-pages/page.tsx index 2ab90bdb..79065513 100644 --- a/src/app/[orgId]/settings/general/auth-pages/page.tsx +++ b/src/app/[orgId]/settings/general/auth-pages/page.tsx @@ -1,5 +1,11 @@ import AuthPageCustomizationForm from "@app/components/AuthPagesCustomizationForm"; import { SettingsContainer } from "@app/components/Settings"; +import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; +import { pullEnv } from "@app/lib/pullEnv"; +import { build } from "@server/build"; +import { TierId } from "@server/lib/billing/tiers"; +import type { GetOrgTierResponse } from "@server/routers/billing/types"; +import { redirect } from "next/navigation"; export interface AuthPageProps { params: Promise<{ orgId: string }>; @@ -7,6 +13,21 @@ export interface AuthPageProps { export default async function AuthPage(props: AuthPageProps) { const orgId = (await props.params).orgId; + const env = pullEnv(); + let subscriptionStatus: GetOrgTierResponse | null = null; + try { + const subRes = await getCachedSubscription(orgId); + subscriptionStatus = subRes.data.data; + } catch {} + const subscribed = + build === "enterprise" + ? true + : subscriptionStatus?.tier === TierId.STANDARD; + + if (!subscribed) { + redirect(env.app.dashboardUrl); + } + return ( diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index fc501d82..5ace97ca 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -9,8 +9,14 @@ import { GetOrgResponse } from "@server/routers/org"; import { GetOrgUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; -import { cache } from "react"; + import { getTranslations } from "next-intl/server"; +import { getCachedOrg } from "@app/lib/api/getCachedOrg"; +import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; +import { GetOrgTierResponse } from "@server/routers/billing/types"; +import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; +import { build } from "@server/build"; +import { TierId } from "@server/lib/billing/tiers"; type GeneralSettingsProps = { children: React.ReactNode; @@ -23,8 +29,7 @@ export default async function GeneralSettingsPage({ }: GeneralSettingsProps) { const { orgId } = await params; - const getUser = cache(verifySession); - const user = await getUser(); + const user = await verifySession(); if (!user) { redirect(`/`); @@ -32,13 +37,7 @@ export default async function GeneralSettingsPage({ let orgUser = null; try { - const getOrgUser = cache(async () => - internal.get>( - `/org/${orgId}/user/${user.userId}`, - await authCookieHeader() - ) - ); - const res = await getOrgUser(); + const res = await getCachedOrgUser(orgId, user.userId); orgUser = res.data.data; } catch { redirect(`/${orgId}`); @@ -46,18 +45,22 @@ export default async function GeneralSettingsPage({ let org = null; try { - const getOrg = cache(async () => - internal.get>( - `/org/${orgId}`, - await authCookieHeader() - ) - ); - const res = await getOrg(); + const res = await getCachedOrg(orgId); org = res.data.data; } catch { redirect(`/${orgId}`); } + let subscriptionStatus: GetOrgTierResponse | null = null; + try { + const subRes = await getCachedSubscription(orgId); + subscriptionStatus = subRes.data.data; + } catch {} + const subscribed = + build === "enterprise" + ? true + : subscriptionStatus?.tier === TierId.STANDARD; + const t = await getTranslations(); const navItems: TabItem[] = [ @@ -65,12 +68,14 @@ export default async function GeneralSettingsPage({ title: t("general"), href: `/{orgId}/settings/general`, exact: true - }, - { - title: t("authPages"), - href: `/{orgId}/settings/general/auth-pages` } ]; + if (subscribed) { + navItems.push({ + title: t("authPage"), + href: `/{orgId}/settings/general/auth-pages` + }); + } return ( <> diff --git a/src/lib/api/getCachedOrgUser.ts b/src/lib/api/getCachedOrgUser.ts new file mode 100644 index 00000000..89632d97 --- /dev/null +++ b/src/lib/api/getCachedOrgUser.ts @@ -0,0 +1,13 @@ +import type { GetOrgResponse } from "@server/routers/org"; +import type { AxiosResponse } from "axios"; +import { cache } from "react"; +import { authCookieHeader } from "./cookies"; +import { internal } from "."; +import type { GetOrgUserResponse } from "@server/routers/user"; + +export const getCachedOrgUser = cache(async (orgId: string, userId: string) => + internal.get>( + `/org/${orgId}/user/${userId}`, + await authCookieHeader() + ) +); diff --git a/src/lib/api/getCachedSubscription.ts b/src/lib/api/getCachedSubscription.ts new file mode 100644 index 00000000..dbffee5d --- /dev/null +++ b/src/lib/api/getCachedSubscription.ts @@ -0,0 +1,8 @@ +import type { AxiosResponse } from "axios"; +import { cache } from "react"; +import { priv } from "."; +import type { GetOrgTierResponse } from "@server/routers/billing/types"; + +export const getCachedSubscription = cache(async (orgId: string) => + priv.get>(`/org/${orgId}/billing/tier`) +); diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 5cef9f0e..6c4613e9 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -60,4 +60,3 @@ export const priv = axios.create({ }); export * from "./formatAxiosError"; - diff --git a/src/lib/auth/verifySession.ts b/src/lib/auth/verifySession.ts index e51c5096..d679e7b5 100644 --- a/src/lib/auth/verifySession.ts +++ b/src/lib/auth/verifySession.ts @@ -3,9 +3,10 @@ import { authCookieHeader } from "@app/lib/api/cookies"; import { GetUserResponse } from "@server/routers/user"; import { AxiosResponse } from "axios"; import { pullEnv } from "../pullEnv"; +import { cache } from "react"; -export async function verifySession({ - skipCheckVerifyEmail, +export const verifySession = cache(async function ({ + skipCheckVerifyEmail }: { skipCheckVerifyEmail?: boolean; } = {}): Promise { @@ -14,7 +15,7 @@ export async function verifySession({ try { const res = await internal.get>( "/user", - await authCookieHeader(), + await authCookieHeader() ); const user = res.data.data; @@ -35,4 +36,4 @@ export async function verifySession({ } catch (e) { return null; } -} +}); From cfde4e7443f1a5dab20a572df6967ba49d40e8c9 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 12 Nov 2025 03:43:19 +0100 Subject: [PATCH 006/153] =?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 | 14 + server/auth/actions.ts | 4 +- server/db/pg/schema/privateSchema.ts | 109 ++- server/db/pg/schema/schema.ts | 19 - server/db/sqlite/schema/privateSchema.ts | 127 ++- server/db/sqlite/schema/schema.ts | 21 - server/lib/createResponseBodySchema.ts | 13 - .../routers/authPage/getOrgAuthPage.ts | 107 --- server/private/routers/authPage/index.ts | 15 - .../routers/authPage/updateOrgAuthPage.ts | 141 --- server/private/routers/external.ts | 48 +- .../loginPage/deleteLoginPageBranding.ts | 113 +++ .../routers/loginPage/getLoginPageBranding.ts | 103 +++ server/private/routers/loginPage/index.ts | 3 + .../loginPage/upsertLoginPageBranding.ts | 154 ++++ server/routers/loginPage/types.ts | 6 +- .../settings/general/auth-page/page.tsx | 64 ++ .../settings/general/auth-pages/page.tsx | 36 - src/app/[orgId]/settings/general/layout.tsx | 2 +- src/app/[orgId]/settings/general/page.tsx | 16 +- src/components/AuthPageBrandingForm.tsx | 319 +++++++ src/components/AuthPagesCustomizationForm.tsx | 56 -- src/components/private/AuthPageSettings.tsx | 868 +++++++++--------- 23 files changed, 1380 insertions(+), 978 deletions(-) delete mode 100644 server/lib/createResponseBodySchema.ts delete mode 100644 server/private/routers/authPage/getOrgAuthPage.ts delete mode 100644 server/private/routers/authPage/index.ts delete mode 100644 server/private/routers/authPage/updateOrgAuthPage.ts create mode 100644 server/private/routers/loginPage/deleteLoginPageBranding.ts create mode 100644 server/private/routers/loginPage/getLoginPageBranding.ts create mode 100644 server/private/routers/loginPage/upsertLoginPageBranding.ts create mode 100644 src/app/[orgId]/settings/general/auth-page/page.tsx delete mode 100644 src/app/[orgId]/settings/general/auth-pages/page.tsx create mode 100644 src/components/AuthPageBrandingForm.tsx delete mode 100644 src/components/AuthPagesCustomizationForm.tsx diff --git a/messages/en-US.json b/messages/en-US.json index 178a9bb9..b790ed20 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1735,6 +1735,20 @@ "authPage": "Auth Page", "authPageDescription": "Configure the auth page for your organization", "authPageDomain": "Auth Page Domain", + "authPageBranding": "Branding", + "authPageBrandingDescription": "Configure the branding for the auth page for your organization", + "brandingLogoURL": "Logo URL", + "brandingLogoWidth": "Width (px)", + "brandingLogoHeight": "Height (px)", + "brandingOrgTitle": "Title for Organization Auth Page", + "brandingOrgDescription": "{orgName} will be replaced with the organization's name", + "brandingOrgSubtitle": "Subtitle for Organization Auth Page", + "brandingResourceTitle": "Title for Resource Auth Page", + "brandingResourceSubtitle": "Subtitle for Resource Auth Page", + "brandingResourceDescription": "{resourceName} will be replaced with the organization's name", + "saveAuthPage": "Save Auth Page", + "saveAuthPageBranding": "Save Branding", + "removeAuthPageBranding": "Remove Branding", "noDomainSet": "No domain set", "changeDomain": "Change Domain", "selectDomain": "Select Domain", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 411ada44..d08457e5 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -123,9 +123,7 @@ export enum ActionsEnum { getBlueprint = "getBlueprint", applyBlueprint = "applyBlueprint", viewLogs = "viewLogs", - exportLogs = "exportLogs", - updateOrgAuthPage = "updateOrgAuthPage", - getOrgAuthPage = "getOrgAuthPage" + exportLogs = "exportLogs" } export async function checkUserActionPermission( diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index 17d262c6..f9911095 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -204,6 +204,28 @@ export const loginPageOrg = pgTable("loginPageOrg", { .references(() => orgs.orgId, { onDelete: "cascade" }) }); +export const loginPageBranding = pgTable("loginPageBranding", { + loginPageBrandingId: serial("loginPageBrandingId").primaryKey(), + logoUrl: text("logoUrl").notNull(), + logoWidth: integer("logoWidth").notNull(), + logoHeight: integer("logoHeight").notNull(), + title: text("title").notNull(), + subtitle: text("subtitle"), + resourceTitle: text("resourceTitle").notNull(), + resourceSubtitle: text("resourceSubtitle") +}); + +export const loginPageBrandingOrg = pgTable("loginPageBrandingOrg", { + loginPageBrandingId: integer("loginPageBrandingId") + .notNull() + .references(() => loginPageBranding.loginPageBrandingId, { + onDelete: "cascade" + }), + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }) +}); + export const sessionTransferToken = pgTable("sessionTransferToken", { token: varchar("token").primaryKey(), sessionId: varchar("sessionId") @@ -215,42 +237,56 @@ export const sessionTransferToken = pgTable("sessionTransferToken", { expiresAt: bigint("expiresAt", { mode: "number" }).notNull() }); -export const actionAuditLog = pgTable("actionAuditLog", { - id: serial("id").primaryKey(), - timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - actorType: varchar("actorType", { length: 50 }).notNull(), - actor: varchar("actor", { length: 255 }).notNull(), - actorId: varchar("actorId", { length: 255 }).notNull(), - action: varchar("action", { length: 100 }).notNull(), - metadata: text("metadata") -}, (table) => ([ - index("idx_actionAuditLog_timestamp").on(table.timestamp), - index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp) -])); +export const actionAuditLog = pgTable( + "actionAuditLog", + { + id: serial("id").primaryKey(), + timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + actorType: varchar("actorType", { length: 50 }).notNull(), + actor: varchar("actor", { length: 255 }).notNull(), + actorId: varchar("actorId", { length: 255 }).notNull(), + action: varchar("action", { length: 100 }).notNull(), + metadata: text("metadata") + }, + (table) => [ + index("idx_actionAuditLog_timestamp").on(table.timestamp), + index("idx_actionAuditLog_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); -export const accessAuditLog = pgTable("accessAuditLog", { - id: serial("id").primaryKey(), - timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - actorType: varchar("actorType", { length: 50 }), - actor: varchar("actor", { length: 255 }), - actorId: varchar("actorId", { length: 255 }), - resourceId: integer("resourceId"), - ip: varchar("ip", { length: 45 }), - type: varchar("type", { length: 100 }).notNull(), - action: boolean("action").notNull(), - location: text("location"), - userAgent: text("userAgent"), - metadata: text("metadata") -}, (table) => ([ - index("idx_identityAuditLog_timestamp").on(table.timestamp), - index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp) -])); +export const accessAuditLog = pgTable( + "accessAuditLog", + { + id: serial("id").primaryKey(), + timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + actorType: varchar("actorType", { length: 50 }), + actor: varchar("actor", { length: 255 }), + actorId: varchar("actorId", { length: 255 }), + resourceId: integer("resourceId"), + ip: varchar("ip", { length: 45 }), + type: varchar("type", { length: 100 }).notNull(), + action: boolean("action").notNull(), + location: text("location"), + userAgent: text("userAgent"), + metadata: text("metadata") + }, + (table) => [ + index("idx_identityAuditLog_timestamp").on(table.timestamp), + index("idx_identityAuditLog_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); export type Limit = InferSelectModel; export type Account = InferSelectModel; @@ -269,5 +305,6 @@ export type RemoteExitNodeSession = InferSelectModel< >; export type ExitNodeOrg = InferSelectModel; export type LoginPage = InferSelectModel; +export type LoginPageBranding = InferSelectModel; export type ActionAuditLog = InferSelectModel; -export type AccessAuditLog = InferSelectModel; \ No newline at end of file +export type AccessAuditLog = InferSelectModel; diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 90cd1984..0b750d4e 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -65,24 +65,6 @@ export const orgDomains = pgTable("orgDomains", { .references(() => domains.domainId, { onDelete: "cascade" }) }); -export const orgAuthPages = pgTable( - "orgAuthPages", - { - orgId: varchar("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - orgAuthPageId: serial("orgAuthPageId").primaryKey(), - logoUrl: text("logoUrl").notNull(), - logoWidth: integer("logoWidth").notNull(), - logoHeight: integer("logoHeight").notNull(), - title: text("title").notNull(), - subtitle: text("subtitle"), - resourceTitle: text("resourceTitle").notNull(), - resourceSubtitle: text("resourceSubtitle") - }, - (t) => [uniqueIndex("uniqueAuthPagePerOrgIdx").on(t.orgId)] -); - export const sites = pgTable("sites", { siteId: serial("siteId").primaryKey(), orgId: varchar("orgId") @@ -828,4 +810,3 @@ export type LicenseKey = InferSelectModel; export type SecurityKey = InferSelectModel; export type WebauthnChallenge = InferSelectModel; export type RequestAuditLog = InferSelectModel; -export type OrgAuthPage = InferSelectModel; diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index 65396770..e74964c2 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -29,7 +29,9 @@ export const certificates = sqliteTable("certificates", { }); export const dnsChallenge = sqliteTable("dnsChallenges", { - dnsChallengeId: integer("dnsChallengeId").primaryKey({ autoIncrement: true }), + dnsChallengeId: integer("dnsChallengeId").primaryKey({ + autoIncrement: true + }), domain: text("domain").notNull(), token: text("token").notNull(), keyAuthorization: text("keyAuthorization").notNull(), @@ -61,9 +63,7 @@ export const customers = sqliteTable("customers", { }); export const subscriptions = sqliteTable("subscriptions", { - subscriptionId: text("subscriptionId") - .primaryKey() - .notNull(), + subscriptionId: text("subscriptionId").primaryKey().notNull(), customerId: text("customerId") .notNull() .references(() => customers.customerId, { onDelete: "cascade" }), @@ -75,7 +75,9 @@ export const subscriptions = sqliteTable("subscriptions", { }); export const subscriptionItems = sqliteTable("subscriptionItems", { - subscriptionItemId: integer("subscriptionItemId").primaryKey({ autoIncrement: true }), + subscriptionItemId: integer("subscriptionItemId").primaryKey({ + autoIncrement: true + }), subscriptionId: text("subscriptionId") .notNull() .references(() => subscriptions.subscriptionId, { @@ -129,7 +131,9 @@ export const limits = sqliteTable("limits", { }); export const usageNotifications = sqliteTable("usageNotifications", { - notificationId: integer("notificationId").primaryKey({ autoIncrement: true }), + notificationId: integer("notificationId").primaryKey({ + autoIncrement: true + }), orgId: text("orgId") .notNull() .references(() => orgs.orgId, { onDelete: "cascade" }), @@ -199,6 +203,30 @@ export const loginPageOrg = sqliteTable("loginPageOrg", { .references(() => orgs.orgId, { onDelete: "cascade" }) }); +export const loginPageBranding = sqliteTable("loginPageBranding", { + loginPageBrandingId: integer("loginPageBrandingId").primaryKey({ + autoIncrement: true + }), + logoUrl: text("logoUrl").notNull(), + logoWidth: integer("logoWidth").notNull(), + logoHeight: integer("logoHeight").notNull(), + title: text("title").notNull(), + subtitle: text("subtitle"), + resourceTitle: text("resourceTitle").notNull(), + resourceSubtitle: text("resourceSubtitle") +}); + +export const loginPageBrandingOrg = sqliteTable("loginPageBrandingOrg", { + loginPageBrandingId: integer("loginPageBrandingId") + .notNull() + .references(() => loginPageBranding.loginPageBrandingId, { + onDelete: "cascade" + }), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }) +}); + export const sessionTransferToken = sqliteTable("sessionTransferToken", { token: text("token").primaryKey(), sessionId: text("sessionId") @@ -210,42 +238,56 @@ export const sessionTransferToken = sqliteTable("sessionTransferToken", { expiresAt: integer("expiresAt").notNull() }); -export const actionAuditLog = sqliteTable("actionAuditLog", { - id: integer("id").primaryKey({ autoIncrement: true }), - timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds - orgId: text("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - actorType: text("actorType").notNull(), - actor: text("actor").notNull(), - actorId: text("actorId").notNull(), - action: text("action").notNull(), - metadata: text("metadata") -}, (table) => ([ - index("idx_actionAuditLog_timestamp").on(table.timestamp), - index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp) -])); +export const actionAuditLog = sqliteTable( + "actionAuditLog", + { + id: integer("id").primaryKey({ autoIncrement: true }), + timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + actorType: text("actorType").notNull(), + actor: text("actor").notNull(), + actorId: text("actorId").notNull(), + action: text("action").notNull(), + metadata: text("metadata") + }, + (table) => [ + index("idx_actionAuditLog_timestamp").on(table.timestamp), + index("idx_actionAuditLog_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); -export const accessAuditLog = sqliteTable("accessAuditLog", { - id: integer("id").primaryKey({ autoIncrement: true }), - timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds - orgId: text("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - actorType: text("actorType"), - actor: text("actor"), - actorId: text("actorId"), - resourceId: integer("resourceId"), - ip: text("ip"), - location: text("location"), - type: text("type").notNull(), - action: integer("action", { mode: "boolean" }).notNull(), - userAgent: text("userAgent"), - metadata: text("metadata") -}, (table) => ([ - index("idx_identityAuditLog_timestamp").on(table.timestamp), - index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp) -])); +export const accessAuditLog = sqliteTable( + "accessAuditLog", + { + id: integer("id").primaryKey({ autoIncrement: true }), + timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + actorType: text("actorType"), + actor: text("actor"), + actorId: text("actorId"), + resourceId: integer("resourceId"), + ip: text("ip"), + location: text("location"), + type: text("type").notNull(), + action: integer("action", { mode: "boolean" }).notNull(), + userAgent: text("userAgent"), + metadata: text("metadata") + }, + (table) => [ + index("idx_identityAuditLog_timestamp").on(table.timestamp), + index("idx_identityAuditLog_org_timestamp").on( + table.orgId, + table.timestamp + ) + ] +); export type Limit = InferSelectModel; export type Account = InferSelectModel; @@ -264,5 +306,6 @@ export type RemoteExitNodeSession = InferSelectModel< >; export type ExitNodeOrg = InferSelectModel; export type LoginPage = InferSelectModel; +export type LoginPageBranding = InferSelectModel; export type ActionAuditLog = InferSelectModel; -export type AccessAuditLog = InferSelectModel; \ No newline at end of file +export type AccessAuditLog = InferSelectModel; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 5c293ffd..c96fefc5 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -72,26 +72,6 @@ export const orgDomains = sqliteTable("orgDomains", { .references(() => domains.domainId, { onDelete: "cascade" }) }); -export const orgAuthPages = sqliteTable( - "orgAuthPages", - { - orgId: text("orgId") - .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), - orgAuthPageId: integer("orgAuthPageId").primaryKey({ - autoIncrement: true - }), - logoUrl: text("logoUrl").notNull(), - logoWidth: integer("logoWidth").notNull(), - logoHeight: integer("logoHeight").notNull(), - title: text("title").notNull(), - subtitle: text("subtitle"), - resourceTitle: text("resourceTitle").notNull(), - resourceSubtitle: text("resourceSubtitle") - }, - (t) => [uniqueIndex("uniqueAuthPagePerOrgIdx").on(t.orgId)] -); - export const sites = sqliteTable("sites", { siteId: integer("siteId").primaryKey({ autoIncrement: true }), orgId: text("orgId") @@ -885,4 +865,3 @@ export type LicenseKey = InferSelectModel; export type SecurityKey = InferSelectModel; export type WebauthnChallenge = InferSelectModel; export type RequestAuditLog = InferSelectModel; -export type OrgAuthPage = InferSelectModel; diff --git a/server/lib/createResponseBodySchema.ts b/server/lib/createResponseBodySchema.ts deleted file mode 100644 index 478cc0c3..00000000 --- a/server/lib/createResponseBodySchema.ts +++ /dev/null @@ -1,13 +0,0 @@ -import z, { type ZodSchema } from "zod"; - -export function createResponseBodySchema(dataSchema: T) { - return z.object({ - data: dataSchema.nullable(), - success: z.boolean(), - error: z.boolean(), - message: z.string(), - status: z.number() - }); -} - -export default createResponseBodySchema; diff --git a/server/private/routers/authPage/getOrgAuthPage.ts b/server/private/routers/authPage/getOrgAuthPage.ts deleted file mode 100644 index 03c03ac9..00000000 --- a/server/private/routers/authPage/getOrgAuthPage.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { eq } from "drizzle-orm"; -/* - * 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, orgAuthPages } from "@server/db"; -import { orgs } 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 { OpenAPITags, registry } from "@server/openApi"; -import createResponseBodySchema from "@server/lib/createResponseBodySchema"; - -const getOrgAuthPageParamsSchema = z - .object({ - orgId: z.string() - }) - .strict(); - -const reponseSchema = createResponseBodySchema( - z - .object({ - logoUrl: z.string().url(), - logoWidth: z.number().min(1), - logoHeight: z.number().min(1), - title: z.string(), - subtitle: z.string().optional(), - resourceTitle: z.string(), - resourceSubtitle: z.string().optional() - }) - .strict() -); - -export type GetOrgAuthPageResponse = z.infer; - -registry.registerPath({ - method: "get", - path: "/org/{orgId}/auth-page", - description: "Get an organization auth page", - tags: [OpenAPITags.Org], - request: { - params: getOrgAuthPageParamsSchema - }, - responses: { - 200: { - description: "", - content: { - "application/json": { - schema: reponseSchema - } - } - } - } -}); - -export async function getOrgAuthPage( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = getOrgAuthPageParamsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const { orgId } = parsedParams.data; - - const [orgAuthPage] = await db - .select() - .from(orgAuthPages) - .leftJoin(orgs, eq(orgs.orgId, orgAuthPages.orgId)) - .where(eq(orgs.orgId, orgId)) - .limit(1); - - return response(res, { - data: orgAuthPage?.orgAuthPages ?? null, - success: true, - error: false, - message: "Organization auth page 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/authPage/index.ts b/server/private/routers/authPage/index.ts deleted file mode 100644 index 2a01a879..00000000 --- a/server/private/routers/authPage/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * 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. - */ - -export * from "./updateOrgAuthPage"; -export * from "./getOrgAuthPage"; diff --git a/server/private/routers/authPage/updateOrgAuthPage.ts b/server/private/routers/authPage/updateOrgAuthPage.ts deleted file mode 100644 index e6fa04f4..00000000 --- a/server/private/routers/authPage/updateOrgAuthPage.ts +++ /dev/null @@ -1,141 +0,0 @@ -/* - * 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, orgAuthPages } 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 { OpenAPITags, registry } from "@server/openApi"; -import createResponseBodySchema from "@server/lib/createResponseBodySchema"; - -const updateOrgAuthPageParamsSchema = z - .object({ - orgId: z.string() - }) - .strict(); - -const updateOrgAuthPageBodySchema = z - .object({ - logoUrl: z.string().url(), - logoWidth: z.number().min(1), - logoHeight: z.number().min(1), - title: z.string(), - subtitle: z.string().optional(), - resourceTitle: z.string(), - resourceSubtitle: z.string().optional() - }) - .strict(); - -const reponseSchema = createResponseBodySchema(updateOrgAuthPageBodySchema); - -export type UpdateOrgAuthPageResponse = z.infer; - -registry.registerPath({ - method: "put", - path: "/org/{orgId}/auth-page", - description: "Update an organization auth page", - tags: [OpenAPITags.Org], - request: { - params: updateOrgAuthPageParamsSchema, - body: { - content: { - "application/json": { - schema: updateOrgAuthPageBodySchema - } - } - } - }, - responses: { - 200: { - description: "", - content: { - "application/json": { - schema: reponseSchema - } - } - } - } -}); - -export async function updateOrgAuthPage( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = updateOrgAuthPageParamsSchema.safeParse( - req.params - ); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const parsedBody = updateOrgAuthPageBodySchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { orgId } = parsedParams.data; - const body = parsedBody.data; - - const updatedOrgAuthPages = await db - .insert(orgAuthPages) - .values({ - ...body, - orgId - }) - .onConflictDoUpdate({ - target: orgAuthPages.orgId, - set: { - ...body - } - }) - .returning(); - - if (updatedOrgAuthPages.length === 0) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Organization with ID ${orgId} not found` - ) - ); - } - - return response(res, { - data: updatedOrgAuthPages[0], - success: true, - error: false, - message: "Organization auth page updated 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/external.ts b/server/private/routers/external.ts index 6d1cf6df..2a32d9ac 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -17,7 +17,6 @@ import * as billing from "#private/routers/billing"; import * as remoteExitNode from "#private/routers/remoteExitNode"; import * as loginPage from "#private/routers/loginPage"; import * as orgIdp from "#private/routers/orgIdp"; -import * as authPage from "#private/routers/authPage"; import * as domain from "#private/routers/domain"; import * as auth from "#private/routers/auth"; import * as license from "#private/routers/license"; @@ -309,6 +308,33 @@ authenticated.get( loginPage.getLoginPage ); +authenticated.get( + "/org/:orgId/login-page-branding", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.getLoginPage), + logActionAudit(ActionsEnum.getLoginPage), + loginPage.getLoginPageBranding +); + +authenticated.put( + "/org/:orgId/login-page-branding", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.updateLoginPage), + logActionAudit(ActionsEnum.updateLoginPage), + loginPage.upsertLoginPageBranding +); + +authenticated.delete( + "/org/:orgId/login-page-branding", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.deleteLoginPage), + logActionAudit(ActionsEnum.deleteLoginPage), + loginPage.deleteLoginPageBranding +); + authRouter.post( "/remoteExitNode/get-token", verifyValidLicense, @@ -404,23 +430,3 @@ authenticated.get( logActionAudit(ActionsEnum.exportLogs), logs.exportAccessAuditLogs ); - -authenticated.put( - "/org/:orgId/auth-page", - verifyValidLicense, - verifyValidSubscription, - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.updateOrgAuthPage), - logActionAudit(ActionsEnum.updateOrgAuthPage), - authPage.updateOrgAuthPage -); - -authenticated.get( - "/org/:orgId/auth-page", - verifyValidLicense, - verifyValidSubscription, - verifyOrgAccess, - verifyUserHasAction(ActionsEnum.getOrgAuthPage), - logActionAudit(ActionsEnum.getOrgAuthPage), - authPage.getOrgAuthPage -); diff --git a/server/private/routers/loginPage/deleteLoginPageBranding.ts b/server/private/routers/loginPage/deleteLoginPageBranding.ts new file mode 100644 index 00000000..03bdf805 --- /dev/null +++ b/server/private/routers/loginPage/deleteLoginPageBranding.ts @@ -0,0 +1,113 @@ +/* + * 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, + LoginPageBranding, + loginPageBranding, + loginPageBrandingOrg +} 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 { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { build } from "@server/build"; + +const paramsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +export async function deleteLoginPageBranding( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + } + + const [existingLoginPageBranding] = await db + .select() + .from(loginPageBranding) + .innerJoin( + loginPageBrandingOrg, + eq( + loginPageBrandingOrg.loginPageBrandingId, + loginPageBranding.loginPageBrandingId + ) + ) + .where(eq(loginPageBrandingOrg.orgId, orgId)); + + if (!existingLoginPageBranding) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Login page branding not found" + ) + ); + } + + await db + .delete(loginPageBranding) + .where( + eq( + loginPageBranding.loginPageBrandingId, + existingLoginPageBranding.loginPageBranding + .loginPageBrandingId + ) + ); + + return response(res, { + data: existingLoginPageBranding.loginPageBranding, + success: true, + error: false, + message: "Login page branding deleted 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/loginPage/getLoginPageBranding.ts b/server/private/routers/loginPage/getLoginPageBranding.ts new file mode 100644 index 00000000..e0670545 --- /dev/null +++ b/server/private/routers/loginPage/getLoginPageBranding.ts @@ -0,0 +1,103 @@ +/* + * 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, + LoginPageBranding, + loginPageBranding, + loginPageBrandingOrg +} 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 { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { build } from "@server/build"; + +const paramsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +export async function getLoginPageBranding( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + } + + const [existingLoginPageBranding] = await db + .select() + .from(loginPageBranding) + .innerJoin( + loginPageBrandingOrg, + eq( + loginPageBrandingOrg.loginPageBrandingId, + loginPageBranding.loginPageBrandingId + ) + ) + .where(eq(loginPageBrandingOrg.orgId, orgId)); + + if (!existingLoginPageBranding) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Login page branding not found" + ) + ); + } + + return response(res, { + data: existingLoginPageBranding.loginPageBranding, + success: true, + error: false, + message: "Login page branding retrieved 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/loginPage/index.ts b/server/private/routers/loginPage/index.ts index 2372ddfa..d3ae6540 100644 --- a/server/private/routers/loginPage/index.ts +++ b/server/private/routers/loginPage/index.ts @@ -17,3 +17,6 @@ export * from "./getLoginPage"; export * from "./loadLoginPage"; export * from "./updateLoginPage"; export * from "./deleteLoginPage"; +export * from "./upsertLoginPageBranding"; +export * from "./deleteLoginPageBranding"; +export * from "./getLoginPageBranding"; diff --git a/server/private/routers/loginPage/upsertLoginPageBranding.ts b/server/private/routers/loginPage/upsertLoginPageBranding.ts new file mode 100644 index 00000000..51aa7392 --- /dev/null +++ b/server/private/routers/loginPage/upsertLoginPageBranding.ts @@ -0,0 +1,154 @@ +/* + * 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, + LoginPageBranding, + loginPageBranding, + loginPageBrandingOrg +} 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 { getOrgTierData } from "#private/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { build } from "@server/build"; + +const paramsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +const bodySchema = z + .object({ + logoUrl: z.string().url(), + logoWidth: z.number().min(1), + logoHeight: z.number().min(1), + title: z.string(), + subtitle: z.string().optional(), + resourceTitle: z.string(), + resourceSubtitle: z.string().optional() + }) + .strict(); + +export type UpdateLoginPageBrandingBody = z.infer; + +export async function upsertLoginPageBranding( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = bodySchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const updateData = parsedBody.data; + + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + if (build === "saas") { + const { tier } = await getOrgTierData(orgId); + const subscribed = tier === TierId.STANDARD; + if (!subscribed) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "This organization's current plan does not support this feature." + ) + ); + } + } + + const [existingLoginPageBranding] = await db + .select() + .from(loginPageBranding) + .innerJoin( + loginPageBrandingOrg, + eq( + loginPageBrandingOrg.loginPageBrandingId, + loginPageBranding.loginPageBrandingId + ) + ) + .where(eq(loginPageBrandingOrg.orgId, orgId)); + + let updatedLoginPageBranding: LoginPageBranding; + + if (existingLoginPageBranding) { + updatedLoginPageBranding = await db.transaction(async (tx) => { + const [branding] = await tx + .update(loginPageBranding) + .set({ ...updateData }) + .where( + eq( + loginPageBranding.loginPageBrandingId, + existingLoginPageBranding.loginPageBranding + .loginPageBrandingId + ) + ) + .returning(); + return branding; + }); + } else { + updatedLoginPageBranding = await db.transaction(async (tx) => { + const [branding] = await tx + .insert(loginPageBranding) + .values({ ...updateData }) + .returning(); + + await tx.insert(loginPageBrandingOrg).values({ + loginPageBrandingId: branding.loginPageBrandingId, + orgId: orgId + }); + return branding; + }); + } + + return response(res, { + data: updatedLoginPageBranding, + success: true, + error: false, + message: existingLoginPageBranding + ? "Login page branding updated successfully" + : "Login page branding created successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/loginPage/types.ts b/server/routers/loginPage/types.ts index 26f59cab..652ed4e9 100644 --- a/server/routers/loginPage/types.ts +++ b/server/routers/loginPage/types.ts @@ -1,4 +1,4 @@ -import { LoginPage } from "@server/db"; +import type { LoginPage, LoginPageBranding } from "@server/db"; export type CreateLoginPageResponse = LoginPage; @@ -8,4 +8,6 @@ export type GetLoginPageResponse = LoginPage; export type UpdateLoginPageResponse = LoginPage; -export type LoadLoginPageResponse = LoginPage & { orgId: string }; \ No newline at end of file +export type LoadLoginPageResponse = LoginPage & { orgId: string }; + +export type GetLoginPageBrandingResponse = LoginPageBranding; diff --git a/src/app/[orgId]/settings/general/auth-page/page.tsx b/src/app/[orgId]/settings/general/auth-page/page.tsx new file mode 100644 index 00000000..a0c883f0 --- /dev/null +++ b/src/app/[orgId]/settings/general/auth-page/page.tsx @@ -0,0 +1,64 @@ +import AuthPageBrandingForm from "@app/components/AuthPageBrandingForm"; +import AuthPageSettings from "@app/components/private/AuthPageSettings"; +import { SettingsContainer } from "@app/components/Settings"; +import { priv } from "@app/lib/api"; +import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; +import { pullEnv } from "@app/lib/pullEnv"; +import { build } from "@server/build"; +import { TierId } from "@server/lib/billing/tiers"; +import type { GetOrgTierResponse } from "@server/routers/billing/types"; +import { + GetLoginPageBrandingResponse, + GetLoginPageResponse +} from "@server/routers/loginPage/types"; +import { AxiosResponse } from "axios"; +import { redirect } from "next/navigation"; + +export interface AuthPageProps { + params: Promise<{ orgId: string }>; +} + +export default async function AuthPage(props: AuthPageProps) { + const orgId = (await props.params).orgId; + const env = pullEnv(); + let subscriptionStatus: GetOrgTierResponse | null = null; + try { + const subRes = await getCachedSubscription(orgId); + subscriptionStatus = subRes.data.data; + } catch {} + const subscribed = + build === "enterprise" + ? true + : subscriptionStatus?.tier === TierId.STANDARD; + + if (!subscribed) { + redirect(env.app.dashboardUrl); + } + + let loginPage: GetLoginPageResponse | null = null; + try { + const res = await priv.get>( + `/org/${orgId}/login-page` + ); + if (res.status === 200) { + loginPage = res.data.data; + } + } catch (error) {} + + let loginPageBranding: GetLoginPageBrandingResponse | null = null; + try { + const res = await priv.get>( + `/org/${orgId}/login-page-branding` + ); + if (res.status === 200) { + loginPageBranding = res.data.data; + } + } catch (error) {} + + return ( + + + + + ); +} diff --git a/src/app/[orgId]/settings/general/auth-pages/page.tsx b/src/app/[orgId]/settings/general/auth-pages/page.tsx deleted file mode 100644 index 79065513..00000000 --- a/src/app/[orgId]/settings/general/auth-pages/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import AuthPageCustomizationForm from "@app/components/AuthPagesCustomizationForm"; -import { SettingsContainer } from "@app/components/Settings"; -import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; -import { pullEnv } from "@app/lib/pullEnv"; -import { build } from "@server/build"; -import { TierId } from "@server/lib/billing/tiers"; -import type { GetOrgTierResponse } from "@server/routers/billing/types"; -import { redirect } from "next/navigation"; - -export interface AuthPageProps { - params: Promise<{ orgId: string }>; -} - -export default async function AuthPage(props: AuthPageProps) { - const orgId = (await props.params).orgId; - const env = pullEnv(); - let subscriptionStatus: GetOrgTierResponse | null = null; - try { - const subRes = await getCachedSubscription(orgId); - subscriptionStatus = subRes.data.data; - } catch {} - const subscribed = - build === "enterprise" - ? true - : subscriptionStatus?.tier === TierId.STANDARD; - - if (!subscribed) { - redirect(env.app.dashboardUrl); - } - - return ( - - - - ); -} diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 5ace97ca..9472eb52 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -73,7 +73,7 @@ export default async function GeneralSettingsPage({ if (subscribed) { navItems.push({ title: t("authPage"), - href: `/{orgId}/settings/general/auth-pages` + href: `/{orgId}/settings/general/auth-page` }); } diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index fdedba5c..5dd5ccb8 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -43,8 +43,7 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionForm, - SettingsSectionFooter + SettingsSectionForm } from "@app/components/Settings"; import { useUserContext } from "@app/hooks/useUserContext"; import { useTranslations } from "next-intl"; @@ -129,7 +128,6 @@ export default function GeneralPage() { const [loadingSave, setLoadingSave] = useState(false); const [isSecurityPolicyConfirmOpen, setIsSecurityPolicyConfirmOpen] = useState(false); - const authPageSettingsRef = useRef(null); const form = useForm({ resolver: zodResolver(GeneralFormSchema), @@ -252,14 +250,6 @@ export default function GeneralPage() { // Update organization await api.post(`/org/${org?.org.orgId}`, reqData); - // Also save auth page settings if they have unsaved changes - if ( - build === "saas" && - authPageSettingsRef.current?.hasUnsavedChanges() - ) { - await authPageSettingsRef.current.saveAuthSettings(); - } - toast({ title: t("orgUpdated"), description: t("orgUpdatedDescription") @@ -600,7 +590,7 @@ export default function GeneralPage() { - + - {build === "saas" && } -
{build !== "saas" && ( + )} */} + +
+ + + ); +} diff --git a/src/components/AuthPagesCustomizationForm.tsx b/src/components/AuthPagesCustomizationForm.tsx deleted file mode 100644 index fc695eeb..00000000 --- a/src/components/AuthPagesCustomizationForm.tsx +++ /dev/null @@ -1,56 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import * as React 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"; - -export type AuthPageCustomizationProps = { - orgId: string; -}; - -const AuthPageFormSchema = z.object({ - logoUrl: z.string().url(), - logoWidth: z.number().min(1), - logoHeight: z.number().min(1), - title: z.string(), - subtitle: z.string().optional(), - resourceTitle: z.string(), - resourceSubtitle: z.string().optional() -}); - -export default function AuthPageCustomizationForm({ - orgId -}: AuthPageCustomizationProps) { - const [, formAction, isSubmitting] = React.useActionState(onSubmit, null); - - const form = useForm({ - resolver: zodResolver(AuthPageFormSchema), - defaultValues: { - title: `Log in to {{orgName}}`, - resourceTitle: `Authenticate to access {{resourceName}}` - } - }); - - async function onSubmit() { - const isValid = await form.trigger(); - - if (!isValid) return; - // ... - } - - return ( -
- -
- ); -} diff --git a/src/components/private/AuthPageSettings.tsx b/src/components/private/AuthPageSettings.tsx index 95097a33..e6cd017d 100644 --- a/src/components/private/AuthPageSettings.tsx +++ b/src/components/private/AuthPageSettings.tsx @@ -3,7 +3,15 @@ import { Button } from "@app/components/ui/button"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { toast } from "@app/hooks/useToast"; -import { useState, useEffect, forwardRef, useImperativeHandle } from "react"; +import { + useState, + useEffect, + forwardRef, + useImperativeHandle, + RefObject, + Ref, + useActionState +} from "react"; import { Form, FormControl, @@ -52,7 +60,6 @@ import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; import { InfoPopup } from "@app/components/ui/info-popup"; import { Alert, AlertDescription } from "@app/components/ui/alert"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; -import { TierId } from "@server/lib/billing/tiers"; import { build } from "@server/build"; // Auth page form schema @@ -66,6 +73,7 @@ type AuthPageFormValues = z.infer; interface AuthPageSettingsProps { onSaveSuccess?: () => void; onSaveError?: (error: any) => void; + loginPage: GetLoginPageResponse | null; } export interface AuthPageSettingsRef { @@ -73,476 +81,434 @@ export interface AuthPageSettingsRef { hasUnsavedChanges: () => boolean; } -const AuthPageSettings = forwardRef( - ({ onSaveSuccess, onSaveError }, ref) => { - const { org } = useOrgContext(); - const api = createApiClient(useEnvContext()); - const router = useRouter(); - const t = useTranslations(); - const { env } = useEnvContext(); +function AuthPageSettings({ + onSaveSuccess, + onSaveError, + loginPage: defaultLoginPage +}: AuthPageSettingsProps) { + const { org } = useOrgContext(); + const api = createApiClient(useEnvContext()); + const router = useRouter(); + const t = useTranslations(); + const { env } = useEnvContext(); - const subscription = useSubscriptionStatusContext(); + const subscription = useSubscriptionStatusContext(); - // Auth page domain state - const [loginPage, setLoginPage] = useState( - null - ); - const [loginPageExists, setLoginPageExists] = useState(false); - const [editDomainOpen, setEditDomainOpen] = useState(false); - const [baseDomains, setBaseDomains] = useState([]); - const [selectedDomain, setSelectedDomain] = useState<{ - domainId: string; - subdomain?: string; - fullDomain: string; - baseDomain: string; - } | null>(null); - const [loadingLoginPage, setLoadingLoginPage] = useState(true); - const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const [loadingSave, setLoadingSave] = useState(false); + // Auth page domain state + const [loginPage, setLoginPage] = useState(defaultLoginPage); + const [, formAction, isSubmitting] = useActionState(onSubmit, null); + const [loginPageExists, setLoginPageExists] = useState( + Boolean(defaultLoginPage) + ); + const [editDomainOpen, setEditDomainOpen] = useState(false); + const [baseDomains, setBaseDomains] = useState([]); + const [selectedDomain, setSelectedDomain] = useState<{ + domainId: string; + subdomain?: string; + fullDomain: string; + baseDomain: string; + } | null>(null); + const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); - const form = useForm({ - resolver: zodResolver(AuthPageFormSchema), - defaultValues: { - authPageDomainId: loginPage?.domainId || "", - authPageSubdomain: loginPage?.subdomain || "" - }, - mode: "onChange" - }); + const form = useForm({ + resolver: zodResolver(AuthPageFormSchema), + defaultValues: { + authPageDomainId: loginPage?.domainId || "", + authPageSubdomain: loginPage?.subdomain || "" + }, + mode: "onChange" + }); - // Expose save function to parent component - useImperativeHandle( - ref, - () => ({ - saveAuthSettings: async () => { - await form.handleSubmit(onSubmit)(); - }, - hasUnsavedChanges: () => hasUnsavedChanges - }), - [form, hasUnsavedChanges] - ); - - // Fetch login page and domains data - useEffect(() => { - const fetchLoginPage = async () => { - try { - const res = await api.get< - AxiosResponse - >(`/org/${org?.org.orgId}/login-page`); - if (res.status === 200) { - setLoginPage(res.data.data); - setLoginPageExists(true); - // Update form with login page data - form.setValue( - "authPageDomainId", - res.data.data.domainId || "" - ); - form.setValue( - "authPageSubdomain", - res.data.data.subdomain || "" - ); - } - } catch (err) { - // Login page doesn't exist yet, that's okay - setLoginPage(null); - setLoginPageExists(false); - } finally { - setLoadingLoginPage(false); - } - }; - - const fetchDomains = async () => { - try { - const res = await api.get< - AxiosResponse - >(`/org/${org?.org.orgId}/domains/`); - if (res.status === 200) { - const rawDomains = res.data.data.domains as DomainRow[]; - const domains = rawDomains.map((domain) => ({ - ...domain, - baseDomain: toUnicode(domain.baseDomain) - })); - setBaseDomains(domains); - } - } catch (err) { - console.error("Failed to fetch domains:", err); - } - }; - - if (org?.org.orgId) { - fetchLoginPage(); - fetchDomains(); - } - }, []); - - // Handle domain selection from modal - function handleDomainSelection(domain: { - domainId: string; - subdomain?: string; - fullDomain: string; - baseDomain: string; - }) { - form.setValue("authPageDomainId", domain.domainId); - form.setValue("authPageSubdomain", domain.subdomain || ""); - setEditDomainOpen(false); - - // Update loginPage state to show the selected domain immediately - const sanitizedSubdomain = domain.subdomain - ? finalizeSubdomainSanitize(domain.subdomain) - : ""; - - const sanitizedFullDomain = sanitizedSubdomain - ? `${sanitizedSubdomain}.${domain.baseDomain}` - : domain.baseDomain; - - // Only update loginPage state if a login page already exists - if (loginPageExists && loginPage) { - setLoginPage({ - ...loginPage, - domainId: domain.domainId, - subdomain: sanitizedSubdomain, - fullDomain: sanitizedFullDomain - }); - } - - setHasUnsavedChanges(true); - } - - // Clear auth page domain - function clearAuthPageDomain() { - form.setValue("authPageDomainId", ""); - form.setValue("authPageSubdomain", ""); - setLoginPage(null); - setHasUnsavedChanges(true); - } - - async function onSubmit(data: AuthPageFormValues) { - setLoadingSave(true); + // Expose save function to parent component + // useImperativeHandle( + // ref, + // () => ({ + // saveAuthSettings: async () => { + // await form.handleSubmit(onSubmit)(); + // }, + // hasUnsavedChanges: () => hasUnsavedChanges + // }), + // [form, hasUnsavedChanges] + // ); + // Fetch login page and domains data + useEffect(() => { + const fetchDomains = async () => { try { - // Handle auth page domain - if (data.authPageDomainId) { - if ( - build === "enterprise" || - (build === "saas" && subscription?.subscribed) - ) { - const sanitizedSubdomain = data.authPageSubdomain - ? finalizeSubdomainSanitize(data.authPageSubdomain) - : ""; + const res = await api.get>( + `/org/${org?.org.orgId}/domains/` + ); + if (res.status === 200) { + const rawDomains = res.data.data.domains as DomainRow[]; + const domains = rawDomains.map((domain) => ({ + ...domain, + baseDomain: toUnicode(domain.baseDomain) + })); + setBaseDomains(domains); + } + } catch (err) { + console.error("Failed to fetch domains:", err); + } + }; - if (loginPageExists) { - // Login page exists on server - need to update it - // First, we need to get the loginPageId from the server since loginPage might be null locally - let loginPageId: number; + if (org?.org.orgId) { + fetchDomains(); + } + }, []); - if (loginPage) { - // We have the loginPage data locally - loginPageId = loginPage.loginPageId; - } else { - // User cleared selection locally, but login page still exists on server - // We need to fetch it to get the loginPageId - const fetchRes = await api.get< - AxiosResponse - >(`/org/${org?.org.orgId}/login-page`); - loginPageId = fetchRes.data.data.loginPageId; - } + // Handle domain selection from modal + function handleDomainSelection(domain: { + domainId: string; + subdomain?: string; + fullDomain: string; + baseDomain: string; + }) { + form.setValue("authPageDomainId", domain.domainId); + form.setValue("authPageSubdomain", domain.subdomain || ""); + setEditDomainOpen(false); - // Update existing auth page domain - const updateRes = await api.post( - `/org/${org?.org.orgId}/login-page/${loginPageId}`, - { - domainId: data.authPageDomainId, - subdomain: sanitizedSubdomain || null - } - ); + // Update loginPage state to show the selected domain immediately + const sanitizedSubdomain = domain.subdomain + ? finalizeSubdomainSanitize(domain.subdomain) + : ""; - if (updateRes.status === 201) { - setLoginPage(updateRes.data.data); - setLoginPageExists(true); - } + const sanitizedFullDomain = sanitizedSubdomain + ? `${sanitizedSubdomain}.${domain.baseDomain}` + : domain.baseDomain; + + // Only update loginPage state if a login page already exists + if (loginPageExists && loginPage) { + setLoginPage({ + ...loginPage, + domainId: domain.domainId, + subdomain: sanitizedSubdomain, + fullDomain: sanitizedFullDomain + }); + } + + setHasUnsavedChanges(true); + } + + // Clear auth page domain + function clearAuthPageDomain() { + form.setValue("authPageDomainId", ""); + form.setValue("authPageSubdomain", ""); + setLoginPage(null); + setHasUnsavedChanges(true); + } + + async function onSubmit() { + const isValid = await form.trigger(); + if (!isValid) return; + + const data = form.getValues(); + + try { + // Handle auth page domain + if (data.authPageDomainId) { + if ( + build === "enterprise" || + (build === "saas" && subscription?.subscribed) + ) { + const sanitizedSubdomain = data.authPageSubdomain + ? finalizeSubdomainSanitize(data.authPageSubdomain) + : ""; + + if (loginPageExists) { + // Login page exists on server - need to update it + // First, we need to get the loginPageId from the server since loginPage might be null locally + let loginPageId: number; + + if (loginPage) { + // We have the loginPage data locally + loginPageId = loginPage.loginPageId; } else { - // No login page exists on server - create new one - const createRes = await api.put( - `/org/${org?.org.orgId}/login-page`, - { - domainId: data.authPageDomainId, - subdomain: sanitizedSubdomain || null - } - ); + // User cleared selection locally, but login page still exists on server + // We need to fetch it to get the loginPageId + const fetchRes = await api.get< + AxiosResponse + >(`/org/${org?.org.orgId}/login-page`); + loginPageId = fetchRes.data.data.loginPageId; + } - if (createRes.status === 201) { - setLoginPage(createRes.data.data); - setLoginPageExists(true); + // Update existing auth page domain + const updateRes = await api.post( + `/org/${org?.org.orgId}/login-page/${loginPageId}`, + { + domainId: data.authPageDomainId, + subdomain: sanitizedSubdomain || null } + ); + + if (updateRes.status === 201) { + setLoginPage(updateRes.data.data); + setLoginPageExists(true); + } + } else { + // No login page exists on server - create new one + const createRes = await api.put( + `/org/${org?.org.orgId}/login-page`, + { + domainId: data.authPageDomainId, + subdomain: sanitizedSubdomain || null + } + ); + + if (createRes.status === 201) { + setLoginPage(createRes.data.data); + setLoginPageExists(true); } } - } else if (loginPageExists) { - // Delete existing auth page domain if no domain selected - let loginPageId: number; + } + } else if (loginPageExists) { + // Delete existing auth page domain if no domain selected + let loginPageId: number; - if (loginPage) { - // We have the loginPage data locally - loginPageId = loginPage.loginPageId; - } else { - // User cleared selection locally, but login page still exists on server - // We need to fetch it to get the loginPageId - const fetchRes = await api.get< - AxiosResponse - >(`/org/${org?.org.orgId}/login-page`); - loginPageId = fetchRes.data.data.loginPageId; - } - - await api.delete( - `/org/${org?.org.orgId}/login-page/${loginPageId}` - ); - setLoginPage(null); - setLoginPageExists(false); + if (loginPage) { + // We have the loginPage data locally + loginPageId = loginPage.loginPageId; + } else { + // User cleared selection locally, but login page still exists on server + // We need to fetch it to get the loginPageId + const fetchRes = await api.get< + AxiosResponse + >(`/org/${org?.org.orgId}/login-page`); + loginPageId = fetchRes.data.data.loginPageId; } - setHasUnsavedChanges(false); - router.refresh(); - onSaveSuccess?.(); - } catch (e) { - toast({ - variant: "destructive", - title: t("authPageErrorUpdate"), - description: formatAxiosError( - e, - t("authPageErrorUpdateMessage") - ) - }); - onSaveError?.(e); - } finally { - setLoadingSave(false); + await api.delete( + `/org/${org?.org.orgId}/login-page/${loginPageId}` + ); + setLoginPage(null); + setLoginPageExists(false); } + + setHasUnsavedChanges(false); + router.refresh(); + onSaveSuccess?.(); + } catch (e) { + toast({ + variant: "destructive", + title: t("authPageErrorUpdate"), + description: formatAxiosError( + e, + t("authPageErrorUpdateMessage") + ) + }); + onSaveError?.(e); } - - return ( - <> - - - - {t("authPage")} - - - {t("authPageDescription")} - - - - {build === "saas" && !subscription?.subscribed ? ( - - - {t("orgAuthPageDisabled")}{" "} - {t("subscriptionRequiredToUse")} - - - ) : null} - - - {loadingLoginPage ? ( -
-
- {t("loading")} -
-
- ) : ( -
- -
- -
- - - {loginPage && - !loginPage.domainId ? ( - - ) : loginPage?.fullDomain ? ( - - {`${window.location.protocol}//${loginPage.fullDomain}`} - - ) : form.watch( - "authPageDomainId" - ) ? ( - // Show selected domain from form state when no loginPage exists yet - (() => { - const selectedDomainId = - form.watch( - "authPageDomainId" - ); - const selectedSubdomain = - form.watch( - "authPageSubdomain" - ); - const domain = - baseDomains.find( - (d) => - d.domainId === - selectedDomainId - ); - if (domain) { - const sanitizedSubdomain = - selectedSubdomain - ? finalizeSubdomainSanitize( - selectedSubdomain - ) - : ""; - const fullDomain = - sanitizedSubdomain - ? `${sanitizedSubdomain}.${domain.baseDomain}` - : domain.baseDomain; - return fullDomain; - } - return t( - "noDomainSet" - ); - })() - ) : ( - t("noDomainSet") - )} - -
- - {form.watch( - "authPageDomainId" - ) && ( - - )} -
-
- - {!form.watch( - "authPageDomainId" - ) && ( -
- {t( - "addDomainToEnableCustomAuthPages" - )} -
- )} - - {env.flags - .usePangolinDns && - (build === "enterprise" || - (build === "saas" && - subscription?.subscribed)) && - loginPage?.domainId && - loginPage?.fullDomain && - !hasUnsavedChanges && ( - - )} -
-
- - )} -
-
-
- - {/* Domain Picker Modal */} - setEditDomainOpen(setOpen)} - > - - - - {loginPage - ? t("editAuthPageDomain") - : t("setAuthPageDomain")} - - - {t("selectDomainForOrgAuthPage")} - - - - { - const selected = { - domainId: res.domainId, - subdomain: res.subdomain, - fullDomain: res.fullDomain, - baseDomain: res.baseDomain - }; - setSelectedDomain(selected); - }} - /> - - - - - - - - - - - ); } -); + + return ( + <> + + + {t("authPage")} + + {t("authPageDescription")} + + + + {build === "saas" && !subscription?.subscribed ? ( + + + {t("orgAuthPageDisabled")}{" "} + {t("subscriptionRequiredToUse")} + + + ) : null} + + +
+ +
+ +
+ + + {loginPage && + !loginPage.domainId ? ( + + ) : loginPage?.fullDomain ? ( + + {`${window.location.protocol}//${loginPage.fullDomain}`} + + ) : form.watch( + "authPageDomainId" + ) ? ( + // Show selected domain from form state when no loginPage exists yet + (() => { + const selectedDomainId = + form.watch( + "authPageDomainId" + ); + const selectedSubdomain = + form.watch( + "authPageSubdomain" + ); + const domain = + baseDomains.find( + (d) => + d.domainId === + selectedDomainId + ); + if (domain) { + const sanitizedSubdomain = + selectedSubdomain + ? finalizeSubdomainSanitize( + selectedSubdomain + ) + : ""; + const fullDomain = + sanitizedSubdomain + ? `${sanitizedSubdomain}.${domain.baseDomain}` + : domain.baseDomain; + return fullDomain; + } + return t("noDomainSet"); + })() + ) : ( + t("noDomainSet") + )} + +
+ + {form.watch("authPageDomainId") && ( + + )} +
+
+ + {!form.watch("authPageDomainId") && ( +
+ {t( + "addDomainToEnableCustomAuthPages" + )} +
+ )} + + {env.flags.usePangolinDns && + (build === "enterprise" || + (build === "saas" && + subscription?.subscribed)) && + loginPage?.domainId && + loginPage?.fullDomain && + !hasUnsavedChanges && ( + + )} +
+
+ +
+
+ +
+ +
+
+ + {/* Domain Picker Modal */} + setEditDomainOpen(setOpen)} + > + + + + {loginPage + ? t("editAuthPageDomain") + : t("setAuthPageDomain")} + + + {t("selectDomainForOrgAuthPage")} + + + + { + const selected = { + domainId: res.domainId, + subdomain: res.subdomain, + fullDomain: res.fullDomain, + baseDomain: res.baseDomain + }; + setSelectedDomain(selected); + }} + /> + + + + + + + + + + + ); +} AuthPageSettings.displayName = "AuthPageSettings"; From 4bd1c4e0c650a5f17241ce4bddfc834c8e8af788 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 12 Nov 2025 03:50:04 +0100 Subject: [PATCH 007/153] =?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 | 2 +- src/components/private/AuthPageSettings.tsx | 16 +--------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index b790ed20..7b395d0a 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1746,7 +1746,7 @@ "brandingResourceTitle": "Title for Resource Auth Page", "brandingResourceSubtitle": "Subtitle for Resource Auth Page", "brandingResourceDescription": "{resourceName} will be replaced with the organization's name", - "saveAuthPage": "Save Auth Page", + "saveAuthPageDomain": "Save Domain", "saveAuthPageBranding": "Save Branding", "removeAuthPageBranding": "Remove Branding", "noDomainSet": "No domain set", diff --git a/src/components/private/AuthPageSettings.tsx b/src/components/private/AuthPageSettings.tsx index e6cd017d..b20a1876 100644 --- a/src/components/private/AuthPageSettings.tsx +++ b/src/components/private/AuthPageSettings.tsx @@ -68,8 +68,6 @@ const AuthPageFormSchema = z.object({ authPageSubdomain: z.string().optional() }); -type AuthPageFormValues = z.infer; - interface AuthPageSettingsProps { onSaveSuccess?: () => void; onSaveError?: (error: any) => void; @@ -119,18 +117,6 @@ function AuthPageSettings({ mode: "onChange" }); - // Expose save function to parent component - // useImperativeHandle( - // ref, - // () => ({ - // saveAuthSettings: async () => { - // await form.handleSubmit(onSubmit)(); - // }, - // hasUnsavedChanges: () => hasUnsavedChanges - // }), - // [form, hasUnsavedChanges] - // ); - // Fetch login page and domains data useEffect(() => { const fetchDomains = async () => { @@ -452,7 +438,7 @@ function AuthPageSettings({ loading={isSubmitting} disabled={isSubmitting || !hasUnsavedChanges} > - {t("saveAuthPage")} + {t("saveAuthPageDomain")} From d218a4bbc3a85d11f712f2b9635c312b34af3878 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 12 Nov 2025 03:50:11 +0100 Subject: [PATCH 008/153] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20fix=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/(private)/idp/[idpId]/layout.tsx | 4 ++-- src/app/admin/idp/[idpId]/layout.tsx | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx b/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx index 7cdea07a..6cdbf23c 100644 --- a/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx +++ b/src/app/[orgId]/settings/(private)/idp/[idpId]/layout.tsx @@ -3,7 +3,7 @@ import { GetIdpResponse as GetOrgIdpResponse } from "@server/routers/idp"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; @@ -28,7 +28,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { redirect(`/${params.orgId}/settings/idp`); } - const navItems: HorizontalTabs = [ + const navItems: TabItem[] = [ { title: t("general"), href: `/${params.orgId}/settings/idp/${params.idpId}/general` diff --git a/src/app/admin/idp/[idpId]/layout.tsx b/src/app/admin/idp/[idpId]/layout.tsx index af64e440..9634a3de 100644 --- a/src/app/admin/idp/[idpId]/layout.tsx +++ b/src/app/admin/idp/[idpId]/layout.tsx @@ -3,7 +3,7 @@ import { GetIdpResponse } from "@server/routers/idp"; import { AxiosResponse } from "axios"; import { redirect } from "next/navigation"; import { authCookieHeader } from "@app/lib/api/cookies"; -import { HorizontalTabs } from "@app/components/HorizontalTabs"; +import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { getTranslations } from "next-intl/server"; @@ -28,13 +28,13 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { redirect("/admin/idp"); } - const navItems: HorizontalTabs = [ + const navItems: TabItem[] = [ { - title: t('general'), + title: t("general"), href: `/admin/idp/${params.idpId}/general` }, { - title: t('orgPolicies'), + title: t("orgPolicies"), href: `/admin/idp/${params.idpId}/policies` } ]; @@ -42,8 +42,8 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { return ( <>
From 02cd2cfb1705e8c0ac9e6f32cf40abe6daf601a7 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 13 Nov 2025 02:18:52 +0100 Subject: [PATCH 009/153] =?UTF-8?q?=E2=9C=A8=20save=20and=20update=20brand?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 5 + .../loginPage/deleteLoginPageBranding.ts | 2 +- .../routers/loginPage/getLoginPageBranding.ts | 2 +- .../loginPage/upsertLoginPageBranding.ts | 2 +- .../settings/general/auth-page/page.tsx | 14 +- src/components/AuthPageBrandingForm.tsx | 180 +++++++++++++++--- 6 files changed, 166 insertions(+), 39 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 7b395d0a..2531350e 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1737,6 +1737,11 @@ "authPageDomain": "Auth Page Domain", "authPageBranding": "Branding", "authPageBrandingDescription": "Configure the branding for the auth page for your organization", + "authPageBrandingUpdated": "Auth page Branding updated successfully", + "authPageBrandingRemoved": "Auth page Branding removed successfully", + "authPageBrandingRemoveTitle": "Remove Auth Page Branding", + "authPageBrandingQuestionRemove": "Are you sure you want to remove the branding for Auth Pages ?", + "authPageBrandingDeleteConfirm": "Confirm Delete Branding", "brandingLogoURL": "Logo URL", "brandingLogoWidth": "Width (px)", "brandingLogoHeight": "Height (px)", diff --git a/server/private/routers/loginPage/deleteLoginPageBranding.ts b/server/private/routers/loginPage/deleteLoginPageBranding.ts index 03bdf805..1fb243b0 100644 --- a/server/private/routers/loginPage/deleteLoginPageBranding.ts +++ b/server/private/routers/loginPage/deleteLoginPageBranding.ts @@ -102,7 +102,7 @@ export async function deleteLoginPageBranding( success: true, error: false, message: "Login page branding deleted successfully", - status: HttpCode.CREATED + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/private/routers/loginPage/getLoginPageBranding.ts b/server/private/routers/loginPage/getLoginPageBranding.ts index e0670545..262e9ce8 100644 --- a/server/private/routers/loginPage/getLoginPageBranding.ts +++ b/server/private/routers/loginPage/getLoginPageBranding.ts @@ -92,7 +92,7 @@ export async function getLoginPageBranding( success: true, error: false, message: "Login page branding retrieved successfully", - status: HttpCode.CREATED + status: HttpCode.OK }); } catch (error) { logger.error(error); diff --git a/server/private/routers/loginPage/upsertLoginPageBranding.ts b/server/private/routers/loginPage/upsertLoginPageBranding.ts index 51aa7392..e553d14d 100644 --- a/server/private/routers/loginPage/upsertLoginPageBranding.ts +++ b/server/private/routers/loginPage/upsertLoginPageBranding.ts @@ -143,7 +143,7 @@ export async function upsertLoginPageBranding( message: existingLoginPageBranding ? "Login page branding updated successfully" : "Login page branding created successfully", - status: HttpCode.CREATED + status: existingLoginPageBranding ? HttpCode.OK : HttpCode.CREATED }); } catch (error) { logger.error(error); diff --git a/src/app/[orgId]/settings/general/auth-page/page.tsx b/src/app/[orgId]/settings/general/auth-page/page.tsx index a0c883f0..0030afe1 100644 --- a/src/app/[orgId]/settings/general/auth-page/page.tsx +++ b/src/app/[orgId]/settings/general/auth-page/page.tsx @@ -1,7 +1,8 @@ import AuthPageBrandingForm from "@app/components/AuthPageBrandingForm"; import AuthPageSettings from "@app/components/private/AuthPageSettings"; import { SettingsContainer } from "@app/components/Settings"; -import { priv } from "@app/lib/api"; +import { internal, priv } from "@app/lib/api"; +import { authCookieHeader } from "@app/lib/api/cookies"; import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; import { pullEnv } from "@app/lib/pullEnv"; import { build } from "@server/build"; @@ -37,8 +38,9 @@ export default async function AuthPage(props: AuthPageProps) { let loginPage: GetLoginPageResponse | null = null; try { - const res = await priv.get>( - `/org/${orgId}/login-page` + const res = await internal.get>( + `/org/${orgId}/login-page`, + await authCookieHeader() ); if (res.status === 200) { loginPage = res.data.data; @@ -47,9 +49,9 @@ export default async function AuthPage(props: AuthPageProps) { let loginPageBranding: GetLoginPageBrandingResponse | null = null; try { - const res = await priv.get>( - `/org/${orgId}/login-page-branding` - ); + const res = await internal.get< + AxiosResponse + >(`/org/${orgId}/login-page-branding`, await authCookieHeader()); if (res.status === 200) { loginPageBranding = res.data.data; } diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx index 14474f12..39cac35a 100644 --- a/src/components/AuthPageBrandingForm.tsx +++ b/src/components/AuthPageBrandingForm.tsx @@ -1,7 +1,7 @@ "use client"; import { zodResolver } from "@hookform/resolvers/zod"; -import * as React from "react"; +import { useActionState, useState } from "react"; import { useForm } from "react-hook-form"; import z from "zod"; import { @@ -22,18 +22,25 @@ import { SettingsSectionTitle } from "./Settings"; import { useTranslations } from "next-intl"; -import AuthPageSettings, { - AuthPageSettingsRef -} from "./private/AuthPageSettings"; -import type { - GetLoginPageBrandingResponse, - GetLoginPageResponse -} from "@server/routers/loginPage/types"; + +import type { GetLoginPageBrandingResponse } from "@server/routers/loginPage/types"; import { Input } from "./ui/input"; -import { XIcon } from "lucide-react"; +import { Trash2, XIcon } from "lucide-react"; import { Button } from "./ui/button"; import { Separator } from "./ui/separator"; -import { build } from "@server/build"; +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 { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "./Credenza"; export type AuthPageCustomizationProps = { orgId: string; @@ -74,7 +81,21 @@ export default function AuthPageBrandingForm({ orgId, branding }: AuthPageCustomizationProps) { - const [, formAction, isSubmitting] = React.useActionState(onSubmit, null); + const env = useEnvContext(); + const api = createApiClient(env); + + const router = useRouter(); + + const [, updateFormAction, isUpdatingBranding] = useActionState( + updateBranding, + null + ); + const [, deleteFormAction, isDeletingBranding] = useActionState( + deleteBranding, + null + ); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const t = useTranslations(); const form = useForm({ @@ -94,14 +115,70 @@ export default function AuthPageBrandingForm({ } }); - async function onSubmit() { - console.log({ - dirty: form.formState.isDirty - }); + async function updateBranding() { const isValid = await form.trigger(); + const brandingData = form.getValues(); if (!isValid) return; - // ... + try { + // Update or existing auth page domain + const updateRes = await api.put( + `/org/${orgId}/login-page-branding`, + { + ...brandingData + } + ); + + if (updateRes.status === 200 || updateRes.status === 201) { + // update the data from the API + router.refresh(); + toast({ + variant: "default", + title: t("success"), + description: t("authPageBrandingUpdated") + }); + } + } catch (error) { + toast({ + variant: "destructive", + title: t("authPageErrorUpdate"), + description: formatAxiosError( + error, + t("authPageErrorUpdateMessage") + ) + }); + } + } + + async function deleteBranding() { + try { + // Update or existing auth page domain + const updateRes = await api.delete( + `/org/${orgId}/login-page-branding` + ); + + if (updateRes.status === 200) { + // update the data from the API + router.refresh(); + form.reset(); + setIsDeleteModalOpen(false); + + toast({ + variant: "default", + title: t("success"), + description: t("authPageBrandingRemoved") + }); + } + } catch (error) { + toast({ + variant: "destructive", + title: t("authPageErrorUpdate"), + description: formatAxiosError( + error, + t("authPageErrorUpdateMessage") + ) + }); + } } return ( @@ -120,7 +197,7 @@ export default function AuthPageBrandingForm({
@@ -293,22 +370,65 @@ export default function AuthPageBrandingForm({ -
- {/* {branding && ( - - )} */} + + + + + {t("authPageBrandingRemoveTitle")} + + + +

{t("authPageBrandingQuestionRemove")}

+
+ {t("cannotbeUndone")} +
+ +
+ + + + + + +
+
+ +
+ {branding && ( + + )} From 228481444f4ced3f61ab4f29cee9a77dd08c279a Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 13 Nov 2025 02:19:25 +0100 Subject: [PATCH 010/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20do=20not=20manuall?= =?UTF-8?q?y=20track=20the=20loading=20state=20in=20`ConfirmDeleteDialog`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ConfirmDeleteDialog.tsx | 44 ++++---------------------- 1 file changed, 7 insertions(+), 37 deletions(-) diff --git a/src/components/ConfirmDeleteDialog.tsx b/src/components/ConfirmDeleteDialog.tsx index e2bc271f..0ae36bf7 100644 --- a/src/components/ConfirmDeleteDialog.tsx +++ b/src/components/ConfirmDeleteDialog.tsx @@ -6,43 +6,22 @@ import { FormControl, FormField, FormItem, - FormLabel, FormMessage } from "@app/components/ui/form"; import { Input } from "@app/components/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@app/components/ui/select"; -import { useToast } from "@app/hooks/useToast"; import { zodResolver } from "@hookform/resolvers/zod"; -import { - InviteUserBody, - InviteUserResponse, - ListUsersResponse -} from "@server/routers/user"; -import { AxiosResponse } from "axios"; -import React, { useState } from "react"; +import React, { useActionState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import CopyTextBox from "@app/components/CopyTextBox"; import { Credenza, CredenzaBody, CredenzaClose, CredenzaContent, - CredenzaDescription, CredenzaFooter, CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; -import { useOrgContext } from "@app/hooks/useOrgContext"; -import { Description } from "@radix-ui/react-toast"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; import { useTranslations } from "next-intl"; import CopyToClipboard from "./CopyToClipboard"; @@ -57,7 +36,7 @@ type InviteUserFormProps = { warningText?: string; }; -export default function InviteUserForm({ +export default function ConfirmDeleteDialog({ open, setOpen, string, @@ -67,9 +46,7 @@ export default function InviteUserForm({ dialog, warningText }: InviteUserFormProps) { - const [loading, setLoading] = useState(false); - - const api = createApiClient(useEnvContext()); + const [, formAction, loading] = useActionState(onSubmit, null); const t = useTranslations(); @@ -86,21 +63,14 @@ export default function InviteUserForm({ } }); - function reset() { - form.reset(); - } - - async function onSubmit(values: z.infer) { - setLoading(true); + async function onSubmit() { try { await onConfirm(); setOpen(false); - reset(); + form.reset(); } catch (error) { // Handle error if needed console.error("Confirmation failed:", error); - } finally { - setLoading(false); } } @@ -110,7 +80,7 @@ export default function InviteUserForm({ open={open} onOpenChange={(val) => { setOpen(val); - reset(); + form.reset(); }} > @@ -136,7 +106,7 @@ export default function InviteUserForm({
From 4beed9d46423c15521e2123f366214fbd3a741aa Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 13 Nov 2025 03:24:47 +0100 Subject: [PATCH 011/153] =?UTF-8?q?=E2=9C=A8=20apply=20auth=20branding=20t?= =?UTF-8?q?o=20resource=20auth=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loginPage/upsertLoginPageBranding.ts | 4 +- src/app/auth/resource/[resourceGuid]/page.tsx | 23 +++++- src/components/AuthPageBrandingForm.tsx | 10 +-- src/components/BrandingLogo.tsx | 12 +-- src/components/ResourceAuthPortal.tsx | 75 ++++++++++++++++--- 5 files changed, 99 insertions(+), 25 deletions(-) diff --git a/server/private/routers/loginPage/upsertLoginPageBranding.ts b/server/private/routers/loginPage/upsertLoginPageBranding.ts index e553d14d..fc212538 100644 --- a/server/private/routers/loginPage/upsertLoginPageBranding.ts +++ b/server/private/routers/loginPage/upsertLoginPageBranding.ts @@ -38,8 +38,8 @@ const paramsSchema = z const bodySchema = z .object({ logoUrl: z.string().url(), - logoWidth: z.number().min(1), - logoHeight: z.number().min(1), + logoWidth: z.coerce.number().min(1), + logoHeight: z.coerce.number().min(1), title: z.string(), subtitle: z.string().optional(), resourceTitle: z.string(), diff --git a/src/app/auth/resource/[resourceGuid]/page.tsx b/src/app/auth/resource/[resourceGuid]/page.tsx index d51f2210..eeb01eaa 100644 --- a/src/app/auth/resource/[resourceGuid]/page.tsx +++ b/src/app/auth/resource/[resourceGuid]/page.tsx @@ -19,7 +19,10 @@ import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; import AutoLoginHandler from "@app/components/AutoLoginHandler"; import { build } from "@server/build"; import { headers } from "next/headers"; -import { GetLoginPageResponse } from "@server/routers/loginPage/types"; +import { + GetLoginPageBrandingResponse, + GetLoginPageResponse +} from "@server/routers/loginPage/types"; import { GetOrgTierResponse } from "@server/routers/billing/types"; import { TierId } from "@server/lib/billing/tiers"; import { CheckOrgUserAccessResponse } from "@server/routers/org"; @@ -261,6 +264,23 @@ export default async function ResourceAuthPage(props: { } } + let loginPageBranding: Omit< + GetLoginPageBrandingResponse, + "loginPageBrandingId" + > | null = null; + try { + const res = await internal.get< + AxiosResponse + >( + `/org/${authInfo.orgId}/login-page-branding`, + await authCookieHeader() + ); + if (res.status === 200) { + const { loginPageBrandingId, ...rest } = res.data.data; + loginPageBranding = rest; + } + } catch (error) {} + return ( <> {userIsUnauthorized && isSSOOnly ? ( @@ -283,6 +303,7 @@ export default async function ResourceAuthPage(props: { redirect={redirectUrl} idps={loginIdps} orgId={build === "saas" ? authInfo.orgId : undefined} + branding={loginPageBranding} />
)} diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx index 39cac35a..6d013816 100644 --- a/src/components/AuthPageBrandingForm.tsx +++ b/src/components/AuthPageBrandingForm.tsx @@ -69,8 +69,8 @@ const AuthPageFormSchema = z.object({ message: "Invalid logo URL, must be a valid image URL" } ), - logoWidth: z.number().min(1), - logoHeight: z.number().min(1), + logoWidth: z.coerce.number().min(1), + logoHeight: z.coerce.number().min(1), title: z.string(), subtitle: z.string().optional(), resourceTitle: z.string(), @@ -102,8 +102,8 @@ export default function AuthPageBrandingForm({ resolver: zodResolver(AuthPageFormSchema), defaultValues: { logoUrl: branding?.logoUrl ?? "", - logoWidth: branding?.logoWidth ?? 500, - logoHeight: branding?.logoHeight ?? 500, + logoWidth: branding?.logoWidth ?? 100, + logoHeight: branding?.logoHeight ?? 100, title: branding?.title ?? `Log in to {{orgName}}`, subtitle: branding?.subtitle ?? `Log in to {{orgName}}`, resourceTitle: @@ -240,7 +240,7 @@ export default function AuthPageBrandingForm({ ( diff --git a/src/components/BrandingLogo.tsx b/src/components/BrandingLogo.tsx index 540b8e0e..86a49496 100644 --- a/src/components/BrandingLogo.tsx +++ b/src/components/BrandingLogo.tsx @@ -7,6 +7,7 @@ import Image from "next/image"; import { useEffect, useState } from "react"; type BrandingLogoProps = { + logoPath?: string; width: number; height: number; }; @@ -38,16 +39,17 @@ export default function BrandingLogo(props: BrandingLogoProps) { if (isUnlocked() && env.branding.logo?.darkPath) { return env.branding.logo.darkPath; } - return "/logo/word_mark_white.png"; + return "/logo/word_mark_white.png"; } - const path = getPath(); - setPath(path); - }, [theme, env]); + setPath(props.logoPath ?? getPath()); + }, [theme, env, props.logoPath]); + + const Component = props.logoPath ? "img" : Image; return ( path && ( - Logo getNumMethods()); const [passwordError, setPasswordError] = useState(null); const [pincodeError, setPincodeError] = useState(null); @@ -309,13 +318,37 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { } } - function getTitle() { + function replacePlaceholder( + stringWithPlaceholder: string, + data: Record + ) { + let newString = stringWithPlaceholder; + + const keys = Object.keys(data); + + for (const key of keys) { + newString = newString.replace( + new RegExp(`{{${key}}}`, "gm"), + data[key] + ); + } + + return newString; + } + + function getTitle(resourceName: string) { if ( isUnlocked() && build !== "oss" && - env.branding.resourceAuthPage?.titleText + (!!env.branding.resourceAuthPage?.titleText || + !!props.branding?.resourceTitle) ) { - return env.branding.resourceAuthPage.titleText; + if (props.branding?.resourceTitle) { + return replacePlaceholder(props.branding?.resourceTitle, { + resourceName + }); + } + return env.branding.resourceAuthPage?.titleText; } return t("authenticationRequired"); } @@ -324,10 +357,16 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { if ( isUnlocked() && build !== "oss" && - env.branding.resourceAuthPage?.subtitleText + (env.branding.resourceAuthPage?.subtitleText || + props.branding?.resourceSubtitle) ) { - return env.branding.resourceAuthPage.subtitleText - .split("{{resourceName}}") + if (props.branding?.resourceSubtitle) { + return replacePlaceholder(props.branding?.resourceSubtitle, { + resourceName + }); + } + return env.branding.resourceAuthPage?.subtitleText + ?.split("{{resourceName}}") .join(resourceName); } return numMethods > 1 @@ -335,8 +374,16 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { : t("authenticationRequest", { name: resourceName }); } - const logoWidth = isUnlocked() ? env.branding.logo?.authPage?.width || 100 : 100; - const logoHeight = isUnlocked() ? env.branding.logo?.authPage?.height || 100 : 100; + const logoWidth = isUnlocked() + ? (props.branding?.logoWidth ?? + env.branding.logo?.authPage?.width ?? + 100) + : 100; + const logoHeight = isUnlocked() + ? (props.branding?.logoHeight ?? + env.branding.logo?.authPage?.height ?? + 100) + : 100; return (
@@ -377,15 +424,19 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { {isUnlocked() && build !== "oss" && - env.branding?.resourceAuthPage?.showLogo && ( + (env.branding?.resourceAuthPage?.showLogo || + props.branding) && (
)} - {getTitle()} + + {getTitle(props.resource.name)} + {getSubtitle(props.resource.name)} From 955f927c59da4c29d6bac9f6b88793f752037247 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 14 Nov 2025 01:24:15 +0100 Subject: [PATCH 012/153] =?UTF-8?q?=F0=9F=9A=A7WIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../settings/general/auth-page/page.tsx | 16 +- src/app/auth/org/[orgId]/page.tsx | 188 ++++++++++++++++++ src/components/ResourceAuthPortal.tsx | 15 ++ src/hooks/usePaidStatus.ts | 18 ++ 4 files changed, 230 insertions(+), 7 deletions(-) create mode 100644 src/app/auth/org/[orgId]/page.tsx create mode 100644 src/hooks/usePaidStatus.ts diff --git a/src/app/[orgId]/settings/general/auth-page/page.tsx b/src/app/[orgId]/settings/general/auth-page/page.tsx index 0030afe1..139449bf 100644 --- a/src/app/[orgId]/settings/general/auth-page/page.tsx +++ b/src/app/[orgId]/settings/general/auth-page/page.tsx @@ -38,12 +38,14 @@ export default async function AuthPage(props: AuthPageProps) { let loginPage: GetLoginPageResponse | null = null; try { - const res = await internal.get>( - `/org/${orgId}/login-page`, - await authCookieHeader() - ); - if (res.status === 200) { - loginPage = res.data.data; + if (build === "saas") { + const res = await internal.get>( + `/org/${orgId}/login-page`, + await authCookieHeader() + ); + if (res.status === 200) { + loginPage = res.data.data; + } } } catch (error) {} @@ -59,7 +61,7 @@ export default async function AuthPage(props: AuthPageProps) { return ( - + {build === "saas" && } ); diff --git a/src/app/auth/org/[orgId]/page.tsx b/src/app/auth/org/[orgId]/page.tsx new file mode 100644 index 00000000..7de991ca --- /dev/null +++ b/src/app/auth/org/[orgId]/page.tsx @@ -0,0 +1,188 @@ +import { formatAxiosError, priv } from "@app/lib/api"; +import { AxiosResponse } from "axios"; +import { authCookieHeader } from "@app/lib/api/cookies"; +import { cache } from "react"; +import { verifySession } from "@app/lib/auth/verifySession"; +import { redirect } from "next/navigation"; +import { pullEnv } from "@app/lib/pullEnv"; +import { LoginFormIDP } from "@app/components/LoginForm"; +import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; +import { build } from "@server/build"; +import { headers } from "next/headers"; +import { LoadLoginPageResponse } from "@server/routers/loginPage/types"; +import IdpLoginButtons from "@app/components/private/IdpLoginButtons"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle +} from "@app/components/ui/card"; +import { Button } from "@app/components/ui/button"; +import Link from "next/link"; +import { getTranslations } from "next-intl/server"; +import { GetSessionTransferTokenRenponse } from "@server/routers/auth/types"; +import ValidateSessionTransferToken from "@app/components/private/ValidateSessionTransferToken"; +import { GetOrgTierResponse } from "@server/routers/billing/types"; +import { TierId } from "@server/lib/billing/tiers"; + +export const dynamic = "force-dynamic"; + +export default async function OrgAuthPage(props: { + params: Promise<{}>; + searchParams: Promise<{ token?: string }>; +}) { + const params = await props.params; + const searchParams = await props.searchParams; + + const env = pullEnv(); + + const authHeader = await authCookieHeader(); + + if (searchParams.token) { + return ; + } + + const getUser = cache(verifySession); + const user = await getUser({ skipCheckVerifyEmail: true }); + + const allHeaders = await headers(); + const host = allHeaders.get("host"); + + const t = await getTranslations(); + + const expectedHost = env.app.dashboardUrl.split("//")[1]; + + let redirectToUrl: string | undefined; + let loginPage: LoadLoginPageResponse | undefined; + if (host !== expectedHost) { + try { + const res = await priv.get>( + `/login-page?fullDomain=${host}` + ); + + if (res && res.status === 200) { + loginPage = res.data.data; + } + } catch (e) {} + + if (!loginPage) { + console.debug( + `No login page found for host ${host}, redirecting to dashboard` + ); + redirect(env.app.dashboardUrl); + } + + let subscriptionStatus: GetOrgTierResponse | null = null; + if (build === "saas") { + try { + const getSubscription = cache(() => + priv.get>( + `/org/${loginPage!.orgId}/billing/tier` + ) + ); + const subRes = await getSubscription(); + subscriptionStatus = subRes.data.data; + } catch {} + } + const subscribed = + build === "enterprise" + ? true + : subscriptionStatus?.tier === TierId.STANDARD; + + if (build === "saas" && !subscribed) { + console.log( + `Org ${loginPage.orgId} is not subscribed, redirecting to dashboard` + ); + redirect(env.app.dashboardUrl); + } + + if (user) { + let redirectToken: string | undefined; + try { + const res = await priv.post< + AxiosResponse + >(`/get-session-transfer-token`, {}, authHeader); + + if (res && res.status === 200) { + const newToken = res.data.data.token; + redirectToken = newToken; + } + } catch (e) { + console.error( + formatAxiosError(e, "Failed to get transfer token") + ); + } + + if (redirectToken) { + redirectToUrl = `${env.app.dashboardUrl}/auth/org?token=${redirectToken}`; + redirect(redirectToUrl); + } + } + } else { + console.log(`Host ${host} is the same`); + redirect(env.app.dashboardUrl); + } + + let loginIdps: LoginFormIDP[] = []; + if (build === "saas") { + const idpsRes = await cache( + async () => + await priv.get>( + `/org/${loginPage!.orgId}/idp` + ) + )(); + loginIdps = idpsRes.data.data.idps.map((idp) => ({ + idpId: idp.idpId, + name: idp.name, + variant: idp.variant + })) as LoginFormIDP[]; + } + + return ( +
+
+ + {t("poweredBy")}{" "} + + {env.branding.appName || "Pangolin"} + + +
+ + + {t("orgAuthSignInTitle")} + + {loginIdps.length > 0 + ? t("orgAuthChooseIdpDescription") + : ""} + + + + {loginIdps.length > 0 ? ( + + ) : ( +
+

+ {t("orgAuthNoIdpConfigured")} +

+ + + +
+ )} +
+
+
+ ); +} diff --git a/src/components/ResourceAuthPortal.tsx b/src/components/ResourceAuthPortal.tsx index 81ddf58e..e9e7b731 100644 --- a/src/components/ResourceAuthPortal.tsx +++ b/src/components/ResourceAuthPortal.tsx @@ -49,6 +49,8 @@ import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext" import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; const pinSchema = z.object({ pin: z @@ -99,6 +101,19 @@ type ResourceAuthPortalProps = { } | null; }; +/** + * TODO: remove +- Auth page domain => only in SaaS +- Branding => saas & enterprise for a paid user ? +- ... +- resource auth page: `/auth/resource/[guid]` || (auth page domain/...) +- org auth page: `/auth/org/[orgId]` + => only in SaaS + => branding org title/subtitle only in SaaS + => unauthenticated + + */ + export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { const router = useRouter(); const t = useTranslations(); diff --git a/src/hooks/usePaidStatus.ts b/src/hooks/usePaidStatus.ts new file mode 100644 index 00000000..e5754f07 --- /dev/null +++ b/src/hooks/usePaidStatus.ts @@ -0,0 +1,18 @@ +import { build } from "@server/build"; +import { useLicenseStatusContext } from "./useLicenseStatusContext"; +import { useSubscriptionStatusContext } from "./useSubscriptionStatusContext"; + +export function usePaidStatus() { + const { isUnlocked } = useLicenseStatusContext(); + const subscription = useSubscriptionStatusContext(); + + // Check if features are disabled due to licensing/subscription + const isEnterpriseLicensed = build === "enterprise" && isUnlocked(); + const isSaasSubscribed = build === "saas" && subscription?.isSubscribed(); + + return { + isEnterpriseLicensed, + isSaasSubscribed, + isPaidUser: isEnterpriseLicensed || isSaasSubscribed + }; +} From b505cc60b056f5872f36f59da43bbf06689a49b3 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 01:06:09 +0100 Subject: [PATCH 013/153] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20Add=20`primaryC?= =?UTF-8?q?olor`=20to=20login=20page=20branding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/privateSchema.ts | 3 ++- server/db/sqlite/schema/privateSchema.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index f9911095..7707f3fd 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -209,8 +209,9 @@ export const loginPageBranding = pgTable("loginPageBranding", { logoUrl: text("logoUrl").notNull(), logoWidth: integer("logoWidth").notNull(), logoHeight: integer("logoHeight").notNull(), - title: text("title").notNull(), + title: text("title"), subtitle: text("subtitle"), + primaryColor: text("primaryColor"), resourceTitle: text("resourceTitle").notNull(), resourceSubtitle: text("resourceSubtitle") }); diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index e74964c2..e296ba4d 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -210,8 +210,9 @@ export const loginPageBranding = sqliteTable("loginPageBranding", { logoUrl: text("logoUrl").notNull(), logoWidth: integer("logoWidth").notNull(), logoHeight: integer("logoHeight").notNull(), - title: text("title").notNull(), + title: text("title"), subtitle: text("subtitle"), + primaryColor: text("primaryColor"), resourceTitle: text("resourceTitle").notNull(), resourceSubtitle: text("resourceSubtitle") }); From b961271aa689979e7f81eaebbdca66f70ba6b837 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 01:06:22 +0100 Subject: [PATCH 014/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20some=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/auth/(private)/org/page.tsx | 12 +- src/app/auth/org/[orgId]/page.tsx | 188 ---------------------------- 2 files changed, 3 insertions(+), 197 deletions(-) delete mode 100644 src/app/auth/org/[orgId]/page.tsx diff --git a/src/app/auth/(private)/org/page.tsx b/src/app/auth/(private)/org/page.tsx index 1d8dea21..2ac24e5a 100644 --- a/src/app/auth/(private)/org/page.tsx +++ b/src/app/auth/(private)/org/page.tsx @@ -9,9 +9,7 @@ import { LoginFormIDP } from "@app/components/LoginForm"; import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; import { build } from "@server/build"; import { headers } from "next/headers"; -import { - LoadLoginPageResponse -} from "@server/routers/loginPage/types"; +import { LoadLoginPageResponse } from "@server/routers/loginPage/types"; import IdpLoginButtons from "@app/components/private/IdpLoginButtons"; import { Card, @@ -27,6 +25,7 @@ import { GetSessionTransferTokenRenponse } from "@server/routers/auth/types"; import ValidateSessionTransferToken from "@app/components/private/ValidateSessionTransferToken"; import { GetOrgTierResponse } from "@server/routers/billing/types"; import { TierId } from "@server/lib/billing/tiers"; +import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; export const dynamic = "force-dynamic"; @@ -78,12 +77,7 @@ export default async function OrgAuthPage(props: { let subscriptionStatus: GetOrgTierResponse | null = null; if (build === "saas") { try { - const getSubscription = cache(() => - priv.get>( - `/org/${loginPage!.orgId}/billing/tier` - ) - ); - const subRes = await getSubscription(); + const subRes = await getCachedSubscription(loginPage.orgId); subscriptionStatus = subRes.data.data; } catch {} } diff --git a/src/app/auth/org/[orgId]/page.tsx b/src/app/auth/org/[orgId]/page.tsx deleted file mode 100644 index 7de991ca..00000000 --- a/src/app/auth/org/[orgId]/page.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { formatAxiosError, priv } from "@app/lib/api"; -import { AxiosResponse } from "axios"; -import { authCookieHeader } from "@app/lib/api/cookies"; -import { cache } from "react"; -import { verifySession } from "@app/lib/auth/verifySession"; -import { redirect } from "next/navigation"; -import { pullEnv } from "@app/lib/pullEnv"; -import { LoginFormIDP } from "@app/components/LoginForm"; -import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; -import { build } from "@server/build"; -import { headers } from "next/headers"; -import { LoadLoginPageResponse } from "@server/routers/loginPage/types"; -import IdpLoginButtons from "@app/components/private/IdpLoginButtons"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle -} from "@app/components/ui/card"; -import { Button } from "@app/components/ui/button"; -import Link from "next/link"; -import { getTranslations } from "next-intl/server"; -import { GetSessionTransferTokenRenponse } from "@server/routers/auth/types"; -import ValidateSessionTransferToken from "@app/components/private/ValidateSessionTransferToken"; -import { GetOrgTierResponse } from "@server/routers/billing/types"; -import { TierId } from "@server/lib/billing/tiers"; - -export const dynamic = "force-dynamic"; - -export default async function OrgAuthPage(props: { - params: Promise<{}>; - searchParams: Promise<{ token?: string }>; -}) { - const params = await props.params; - const searchParams = await props.searchParams; - - const env = pullEnv(); - - const authHeader = await authCookieHeader(); - - if (searchParams.token) { - return ; - } - - const getUser = cache(verifySession); - const user = await getUser({ skipCheckVerifyEmail: true }); - - const allHeaders = await headers(); - const host = allHeaders.get("host"); - - const t = await getTranslations(); - - const expectedHost = env.app.dashboardUrl.split("//")[1]; - - let redirectToUrl: string | undefined; - let loginPage: LoadLoginPageResponse | undefined; - if (host !== expectedHost) { - try { - const res = await priv.get>( - `/login-page?fullDomain=${host}` - ); - - if (res && res.status === 200) { - loginPage = res.data.data; - } - } catch (e) {} - - if (!loginPage) { - console.debug( - `No login page found for host ${host}, redirecting to dashboard` - ); - redirect(env.app.dashboardUrl); - } - - let subscriptionStatus: GetOrgTierResponse | null = null; - if (build === "saas") { - try { - const getSubscription = cache(() => - priv.get>( - `/org/${loginPage!.orgId}/billing/tier` - ) - ); - const subRes = await getSubscription(); - subscriptionStatus = subRes.data.data; - } catch {} - } - const subscribed = - build === "enterprise" - ? true - : subscriptionStatus?.tier === TierId.STANDARD; - - if (build === "saas" && !subscribed) { - console.log( - `Org ${loginPage.orgId} is not subscribed, redirecting to dashboard` - ); - redirect(env.app.dashboardUrl); - } - - if (user) { - let redirectToken: string | undefined; - try { - const res = await priv.post< - AxiosResponse - >(`/get-session-transfer-token`, {}, authHeader); - - if (res && res.status === 200) { - const newToken = res.data.data.token; - redirectToken = newToken; - } - } catch (e) { - console.error( - formatAxiosError(e, "Failed to get transfer token") - ); - } - - if (redirectToken) { - redirectToUrl = `${env.app.dashboardUrl}/auth/org?token=${redirectToken}`; - redirect(redirectToUrl); - } - } - } else { - console.log(`Host ${host} is the same`); - redirect(env.app.dashboardUrl); - } - - let loginIdps: LoginFormIDP[] = []; - if (build === "saas") { - const idpsRes = await cache( - async () => - await priv.get>( - `/org/${loginPage!.orgId}/idp` - ) - )(); - loginIdps = idpsRes.data.data.idps.map((idp) => ({ - idpId: idp.idpId, - name: idp.name, - variant: idp.variant - })) as LoginFormIDP[]; - } - - return ( -
-
- - {t("poweredBy")}{" "} - - {env.branding.appName || "Pangolin"} - - -
- - - {t("orgAuthSignInTitle")} - - {loginIdps.length > 0 - ? t("orgAuthChooseIdpDescription") - : ""} - - - - {loginIdps.length > 0 ? ( - - ) : ( -
-

- {t("orgAuthNoIdpConfigured")} -

- - - -
- )} -
-
-
- ); -} From 0d84b7af6e1249dd3f377939bec6f73de250cbac Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 01:07:00 +0100 Subject: [PATCH 015/153] =?UTF-8?q?=E2=99=BB=EF=B8=8Fshow=20org=20page=20b?= =?UTF-8?q?randing=20section=20only=20in=20saas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AuthPageBrandingForm.tsx | 115 +++++++++++++----------- 1 file changed, 63 insertions(+), 52 deletions(-) diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx index 6d013816..76890d2b 100644 --- a/src/components/AuthPageBrandingForm.tsx +++ b/src/components/AuthPageBrandingForm.tsx @@ -41,6 +41,8 @@ import { CredenzaHeader, CredenzaTitle } from "./Credenza"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import { build } from "@server/build"; export type AuthPageCustomizationProps = { orgId: string; @@ -71,7 +73,7 @@ const AuthPageFormSchema = z.object({ ), logoWidth: z.coerce.number().min(1), logoHeight: z.coerce.number().min(1), - title: z.string(), + title: z.string().optional(), subtitle: z.string().optional(), resourceTitle: z.string(), resourceSubtitle: z.string().optional() @@ -83,6 +85,7 @@ export default function AuthPageBrandingForm({ }: AuthPageCustomizationProps) { const env = useEnvContext(); const api = createApiClient(env); + const { hasSaasSubscription } = usePaidStatus(); const router = useRouter(); @@ -258,58 +261,66 @@ export default function AuthPageBrandingForm({
- + {hasSaasSubscription && ( + <> + -
- ( - - - {t("brandingOrgTitle")} - - - {t( - "brandingOrgDescription", - { - orgName: - "{{orgName}}" - } - )} - - - - - - - )} - /> - ( - - - {t("brandingOrgSubtitle")} - - - {t( - "brandingOrgDescription", - { - orgName: - "{{orgName}}" - } - )} - - - - - - - )} - /> -
+
+ ( + + + {t( + "brandingOrgTitle" + )} + + + {t( + "brandingOrgDescription", + { + orgName: + "{{orgName}}" + } + )} + + + + + + + )} + /> + ( + + + {t( + "brandingOrgSubtitle" + )} + + + {t( + "brandingOrgDescription", + { + orgName: + "{{orgName}}" + } + )} + + + + + + + )} + /> +
+ + )} From 27e8250cd17342cdb50661d6f5e7c773bbadfdb7 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 01:07:07 +0100 Subject: [PATCH 016/153] =?UTF-8?q?=E2=99=BB=EF=B8=8Fsome=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ResourceAuthPortal.tsx | 15 ++------------- src/hooks/usePaidStatus.ts | 11 ++++++----- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/src/components/ResourceAuthPortal.tsx b/src/components/ResourceAuthPortal.tsx index e9e7b731..33e2292f 100644 --- a/src/components/ResourceAuthPortal.tsx +++ b/src/components/ResourceAuthPortal.tsx @@ -39,18 +39,15 @@ import { resourceWhitelistProxy, resourceAccessProxy } from "@app/actions/server"; -import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import Link from "next/link"; -import Image from "next/image"; import BrandingLogo from "@app/components/BrandingLogo"; import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; -import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; -import { usePaidStatus } from "@app/hooks/usePaidStatus"; +import type { GetLoginPageBrandingResponse } from "@server/routers/loginPage/types"; const pinSchema = z.object({ pin: z @@ -90,15 +87,7 @@ type ResourceAuthPortalProps = { redirect: string; idps?: LoginFormIDP[]; orgId?: string; - branding?: { - title: string; - logoUrl: string; - logoWidth: number; - logoHeight: number; - subtitle: string | null; - resourceTitle: string; - resourceSubtitle: string | null; - } | null; + branding?: Omit | null; }; /** diff --git a/src/hooks/usePaidStatus.ts b/src/hooks/usePaidStatus.ts index e5754f07..6b11a6fc 100644 --- a/src/hooks/usePaidStatus.ts +++ b/src/hooks/usePaidStatus.ts @@ -7,12 +7,13 @@ export function usePaidStatus() { const subscription = useSubscriptionStatusContext(); // Check if features are disabled due to licensing/subscription - const isEnterpriseLicensed = build === "enterprise" && isUnlocked(); - const isSaasSubscribed = build === "saas" && subscription?.isSubscribed(); + const hasEnterpriseLicense = build === "enterprise" && isUnlocked(); + const hasSaasSubscription = + build === "saas" && subscription?.isSubscribed(); return { - isEnterpriseLicensed, - isSaasSubscribed, - isPaidUser: isEnterpriseLicensed || isSaasSubscribed + hasEnterpriseLicense, + hasSaasSubscription, + isPaidUser: hasEnterpriseLicense || hasSaasSubscription }; } From e2c4a906c414aeff2e1454677baf2d44cee6a0c9 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 01:41:56 +0100 Subject: [PATCH 017/153] =?UTF-8?q?=E2=99=BB=EF=B8=8Frename=20`title`=20&?= =?UTF-8?q?=20`subtitle`=20to=20`orgTitle`=20and=20`orgSubtitle`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/db/pg/schema/privateSchema.ts | 6 +++--- server/db/sqlite/schema/privateSchema.ts | 6 +++--- src/components/AuthPageBrandingForm.tsx | 12 ++++++------ 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index 7707f3fd..1f30dbf5 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -209,11 +209,11 @@ export const loginPageBranding = pgTable("loginPageBranding", { logoUrl: text("logoUrl").notNull(), logoWidth: integer("logoWidth").notNull(), logoHeight: integer("logoHeight").notNull(), - title: text("title"), - subtitle: text("subtitle"), primaryColor: text("primaryColor"), resourceTitle: text("resourceTitle").notNull(), - resourceSubtitle: text("resourceSubtitle") + resourceSubtitle: text("resourceSubtitle"), + orgTitle: text("orgTitle"), + orgSubtitle: text("orgSubtitle") }); export const loginPageBrandingOrg = pgTable("loginPageBrandingOrg", { diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index e296ba4d..93056665 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -210,11 +210,11 @@ export const loginPageBranding = sqliteTable("loginPageBranding", { logoUrl: text("logoUrl").notNull(), logoWidth: integer("logoWidth").notNull(), logoHeight: integer("logoHeight").notNull(), - title: text("title"), - subtitle: text("subtitle"), primaryColor: text("primaryColor"), resourceTitle: text("resourceTitle").notNull(), - resourceSubtitle: text("resourceSubtitle") + resourceSubtitle: text("resourceSubtitle"), + orgTitle: text("orgTitle"), + orgSubtitle: text("orgSubtitle") }); export const loginPageBrandingOrg = sqliteTable("loginPageBrandingOrg", { diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx index 76890d2b..7460b067 100644 --- a/src/components/AuthPageBrandingForm.tsx +++ b/src/components/AuthPageBrandingForm.tsx @@ -73,8 +73,8 @@ const AuthPageFormSchema = z.object({ ), logoWidth: z.coerce.number().min(1), logoHeight: z.coerce.number().min(1), - title: z.string().optional(), - subtitle: z.string().optional(), + orgTitle: z.string().optional(), + orgSubtitle: z.string().optional(), resourceTitle: z.string(), resourceSubtitle: z.string().optional() }); @@ -107,8 +107,8 @@ export default function AuthPageBrandingForm({ logoUrl: branding?.logoUrl ?? "", logoWidth: branding?.logoWidth ?? 100, logoHeight: branding?.logoHeight ?? 100, - title: branding?.title ?? `Log in to {{orgName}}`, - subtitle: branding?.subtitle ?? `Log in to {{orgName}}`, + orgTitle: branding?.orgTitle ?? `Log in to {{orgName}}`, + orgSubtitle: branding?.orgSubtitle ?? `Log in to {{orgName}}`, resourceTitle: branding?.resourceTitle ?? `Authenticate to access {{resourceName}}`, @@ -268,7 +268,7 @@ export default function AuthPageBrandingForm({
( @@ -294,7 +294,7 @@ export default function AuthPageBrandingForm({ /> ( From 9776ef43eabc7c16d37b4729c3830b4d892c330f Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 01:42:20 +0100 Subject: [PATCH 018/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20only=20include=20o?= =?UTF-8?q?rg=20settings=20in=20saas=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loginPage/upsertLoginPageBranding.ts | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/server/private/routers/loginPage/upsertLoginPageBranding.ts b/server/private/routers/loginPage/upsertLoginPageBranding.ts index fc212538..1f5908dc 100644 --- a/server/private/routers/loginPage/upsertLoginPageBranding.ts +++ b/server/private/routers/loginPage/upsertLoginPageBranding.ts @@ -24,7 +24,7 @@ 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 { eq, InferInsertModel } from "drizzle-orm"; import { getOrgTierData } from "#private/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import { build } from "@server/build"; @@ -40,10 +40,10 @@ const bodySchema = z logoUrl: z.string().url(), logoWidth: z.coerce.number().min(1), logoHeight: z.coerce.number().min(1), - title: z.string(), - subtitle: z.string().optional(), resourceTitle: z.string(), - resourceSubtitle: z.string().optional() + resourceSubtitle: z.string().optional(), + orgTitle: z.string().optional(), + orgSubtitle: z.string().optional() }) .strict(); @@ -65,8 +65,6 @@ export async function upsertLoginPageBranding( ); } - const updateData = parsedBody.data; - const parsedParams = paramsSchema.safeParse(req.params); if (!parsedParams.success) { return next( @@ -92,6 +90,16 @@ export async function upsertLoginPageBranding( } } + let updateData = parsedBody.data satisfies InferInsertModel< + typeof loginPageBranding + >; + + if (build !== "saas") { + // org branding settings are only considered in the saas build + const { orgTitle, orgSubtitle, ...rest } = updateData; + updateData = rest; + } + const [existingLoginPageBranding] = await db .select() .from(loginPageBranding) From d0034361798fe767727c743fbcffdd41eaa85408 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 01:43:58 +0100 Subject: [PATCH 019/153] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20generate=20build?= =?UTF-8?q?=20variable=20as=20fully=20typed=20to=20prevent=20typos=20(to?= =?UTF-8?q?=20check=20if=20it's=20ok)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 480da7e4..c679b6c8 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts", "db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts", "db:clear-migrations": "rm -rf server/migrations", - "set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts && cp tsconfig.oss.json tsconfig.json", - "set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts && cp tsconfig.saas.json tsconfig.json", - "set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json", + "set:oss": "echo 'export const build = \"oss\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json", + "set:saas": "echo 'export const build = \"saas\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.saas.json tsconfig.json", + "set:enterprise": "echo 'export const build = \"enterprise\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json", "set:sqlite": "echo 'export * from \"./sqlite\";' > server/db/index.ts", "set:pg": "echo 'export * from \"./pg\";' > server/db/index.ts", "next:build": "next build", @@ -79,7 +79,7 @@ "date-fns": "4.1.0", "drizzle-orm": "0.44.7", "eslint": "9.39.0", - "eslint-config-next": "16.0.1", + "eslint-config-next": "15.5.6", "express": "5.1.0", "express-rate-limit": "8.2.1", "glob": "11.0.3", @@ -133,10 +133,10 @@ "@faker-js/faker": "^10.1.0" }, "devDependencies": { - "@dotenvx/dotenvx": "1.51.1", + "@dotenvx/dotenvx": "1.51.0", "@esbuild-plugins/tsconfig-paths": "0.1.2", "@react-email/preview-server": "4.3.2", - "@tailwindcss/postcss": "^4.1.17", + "@tailwindcss/postcss": "^4.1.16", "@types/better-sqlite3": "7.6.12", "@types/cookie-parser": "1.4.10", "@types/cors": "2.8.19", @@ -157,7 +157,7 @@ "@types/ws": "8.18.1", "@types/yargs": "17.0.34", "drizzle-kit": "0.31.6", - "esbuild": "0.25.12", + "esbuild": "0.25.11", "esbuild-node-externals": "1.18.0", "postcss": "^8", "react-email": "4.3.2", @@ -165,7 +165,7 @@ "tsc-alias": "1.8.16", "tsx": "4.20.6", "typescript": "^5", - "typescript-eslint": "^8.46.3" + "typescript-eslint": "^8.46.2" }, "overrides": { "emblor": { From 8f152bdf9f789473d05f182831b97cd87c590bb3 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 02:38:46 +0100 Subject: [PATCH 020/153] =?UTF-8?q?=E2=9C=A8add=20primary=20color=20brandi?= =?UTF-8?q?ng=20to=20the=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + .../loginPage/upsertLoginPageBranding.ts | 6 ++- src/components/AuthPageBrandingForm.tsx | 43 ++++++++++++++++++- src/components/ResourceAuthPortal.tsx | 7 ++- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 2531350e..e43610f3 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1743,6 +1743,7 @@ "authPageBrandingQuestionRemove": "Are you sure you want to remove the branding for Auth Pages ?", "authPageBrandingDeleteConfirm": "Confirm Delete Branding", "brandingLogoURL": "Logo URL", + "brandingPrimaryColor": "Primary Color", "brandingLogoWidth": "Width (px)", "brandingLogoHeight": "Height (px)", "brandingOrgTitle": "Title for Organization Auth Page", diff --git a/server/private/routers/loginPage/upsertLoginPageBranding.ts b/server/private/routers/loginPage/upsertLoginPageBranding.ts index 1f5908dc..495c15fc 100644 --- a/server/private/routers/loginPage/upsertLoginPageBranding.ts +++ b/server/private/routers/loginPage/upsertLoginPageBranding.ts @@ -43,7 +43,11 @@ const bodySchema = z resourceTitle: z.string(), resourceSubtitle: z.string().optional(), orgTitle: z.string().optional(), - orgSubtitle: z.string().optional() + orgSubtitle: z.string().optional(), + primaryColor: z + .string() + .regex(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i) + .optional() }) .strict(); diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx index 7460b067..580215fc 100644 --- a/src/components/AuthPageBrandingForm.tsx +++ b/src/components/AuthPageBrandingForm.tsx @@ -76,7 +76,11 @@ const AuthPageFormSchema = z.object({ orgTitle: z.string().optional(), orgSubtitle: z.string().optional(), resourceTitle: z.string(), - resourceSubtitle: z.string().optional() + resourceSubtitle: z.string().optional(), + primaryColor: z + .string() + .regex(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i) + .optional() }); export default function AuthPageBrandingForm({ @@ -114,7 +118,8 @@ export default function AuthPageBrandingForm({ `Authenticate to access {{resourceName}}`, resourceSubtitle: branding?.resourceSubtitle ?? - `Choose your preferred authentication method for {{resourceName}}` + `Choose your preferred authentication method for {{resourceName}}`, + primaryColor: branding?.primaryColor ?? `#f36117` // default pangolin primary color } }); @@ -204,6 +209,40 @@ export default function AuthPageBrandingForm({ id="auth-page-branding-form" className="flex flex-col gap-8 items-stretch" > + ( + + + {t("brandingPrimaryColor")} + + +
+ + + + +
+ + +
+ )} + /> +
+
{!accessDenied ? (
{isUnlocked() && build === "enterprise" ? ( From 4842648e7b488e95223adf57cafae9ff8d440713 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 02:38:51 +0100 Subject: [PATCH 021/153] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/auth/resource/[resourceGuid]/page.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/app/auth/resource/[resourceGuid]/page.tsx b/src/app/auth/resource/[resourceGuid]/page.tsx index eeb01eaa..32b70298 100644 --- a/src/app/auth/resource/[resourceGuid]/page.tsx +++ b/src/app/auth/resource/[resourceGuid]/page.tsx @@ -57,8 +57,7 @@ export default async function ResourceAuthPage(props: { console.error(e); } - const getUser = cache(verifySession); - const user = await getUser({ skipCheckVerifyEmail: true }); + const user = await verifySession({ skipCheckVerifyEmail: true }); if (!authInfo) { return ( @@ -69,7 +68,7 @@ export default async function ResourceAuthPage(props: { } let subscriptionStatus: GetOrgTierResponse | null = null; - if (build == "saas") { + if (build === "saas") { try { const getSubscription = cache(() => priv.get>( @@ -235,9 +234,7 @@ export default async function ResourceAuthPage(props: { })) as LoginFormIDP[]; } } else { - const idpsRes = await cache( - async () => await priv.get>("/idp") - )(); + const idpsRes = await priv.get>("/idp"); loginIdps = idpsRes.data.data.idps.map((idp) => ({ idpId: idp.idpId, name: idp.name, From 854f638da36f9cee43118a710d78d4d88c5648af Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 04:03:21 +0100 Subject: [PATCH 022/153] =?UTF-8?q?=E2=9C=A8show=20toast=20message=20when?= =?UTF-8?q?=20updating=20auth=20page=20domain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 2 +- src/components/private/AuthPageSettings.tsx | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/messages/en-US.json b/messages/en-US.json index e43610f3..7c3844c9 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1833,7 +1833,7 @@ "securityPolicyChangeWarningText": "This will affect all users in the organization", "authPageErrorUpdateMessage": "An error occurred while updating the auth page settings", "authPageErrorUpdate": "Unable to update auth page", - "authPageUpdated": "Auth page updated successfully", + "authPageDomainUpdated": "Auth page Domain updated successfully", "healthCheckNotAvailable": "Local", "rewritePath": "Rewrite Path", "rewritePathDescription": "Optionally rewrite the path before forwarding to the target.", diff --git a/src/components/private/AuthPageSettings.tsx b/src/components/private/AuthPageSettings.tsx index b20a1876..2203fde2 100644 --- a/src/components/private/AuthPageSettings.tsx +++ b/src/components/private/AuthPageSettings.tsx @@ -272,6 +272,11 @@ function AuthPageSettings({ setHasUnsavedChanges(false); router.refresh(); onSaveSuccess?.(); + toast({ + variant: "default", + title: t("success"), + description: t("authPageDomainUpdated") + }); } catch (e) { toast({ variant: "destructive", From 5c851e82ff341c694b002caa984050cbc6c61564 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 04:03:42 +0100 Subject: [PATCH 023/153] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[orgId]/settings/general/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 5dd5ccb8..84f876d0 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -113,7 +113,7 @@ export default function GeneralPage() { const { user } = useUserContext(); const t = useTranslations(); const { env } = useEnvContext(); - const { licenseStatus, isUnlocked } = useLicenseStatusContext(); + const { isUnlocked } = useLicenseStatusContext(); const subscription = useSubscriptionStatusContext(); // Check if security features are disabled due to licensing/subscription From 790f7083e26fe68cb2b146df5275c9783969a886 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 04:04:10 +0100 Subject: [PATCH 024/153] =?UTF-8?q?=F0=9F=90=9B=20fix=20`cols`=20and=20som?= =?UTF-8?q?e=20other=20refactors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/DomainPicker.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 24f510dc..64fbd726 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -521,13 +521,13 @@ export default function DomainPicker2({
{selectedBaseDomain.type === "organization" ? null : ( - + )} {selectedBaseDomain.domain} {selectedBaseDomain.verified && ( - + )}
) : ( @@ -747,7 +747,11 @@ export default function DomainPicker2({ handleProvidedDomainSelect(option); } }} - className={`grid gap-2 grid-cols-1 sm:grid-cols-${cols}`} + style={{ + // @ts-expect-error CSS variable + "--cols": cols + }} + className="grid gap-2 grid-cols-1 sm:grid-cols-(--cols)" > {displayedProvidedOptions.map((option) => (
)} diff --git a/src/components/ResourceAuthPortal.tsx b/src/components/ResourceAuthPortal.tsx index 56acc967..66be584b 100644 --- a/src/components/ResourceAuthPortal.tsx +++ b/src/components/ResourceAuthPortal.tsx @@ -87,7 +87,14 @@ type ResourceAuthPortalProps = { redirect: string; idps?: LoginFormIDP[]; orgId?: string; - branding?: Omit | null; + branding?: { + logoUrl: string; + logoWidth: number; + logoHeight: number; + primaryColor: string | null; + resourceTitle: string; + resourceSubtitle: string | null; + }; }; /** @@ -342,8 +349,8 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { function getTitle(resourceName: string) { if ( - isUnlocked() && build !== "oss" && + isUnlocked() && (!!env.branding.resourceAuthPage?.titleText || !!props.branding?.resourceTitle) ) { From 87f23f582c423982a3c531cbcce63a6b6164f6fa Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 06:08:02 +0100 Subject: [PATCH 026/153] =?UTF-8?q?=E2=9C=A8apply=20branding=20to=20org=20?= =?UTF-8?q?auth=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loginPage/loadLoginPageBranding.ts | 89 +++---------------- server/routers/loginPage/types.ts | 1 + src/app/auth/(private)/org/page.tsx | 54 ++++++++--- src/components/ResourceAuthPortal.tsx | 21 +---- src/lib/replacePlaceholder.ts | 17 ++++ 5 files changed, 76 insertions(+), 106 deletions(-) create mode 100644 src/lib/replacePlaceholder.ts diff --git a/server/private/routers/loginPage/loadLoginPageBranding.ts b/server/private/routers/loginPage/loadLoginPageBranding.ts index 94632639..823f75a6 100644 --- a/server/private/routers/loginPage/loadLoginPageBranding.ts +++ b/server/private/routers/loginPage/loadLoginPageBranding.ts @@ -13,16 +13,8 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { - db, - idpOrg, - loginPage, - loginPageBranding, - loginPageBrandingOrg, - loginPageOrg, - resources -} from "@server/db"; -import { eq, and, type InferSelectModel } from "drizzle-orm"; +import { db, loginPageBranding, loginPageBrandingOrg, orgs } from "@server/db"; +import { eq, and } from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; @@ -31,37 +23,15 @@ import { fromError } from "zod-validation-error"; import type { LoadLoginPageBrandingResponse } from "@server/routers/loginPage/types"; const querySchema = z.object({ - resourceId: z.coerce.number().int().positive().optional(), - idpId: z.coerce.number().int().positive().optional(), - orgId: z.string().min(1).optional(), - fullDomain: z.string().min(1).optional() + orgId: z.string().min(1) }); -async function query(orgId?: string, fullDomain?: string) { - let orgLink: InferSelectModel | null = null; - if (orgId !== undefined) { - [orgLink] = await db - .select() - .from(loginPageBrandingOrg) - .where(eq(loginPageBrandingOrg.orgId, orgId)); - } else if (fullDomain) { - const [res] = await db - .select() - .from(loginPage) - .where(eq(loginPage.fullDomain, fullDomain)) - .innerJoin( - loginPageOrg, - eq(loginPage.loginPageId, loginPageOrg.loginPageId) - ) - .innerJoin( - loginPageBrandingOrg, - eq(loginPageBrandingOrg.orgId, loginPageOrg.orgId) - ) - .limit(1); - - orgLink = res.loginPageBrandingOrg; - } - +async function query(orgId: string) { + const [orgLink] = await db + .select() + .from(loginPageBrandingOrg) + .where(eq(loginPageBrandingOrg.orgId, orgId)) + .innerJoin(orgs, eq(loginPageBrandingOrg.orgId, orgs.orgId)); if (!orgLink) { return null; } @@ -73,14 +43,15 @@ async function query(orgId?: string, fullDomain?: string) { and( eq( loginPageBranding.loginPageBrandingId, - orgLink.loginPageBrandingId + orgLink.loginPageBrandingOrg.loginPageBrandingId ) ) ) .limit(1); return { ...res, - orgId: orgLink.orgId + orgId: orgLink.orgs.orgId, + orgName: orgLink.orgs.name }; } @@ -100,41 +71,9 @@ export async function loadLoginPageBranding( ); } - const { resourceId, idpId, fullDomain } = parsedQuery.data; + const { orgId } = parsedQuery.data; - let orgId: string | undefined = undefined; - if (resourceId) { - const [resource] = await db - .select() - .from(resources) - .where(eq(resources.resourceId, resourceId)) - .limit(1); - - if (!resource) { - return next( - createHttpError(HttpCode.NOT_FOUND, "Resource not found") - ); - } - - orgId = resource.orgId; - } else if (idpId) { - const [idpOrgLink] = await db - .select() - .from(idpOrg) - .where(eq(idpOrg.idpId, idpId)); - - if (!idpOrgLink) { - return next( - createHttpError(HttpCode.NOT_FOUND, "IdP not found") - ); - } - - orgId = idpOrgLink.orgId; - } else if (parsedQuery.data.orgId) { - orgId = parsedQuery.data.orgId; - } - - const branding = await query(orgId, fullDomain); + const branding = await query(orgId); if (!branding) { return next( diff --git a/server/routers/loginPage/types.ts b/server/routers/loginPage/types.ts index 6ef9ca81..8a253d07 100644 --- a/server/routers/loginPage/types.ts +++ b/server/routers/loginPage/types.ts @@ -12,6 +12,7 @@ export type LoadLoginPageResponse = LoginPage & { orgId: string }; export type LoadLoginPageBrandingResponse = LoginPageBranding & { orgId: string; + orgName: string; }; export type GetLoginPageBrandingResponse = LoginPageBranding; diff --git a/src/app/auth/(private)/org/page.tsx b/src/app/auth/(private)/org/page.tsx index 2ac24e5a..afd9d0c4 100644 --- a/src/app/auth/(private)/org/page.tsx +++ b/src/app/auth/(private)/org/page.tsx @@ -9,7 +9,10 @@ import { LoginFormIDP } from "@app/components/LoginForm"; import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types"; import { build } from "@server/build"; import { headers } from "next/headers"; -import { LoadLoginPageResponse } from "@server/routers/loginPage/types"; +import { + LoadLoginPageBrandingResponse, + LoadLoginPageResponse +} from "@server/routers/loginPage/types"; import IdpLoginButtons from "@app/components/private/IdpLoginButtons"; import { Card, @@ -26,6 +29,7 @@ import ValidateSessionTransferToken from "@app/components/private/ValidateSessio import { GetOrgTierResponse } from "@server/routers/billing/types"; import { TierId } from "@server/lib/billing/tiers"; import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; +import { replacePlaceholder } from "@app/lib/replacePlaceholder"; export const dynamic = "force-dynamic"; @@ -33,7 +37,6 @@ export default async function OrgAuthPage(props: { params: Promise<{}>; searchParams: Promise<{ token?: string }>; }) { - const params = await props.params; const searchParams = await props.searchParams; const env = pullEnv(); @@ -122,12 +125,10 @@ export default async function OrgAuthPage(props: { let loginIdps: LoginFormIDP[] = []; if (build === "saas") { - const idpsRes = await cache( - async () => - await priv.get>( - `/org/${loginPage!.orgId}/idp` - ) - )(); + const idpsRes = await priv.get>( + `/org/${loginPage.orgId}/idp` + ); + loginIdps = idpsRes.data.data.idps.map((idp) => ({ idpId: idp.idpId, name: idp.name, @@ -135,6 +136,16 @@ export default async function OrgAuthPage(props: { })) as LoginFormIDP[]; } + let branding: LoadLoginPageBrandingResponse | null = null; + try { + const res = await priv.get< + AxiosResponse + >(`/login-page-branding?orgId=${loginPage.orgId}`); + if (res.status === 200) { + branding = res.data.data; + } + } catch (error) {} + return (
@@ -152,11 +163,30 @@ export default async function OrgAuthPage(props: {
- {t("orgAuthSignInTitle")} + {branding?.logoUrl && ( +
+ +
+ )} + + {branding?.orgTitle + ? replacePlaceholder(branding.orgTitle, { + orgName: branding.orgName + }) + : t("orgAuthSignInTitle")} + - {loginIdps.length > 0 - ? t("orgAuthChooseIdpDescription") - : ""} + {branding?.orgSubtitle + ? replacePlaceholder(branding.orgSubtitle, { + orgName: branding.orgName + }) + : loginIdps.length > 0 + ? t("orgAuthChooseIdpDescription") + : ""}
diff --git a/src/components/ResourceAuthPortal.tsx b/src/components/ResourceAuthPortal.tsx index 66be584b..aad61e25 100644 --- a/src/components/ResourceAuthPortal.tsx +++ b/src/components/ResourceAuthPortal.tsx @@ -48,6 +48,7 @@ import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import type { GetLoginPageBrandingResponse } from "@server/routers/loginPage/types"; +import { replacePlaceholder } from "@app/lib/replacePlaceholder"; const pinSchema = z.object({ pin: z @@ -329,24 +330,6 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { } } - function replacePlaceholder( - stringWithPlaceholder: string, - data: Record - ) { - let newString = stringWithPlaceholder; - - const keys = Object.keys(data); - - for (const key of keys) { - newString = newString.replace( - new RegExp(`{{${key}}}`, "gm"), - data[key] - ); - } - - return newString; - } - function getTitle(resourceName: string) { if ( build !== "oss" && @@ -399,7 +382,7 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { return (
diff --git a/src/lib/replacePlaceholder.ts b/src/lib/replacePlaceholder.ts new file mode 100644 index 00000000..598056e3 --- /dev/null +++ b/src/lib/replacePlaceholder.ts @@ -0,0 +1,17 @@ +export function replacePlaceholder( + stringWithPlaceholder: string, + data: Record +) { + let newString = stringWithPlaceholder; + + const keys = Object.keys(data); + + for (const key of keys) { + newString = newString.replace( + new RegExp(`{{${key}}}`, "gm"), + data[key] + ); + } + + return newString; +} From 2ada05b286d2c264c18ad202280ad84315d9fc71 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 06:26:17 +0100 Subject: [PATCH 027/153] =?UTF-8?q?=E2=99=BB=EF=B8=8Fonly=20apply=20org=20?= =?UTF-8?q?branding=20in=20saas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/auth/(private)/org/page.tsx | 18 ++++++++++-------- src/components/ResourceAuthPortal.tsx | 13 ------------- 2 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/app/auth/(private)/org/page.tsx b/src/app/auth/(private)/org/page.tsx index afd9d0c4..71d31c01 100644 --- a/src/app/auth/(private)/org/page.tsx +++ b/src/app/auth/(private)/org/page.tsx @@ -137,14 +137,16 @@ export default async function OrgAuthPage(props: { } let branding: LoadLoginPageBrandingResponse | null = null; - try { - const res = await priv.get< - AxiosResponse - >(`/login-page-branding?orgId=${loginPage.orgId}`); - if (res.status === 200) { - branding = res.data.data; - } - } catch (error) {} + if (build === "saas") { + try { + const res = await priv.get< + AxiosResponse + >(`/login-page-branding?orgId=${loginPage.orgId}`); + if (res.status === 200) { + branding = res.data.data; + } + } catch (error) {} + } return (
diff --git a/src/components/ResourceAuthPortal.tsx b/src/components/ResourceAuthPortal.tsx index aad61e25..59edf679 100644 --- a/src/components/ResourceAuthPortal.tsx +++ b/src/components/ResourceAuthPortal.tsx @@ -98,19 +98,6 @@ type ResourceAuthPortalProps = { }; }; -/** - * TODO: remove -- Auth page domain => only in SaaS -- Branding => saas & enterprise for a paid user ? -- ... -- resource auth page: `/auth/resource/[guid]` || (auth page domain/...) -- org auth page: `/auth/org/[orgId]` - => only in SaaS - => branding org title/subtitle only in SaaS - => unauthenticated - - */ - export default function ResourceAuthPortal(props: ResourceAuthPortalProps) { const router = useRouter(); const t = useTranslations(); From 196fbbe334c7c47e612ae448b23173dca3f4afa6 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 06:32:45 +0100 Subject: [PATCH 028/153] =?UTF-8?q?=F0=9F=93=A6update=20lockfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 345 ++++++++++++++++++++++++---------------------- 1 file changed, 180 insertions(+), 165 deletions(-) diff --git a/package-lock.json b/package-lock.json index d190bbb0..d030337d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "date-fns": "4.1.0", "drizzle-orm": "0.44.7", "eslint": "9.39.0", - "eslint-config-next": "16.0.1", + "eslint-config-next": "15.5.6", "express": "5.1.0", "express-rate-limit": "8.2.1", "glob": "11.0.3", @@ -112,11 +112,11 @@ "zod-validation-error": "3.5.2" }, "devDependencies": { - "@dotenvx/dotenvx": "1.51.1", + "@dotenvx/dotenvx": "1.51.0", "@esbuild-plugins/tsconfig-paths": "0.1.2", "@react-email/preview-server": "4.3.2", + "@tailwindcss/postcss": "^4.1.16", "@tanstack/react-query-devtools": "^5.90.2", - "@tailwindcss/postcss": "^4.1.17", "@types/better-sqlite3": "7.6.12", "@types/cookie-parser": "1.4.10", "@types/cors": "2.8.19", @@ -137,7 +137,7 @@ "@types/ws": "8.18.1", "@types/yargs": "17.0.34", "drizzle-kit": "0.31.6", - "esbuild": "0.25.12", + "esbuild": "0.25.11", "esbuild-node-externals": "1.18.0", "postcss": "^8", "react-email": "4.3.2", @@ -145,7 +145,7 @@ "tsc-alias": "1.8.16", "tsx": "4.20.6", "typescript": "^5", - "typescript-eslint": "^8.46.3" + "typescript-eslint": "^8.46.2" } }, "node_modules/@alloc/quick-lru": { @@ -165,6 +165,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1620,6 +1621,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -1634,6 +1636,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1643,6 +1646,7 @@ "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -1673,6 +1677,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1682,6 +1687,7 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.3", @@ -1698,6 +1704,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.4" @@ -1713,6 +1720,7 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.27.2", @@ -1729,6 +1737,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1738,6 +1747,7 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1747,6 +1757,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -1760,6 +1771,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.4" @@ -1775,6 +1787,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -1793,6 +1806,7 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", @@ -1810,6 +1824,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.4" @@ -1825,6 +1840,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -1843,6 +1859,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1852,6 +1869,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1861,6 +1879,7 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1870,6 +1889,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", @@ -1883,6 +1903,7 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.0" @@ -1898,6 +1919,7 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -1912,6 +1934,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.4" @@ -1927,6 +1950,7 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", + "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -1945,6 +1969,7 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -1981,9 +2006,9 @@ "license": "MIT" }, "node_modules/@dotenvx/dotenvx": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.1.tgz", - "integrity": "sha512-fqcQxcxC4LOaUlW8IkyWw8x0yirlLUkbxohz9OnWvVWjf73J5yyw7jxWnkOJaUKXZotcGEScDox9MU6rSkcDgg==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.0.tgz", + "integrity": "sha512-CbMGzyOYSyFF7d4uaeYwO9gpSBzLTnMmSmTVpCZjvpJFV69qYbjYPpzNnCz1mb2wIvEhjWjRwQWuBzTO0jITww==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2510,9 +2535,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", "cpu": [ "ppc64" ], @@ -2527,9 +2552,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", "cpu": [ "arm" ], @@ -2544,9 +2569,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", "cpu": [ "arm64" ], @@ -2561,9 +2586,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", "cpu": [ "x64" ], @@ -2578,9 +2603,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", "cpu": [ "arm64" ], @@ -2595,9 +2620,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", "cpu": [ "x64" ], @@ -2612,9 +2637,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", "cpu": [ "arm64" ], @@ -2629,9 +2654,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", "cpu": [ "x64" ], @@ -2646,9 +2671,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", "cpu": [ "arm" ], @@ -2663,9 +2688,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", "cpu": [ "arm64" ], @@ -2680,9 +2705,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", "cpu": [ "ia32" ], @@ -2697,9 +2722,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", "cpu": [ "loong64" ], @@ -2714,9 +2739,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", "cpu": [ "mips64el" ], @@ -2731,9 +2756,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", "cpu": [ "ppc64" ], @@ -2748,9 +2773,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", "cpu": [ "riscv64" ], @@ -2765,9 +2790,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", "cpu": [ "s390x" ], @@ -2782,9 +2807,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", "cpu": [ "x64" ], @@ -2799,9 +2824,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", "cpu": [ "arm64" ], @@ -2816,9 +2841,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", "cpu": [ "x64" ], @@ -2833,9 +2858,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", "cpu": [ "arm64" ], @@ -2850,9 +2875,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", "cpu": [ "x64" ], @@ -2867,9 +2892,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", "cpu": [ "arm64" ], @@ -2884,9 +2909,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", "cpu": [ "x64" ], @@ -2901,9 +2926,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", "cpu": [ "arm64" ], @@ -2918,9 +2943,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", "cpu": [ "ia32" ], @@ -2935,9 +2960,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", "cpu": [ "x64" ], @@ -3810,6 +3835,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -3831,6 +3857,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -3851,12 +3878,14 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3931,9 +3960,9 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.1.tgz", - "integrity": "sha512-g4Cqmv/gyFEXNeVB2HkqDlYKfy+YrlM2k8AVIO/YQVEPfhVruH1VA99uT1zELLnPLIeOnx8IZ6Ddso0asfTIdw==", + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.6.tgz", + "integrity": "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==", "license": "MIT", "dependencies": { "fast-glob": "3.3.1" @@ -7428,6 +7457,12 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "license": "MIT" }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", + "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", + "license": "MIT" + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -10314,6 +10349,7 @@ "version": "2.8.16", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", + "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -10422,6 +10458,7 @@ "version": "4.26.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", + "dev": true, "funding": [ { "type": "opencollective", @@ -10941,6 +10978,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -11665,6 +11703,7 @@ "version": "1.5.235", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.235.tgz", "integrity": "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==", + "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { @@ -12088,9 +12127,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -12101,32 +12140,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" } }, "node_modules/esbuild-node-externals": { @@ -12245,23 +12284,24 @@ } }, "node_modules/eslint-config-next": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.1.tgz", - "integrity": "sha512-wNuHw5gNOxwLUvpg0cu6IL0crrVC9hAwdS/7UwleNkwyaMiWIOAwf8yzXVqBBzL3c9A7jVRngJxjoSpPP1aEhg==", + "version": "15.5.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.6.tgz", + "integrity": "sha512-cGr3VQlPsZBEv8rtYp4BpG1KNXDqGvPo9VC1iaCgIA11OfziC/vczng+TnAS3WpRIR3Q5ye/6yl+CRUuZ1fPGg==", "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.0.1", + "@next/eslint-plugin-next": "15.5.6", + "@rushstack/eslint-patch": "^1.10.3", + "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.32.0", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^7.0.0", - "globals": "16.4.0", - "typescript-eslint": "^8.46.0" + "eslint-plugin-react-hooks": "^5.0.0" }, "peerDependencies": { - "eslint": ">=9.0.0", + "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", "typescript": ">=3.3.1" }, "peerDependenciesMeta": { @@ -12270,18 +12310,6 @@ } } }, - "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==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "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", @@ -12475,19 +12503,12 @@ } }, "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==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", "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" + "node": ">=10" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" @@ -13233,6 +13254,7 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -13414,6 +13436,7 @@ "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" @@ -13587,21 +13610,6 @@ "node": ">=18.0.0" } }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, "node_modules/html-to-text": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", @@ -14395,6 +14403,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -14432,6 +14441,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -14992,6 +15002,7 @@ "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" @@ -15613,6 +15624,7 @@ "version": "2.0.23", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", + "dev": true, "license": "MIT" }, "node_modules/nodemailer": { @@ -22044,6 +22056,7 @@ "version": "8.46.3", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.3.tgz", "integrity": "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==", + "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.3", @@ -22135,6 +22148,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -22758,6 +22772,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, "license": "ISC" }, "node_modules/yaml": { From 7a31292ec7192efae9b1be30fa5113153150debe Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 06:34:40 +0100 Subject: [PATCH 029/153] =?UTF-8?q?=E2=8F=AA=20revert=20package.json=20cha?= =?UTF-8?q?nges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 345 ++++++++++++++++++++++------------------------ package.json | 10 +- 2 files changed, 170 insertions(+), 185 deletions(-) diff --git a/package-lock.json b/package-lock.json index d030337d..b97e1b30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "date-fns": "4.1.0", "drizzle-orm": "0.44.7", "eslint": "9.39.0", - "eslint-config-next": "15.5.6", + "eslint-config-next": "16.0.1", "express": "5.1.0", "express-rate-limit": "8.2.1", "glob": "11.0.3", @@ -112,10 +112,10 @@ "zod-validation-error": "3.5.2" }, "devDependencies": { - "@dotenvx/dotenvx": "1.51.0", + "@dotenvx/dotenvx": "1.51.1", "@esbuild-plugins/tsconfig-paths": "0.1.2", "@react-email/preview-server": "4.3.2", - "@tailwindcss/postcss": "^4.1.16", + "@tailwindcss/postcss": "^4.1.17", "@tanstack/react-query-devtools": "^5.90.2", "@types/better-sqlite3": "7.6.12", "@types/cookie-parser": "1.4.10", @@ -137,7 +137,7 @@ "@types/ws": "8.18.1", "@types/yargs": "17.0.34", "drizzle-kit": "0.31.6", - "esbuild": "0.25.11", + "esbuild": "0.25.12", "esbuild-node-externals": "1.18.0", "postcss": "^8", "react-email": "4.3.2", @@ -145,7 +145,7 @@ "tsc-alias": "1.8.16", "tsx": "4.20.6", "typescript": "^5", - "typescript-eslint": "^8.46.2" + "typescript-eslint": "^8.46.3" } }, "node_modules/@alloc/quick-lru": { @@ -165,7 +165,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1621,7 +1620,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", @@ -1636,7 +1634,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1646,7 +1643,6 @@ "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", - "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", @@ -1677,7 +1673,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1687,7 +1682,6 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.28.3", @@ -1704,7 +1698,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.4" @@ -1720,7 +1713,6 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.27.2", @@ -1737,7 +1729,6 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -1747,7 +1738,6 @@ "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1757,7 +1747,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.27.1", @@ -1771,7 +1760,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.4" @@ -1787,7 +1775,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -1806,7 +1793,6 @@ "version": "7.28.3", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", @@ -1824,7 +1810,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.4" @@ -1840,7 +1825,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -1859,7 +1843,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1869,7 +1852,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1879,7 +1861,6 @@ "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1889,7 +1870,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", @@ -1903,7 +1883,6 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.27.0" @@ -1919,7 +1898,6 @@ "version": "7.27.2", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", @@ -1934,7 +1912,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.28.4" @@ -1950,7 +1927,6 @@ "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -1969,7 +1945,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", @@ -2006,9 +1981,9 @@ "license": "MIT" }, "node_modules/@dotenvx/dotenvx": { - "version": "1.51.0", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.0.tgz", - "integrity": "sha512-CbMGzyOYSyFF7d4uaeYwO9gpSBzLTnMmSmTVpCZjvpJFV69qYbjYPpzNnCz1mb2wIvEhjWjRwQWuBzTO0jITww==", + "version": "1.51.1", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.1.tgz", + "integrity": "sha512-fqcQxcxC4LOaUlW8IkyWw8x0yirlLUkbxohz9OnWvVWjf73J5yyw7jxWnkOJaUKXZotcGEScDox9MU6rSkcDgg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2535,9 +2510,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", - "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -2552,9 +2527,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", - "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -2569,9 +2544,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", - "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -2586,9 +2561,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", - "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -2603,9 +2578,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", - "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -2620,9 +2595,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", - "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -2637,9 +2612,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", - "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -2654,9 +2629,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", - "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -2671,9 +2646,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", - "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -2688,9 +2663,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", - "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -2705,9 +2680,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", - "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -2722,9 +2697,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", - "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -2739,9 +2714,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", - "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -2756,9 +2731,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", - "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -2773,9 +2748,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", - "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -2790,9 +2765,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", - "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -2807,9 +2782,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", - "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -2824,9 +2799,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", - "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "cpu": [ "arm64" ], @@ -2841,9 +2816,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", - "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -2858,9 +2833,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", - "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "cpu": [ "arm64" ], @@ -2875,9 +2850,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", - "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -2892,9 +2867,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", - "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "cpu": [ "arm64" ], @@ -2909,9 +2884,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", - "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -2926,9 +2901,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", - "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -2943,9 +2918,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", - "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -2960,9 +2935,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", - "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -3835,7 +3810,6 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -3857,7 +3831,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -3878,14 +3851,12 @@ "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -3960,9 +3931,9 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.5.6.tgz", - "integrity": "sha512-YxDvsT2fwy1j5gMqk3ppXlsgDopHnkM4BoxSVASbvvgh5zgsK8lvWerDzPip8k3WVzsTZ1O7A7si1KNfN4OZfQ==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.1.tgz", + "integrity": "sha512-g4Cqmv/gyFEXNeVB2HkqDlYKfy+YrlM2k8AVIO/YQVEPfhVruH1VA99uT1zELLnPLIeOnx8IZ6Ddso0asfTIdw==", "license": "MIT", "dependencies": { "fast-glob": "3.3.1" @@ -7457,12 +7428,6 @@ "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", "license": "MIT" }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", - "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", - "license": "MIT" - }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -10349,7 +10314,6 @@ "version": "2.8.16", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz", "integrity": "sha512-OMu3BGQ4E7P1ErFsIPpbJh0qvDudM/UuJeHgkAvfWe+0HFJCXh+t/l8L6fVLR55RI/UbKrVLnAXZSVwd9ysWYw==", - "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.js" @@ -10458,7 +10422,6 @@ "version": "4.26.3", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz", "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==", - "dev": true, "funding": [ { "type": "opencollective", @@ -10978,7 +10941,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -11703,7 +11665,6 @@ "version": "1.5.235", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.235.tgz", "integrity": "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==", - "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { @@ -12127,9 +12088,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.25.11", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", - "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -12140,32 +12101,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.11", - "@esbuild/android-arm": "0.25.11", - "@esbuild/android-arm64": "0.25.11", - "@esbuild/android-x64": "0.25.11", - "@esbuild/darwin-arm64": "0.25.11", - "@esbuild/darwin-x64": "0.25.11", - "@esbuild/freebsd-arm64": "0.25.11", - "@esbuild/freebsd-x64": "0.25.11", - "@esbuild/linux-arm": "0.25.11", - "@esbuild/linux-arm64": "0.25.11", - "@esbuild/linux-ia32": "0.25.11", - "@esbuild/linux-loong64": "0.25.11", - "@esbuild/linux-mips64el": "0.25.11", - "@esbuild/linux-ppc64": "0.25.11", - "@esbuild/linux-riscv64": "0.25.11", - "@esbuild/linux-s390x": "0.25.11", - "@esbuild/linux-x64": "0.25.11", - "@esbuild/netbsd-arm64": "0.25.11", - "@esbuild/netbsd-x64": "0.25.11", - "@esbuild/openbsd-arm64": "0.25.11", - "@esbuild/openbsd-x64": "0.25.11", - "@esbuild/openharmony-arm64": "0.25.11", - "@esbuild/sunos-x64": "0.25.11", - "@esbuild/win32-arm64": "0.25.11", - "@esbuild/win32-ia32": "0.25.11", - "@esbuild/win32-x64": "0.25.11" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/esbuild-node-externals": { @@ -12284,24 +12245,23 @@ } }, "node_modules/eslint-config-next": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.5.6.tgz", - "integrity": "sha512-cGr3VQlPsZBEv8rtYp4BpG1KNXDqGvPo9VC1iaCgIA11OfziC/vczng+TnAS3WpRIR3Q5ye/6yl+CRUuZ1fPGg==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.1.tgz", + "integrity": "sha512-wNuHw5gNOxwLUvpg0cu6IL0crrVC9hAwdS/7UwleNkwyaMiWIOAwf8yzXVqBBzL3c9A7jVRngJxjoSpPP1aEhg==", "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "15.5.6", - "@rushstack/eslint-patch": "^1.10.3", - "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@next/eslint-plugin-next": "16.0.1", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", - "eslint-plugin-import": "^2.31.0", + "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", - "eslint-plugin-react-hooks": "^5.0.0" + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" }, "peerDependencies": { - "eslint": "^7.23.0 || ^8.0.0 || ^9.0.0", + "eslint": ">=9.0.0", "typescript": ">=3.3.1" }, "peerDependenciesMeta": { @@ -12310,6 +12270,18 @@ } } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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", @@ -12503,12 +12475,19 @@ } }, "node_modules/eslint-plugin-react-hooks": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", - "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "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==", "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": ">=10" + "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" @@ -13254,7 +13233,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -13436,7 +13414,6 @@ "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" @@ -13610,6 +13587,21 @@ "node": ">=18.0.0" } }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/html-to-text": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz", @@ -14403,7 +14395,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -14441,7 +14432,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -15002,7 +14992,6 @@ "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" @@ -15624,7 +15613,6 @@ "version": "2.0.23", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==", - "dev": true, "license": "MIT" }, "node_modules/nodemailer": { @@ -22056,7 +22044,6 @@ "version": "8.46.3", "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.3.tgz", "integrity": "sha512-bAfgMavTuGo+8n6/QQDVQz4tZ4f7Soqg53RbrlZQEoAltYop/XR4RAts/I0BrO3TTClTSTFJ0wYbla+P8cEWJA==", - "dev": true, "license": "MIT", "dependencies": { "@typescript-eslint/eslint-plugin": "8.46.3", @@ -22148,7 +22135,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -22772,7 +22758,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, "license": "ISC" }, "node_modules/yaml": { diff --git a/package.json b/package.json index 90f8665d..6ca20376 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "date-fns": "4.1.0", "drizzle-orm": "0.44.7", "eslint": "9.39.0", - "eslint-config-next": "15.5.6", + "eslint-config-next": "16.0.1", "express": "5.1.0", "express-rate-limit": "8.2.1", "glob": "11.0.3", @@ -135,11 +135,11 @@ "zod-validation-error": "3.5.2" }, "devDependencies": { - "@dotenvx/dotenvx": "1.51.0", + "@dotenvx/dotenvx": "1.51.1", "@esbuild-plugins/tsconfig-paths": "0.1.2", "@react-email/preview-server": "4.3.2", "@tanstack/react-query-devtools": "^5.90.2", - "@tailwindcss/postcss": "^4.1.16", + "@tailwindcss/postcss": "^4.1.17", "@types/better-sqlite3": "7.6.12", "@types/cookie-parser": "1.4.10", "@types/cors": "2.8.19", @@ -160,7 +160,7 @@ "@types/ws": "8.18.1", "@types/yargs": "17.0.34", "drizzle-kit": "0.31.6", - "esbuild": "0.25.11", + "esbuild": "0.25.12", "esbuild-node-externals": "1.18.0", "postcss": "^8", "react-email": "4.3.2", @@ -168,7 +168,7 @@ "tsc-alias": "1.8.16", "tsx": "4.20.6", "typescript": "^5", - "typescript-eslint": "^8.46.2" + "typescript-eslint": "^8.46.3" }, "overrides": { "emblor": { From a2ab7191e57916b387fae85fb4be993608b1395c Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 06:58:05 +0100 Subject: [PATCH 030/153] =?UTF-8?q?=F0=9F=94=87remove=20log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/auth/resource/[resourceGuid]/page.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/auth/resource/[resourceGuid]/page.tsx b/src/app/auth/resource/[resourceGuid]/page.tsx index 9dcba710..4ff33734 100644 --- a/src/app/auth/resource/[resourceGuid]/page.tsx +++ b/src/app/auth/resource/[resourceGuid]/page.tsx @@ -53,9 +53,7 @@ export default async function ResourceAuthPage(props: { if (res && res.status === 200) { authInfo = res.data.data; } - } catch (e) { - console.error(e); - } + } catch (e) {} const user = await verifySession({ skipCheckVerifyEmail: true }); From 616fb9c8e9be76c4d57a41c7ee6c41b847158d4c Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 06:59:15 +0100 Subject: [PATCH 031/153] =?UTF-8?q?=E2=99=BB=EF=B8=8Fremove=20unused=20imp?= =?UTF-8?q?orts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/private/AuthPageSettings.tsx | 20 ++------------------ 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/src/components/private/AuthPageSettings.tsx b/src/components/private/AuthPageSettings.tsx index 2203fde2..4235368b 100644 --- a/src/components/private/AuthPageSettings.tsx +++ b/src/components/private/AuthPageSettings.tsx @@ -3,24 +3,8 @@ import { Button } from "@app/components/ui/button"; import { useOrgContext } from "@app/hooks/useOrgContext"; import { toast } from "@app/hooks/useToast"; -import { - useState, - useEffect, - forwardRef, - useImperativeHandle, - RefObject, - Ref, - useActionState -} from "react"; -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage -} from "@/components/ui/form"; +import { useState, useEffect, useActionState } from "react"; +import { Form } from "@/components/ui/form"; import { Label } from "@/components/ui/label"; import { z } from "zod"; import { useForm } from "react-hook-form"; From 1d9ed9d21968079d5a13b100fdcabc661fad58da Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 07:01:27 +0100 Subject: [PATCH 032/153] =?UTF-8?q?=F0=9F=92=A1remove=20useless=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AuthPageBrandingForm.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx index 580215fc..95b7994d 100644 --- a/src/components/AuthPageBrandingForm.tsx +++ b/src/components/AuthPageBrandingForm.tsx @@ -129,7 +129,6 @@ export default function AuthPageBrandingForm({ if (!isValid) return; try { - // Update or existing auth page domain const updateRes = await api.put( `/org/${orgId}/login-page-branding`, { @@ -138,7 +137,6 @@ export default function AuthPageBrandingForm({ ); if (updateRes.status === 200 || updateRes.status === 201) { - // update the data from the API router.refresh(); toast({ variant: "default", @@ -160,13 +158,11 @@ export default function AuthPageBrandingForm({ async function deleteBranding() { try { - // Update or existing auth page domain const updateRes = await api.delete( `/org/${orgId}/login-page-branding` ); if (updateRes.status === 200) { - // update the data from the API router.refresh(); form.reset(); setIsDeleteModalOpen(false); From 8e8f992876cb87964f15ec3efe9c3108736c257a Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 07:04:36 +0100 Subject: [PATCH 033/153] =?UTF-8?q?=F0=9F=92=A1add=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/BrandingLogo.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/BrandingLogo.tsx b/src/components/BrandingLogo.tsx index 86a49496..139d76b4 100644 --- a/src/components/BrandingLogo.tsx +++ b/src/components/BrandingLogo.tsx @@ -45,6 +45,8 @@ export default function BrandingLogo(props: BrandingLogoProps) { setPath(props.logoPath ?? getPath()); }, [theme, env, props.logoPath]); + // we use `img` tag here because the `logoPath` could be any URL + // and next.js `Image` component only accepts a restricted number of domains const Component = props.logoPath ? "img" : Image; return ( From 2f34def4d71600973a2b2439ad395125178086d9 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 07:06:20 +0100 Subject: [PATCH 034/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20correctly=20apply?= =?UTF-8?q?=20the=20CSS=20variable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/DomainPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 64fbd726..50a83611 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -749,7 +749,7 @@ export default function DomainPicker2({ }} style={{ // @ts-expect-error CSS variable - "--cols": cols + "--cols": `repeat(${cols}, minmax(0, 1fr))` }} className="grid gap-2 grid-cols-1 sm:grid-cols-(--cols)" > From 2466d24c1a8198985f6e1ae7feeb11cdca878d78 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 15 Nov 2025 07:08:07 +0100 Subject: [PATCH 035/153] =?UTF-8?q?=F0=9F=94=A5remove=20unused=20imports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ResourceAuthPortal.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/ResourceAuthPortal.tsx b/src/components/ResourceAuthPortal.tsx index 59edf679..15d3507f 100644 --- a/src/components/ResourceAuthPortal.tsx +++ b/src/components/ResourceAuthPortal.tsx @@ -23,7 +23,7 @@ import { FormLabel, FormMessage } from "@/components/ui/form"; -import { LockIcon, Binary, Key, User, Send, AtSign, Regex } from "lucide-react"; +import { LockIcon, Binary, Key, User, Send, AtSign } from "lucide-react"; import { InputOTP, InputOTPGroup, @@ -47,7 +47,6 @@ import { useSupporterStatusContext } from "@app/hooks/useSupporterStatusContext" import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; -import type { GetLoginPageBrandingResponse } from "@server/routers/loginPage/types"; import { replacePlaceholder } from "@app/lib/replacePlaceholder"; const pinSchema = z.object({ From 83f36bce9d62037335f09ad884c0e242ee52e582 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 17 Nov 2025 22:17:55 +0100 Subject: [PATCH 036/153] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[orgId]/settings/general/layout.tsx | 13 ++----------- src/lib/api/getSubscriptionStatus.ts | 20 ++++++++++++++++++++ 2 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 src/lib/api/getSubscriptionStatus.ts diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 9472eb52..f7ece91d 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -17,6 +17,7 @@ import { GetOrgTierResponse } from "@server/routers/billing/types"; import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; import { build } from "@server/build"; import { TierId } from "@server/lib/billing/tiers"; +import { isSubscribed } from "@app/lib/api/getSubscriptionStatus"; type GeneralSettingsProps = { children: React.ReactNode; @@ -51,16 +52,6 @@ export default async function GeneralSettingsPage({ redirect(`/${orgId}`); } - let subscriptionStatus: GetOrgTierResponse | null = null; - try { - const subRes = await getCachedSubscription(orgId); - subscriptionStatus = subRes.data.data; - } catch {} - const subscribed = - build === "enterprise" - ? true - : subscriptionStatus?.tier === TierId.STANDARD; - const t = await getTranslations(); const navItems: TabItem[] = [ @@ -70,7 +61,7 @@ export default async function GeneralSettingsPage({ exact: true } ]; - if (subscribed) { + if (build === "saas") { navItems.push({ title: t("authPage"), href: `/{orgId}/settings/general/auth-page` diff --git a/src/lib/api/getSubscriptionStatus.ts b/src/lib/api/getSubscriptionStatus.ts new file mode 100644 index 00000000..e9b05e40 --- /dev/null +++ b/src/lib/api/getSubscriptionStatus.ts @@ -0,0 +1,20 @@ +import { build } from "@server/build"; +import { TierId } from "@server/lib/billing/tiers"; +import { cache } from "react"; +import { getCachedSubscription } from "./getCachedSubscription"; +import type { GetOrgTierResponse } from "@server/routers/billing/types"; + +export const isSubscribed = cache(async (orgId: string) => { + let subscriptionStatus: GetOrgTierResponse | null = null; + try { + const subRes = await getCachedSubscription(orgId); + subscriptionStatus = subRes.data.data; + } catch {} + + const subscribed = + build === "enterprise" + ? true + : subscriptionStatus?.tier === TierId.STANDARD; + + return subscribed; +}); From ee7e7778b6c8b94c15f78ee822e4c43fa6610dab Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 17 Nov 2025 22:23:11 +0100 Subject: [PATCH 037/153] =?UTF-8?q?=E2=99=BB=EF=B8=8Fcommit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[orgId]/settings/general/layout.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index f7ece91d..812b9491 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -1,23 +1,15 @@ -import { internal } from "@app/lib/api"; -import { authCookieHeader } from "@app/lib/api/cookies"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; import { HorizontalTabs, type TabItem } from "@app/components/HorizontalTabs"; import { verifySession } from "@app/lib/auth/verifySession"; import OrgProvider from "@app/providers/OrgProvider"; import OrgUserProvider from "@app/providers/OrgUserProvider"; -import { GetOrgResponse } from "@server/routers/org"; -import { GetOrgUserResponse } from "@server/routers/user"; -import { AxiosResponse } from "axios"; + import { redirect } from "next/navigation"; import { getTranslations } from "next-intl/server"; import { getCachedOrg } from "@app/lib/api/getCachedOrg"; import { getCachedOrgUser } from "@app/lib/api/getCachedOrgUser"; -import { GetOrgTierResponse } from "@server/routers/billing/types"; -import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; import { build } from "@server/build"; -import { TierId } from "@server/lib/billing/tiers"; -import { isSubscribed } from "@app/lib/api/getSubscriptionStatus"; type GeneralSettingsProps = { children: React.ReactNode; From 66b01b764faf696896160777ac92fa7615bf47cd Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 18 Nov 2025 01:07:46 +0100 Subject: [PATCH 038/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20adapt=20zod=20sche?= =?UTF-8?q?ma=20to=20v4=20and=20move=20=20form=20description=20below=20the?= =?UTF-8?q?=20inptu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loginPage/upsertLoginPageBranding.ts | 36 ++++----- src/components/AuthPageBrandingForm.tsx | 74 +++++++++---------- 2 files changed, 53 insertions(+), 57 deletions(-) diff --git a/server/private/routers/loginPage/upsertLoginPageBranding.ts b/server/private/routers/loginPage/upsertLoginPageBranding.ts index 495c15fc..f9f9d08c 100644 --- a/server/private/routers/loginPage/upsertLoginPageBranding.ts +++ b/server/private/routers/loginPage/upsertLoginPageBranding.ts @@ -29,27 +29,23 @@ import { getOrgTierData } from "#private/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import { build } from "@server/build"; -const paramsSchema = z - .object({ - orgId: z.string() - }) - .strict(); +const paramsSchema = z.strictObject({ + orgId: z.string() +}); -const bodySchema = z - .object({ - logoUrl: z.string().url(), - logoWidth: z.coerce.number().min(1), - logoHeight: z.coerce.number().min(1), - resourceTitle: z.string(), - resourceSubtitle: z.string().optional(), - orgTitle: z.string().optional(), - orgSubtitle: z.string().optional(), - primaryColor: z - .string() - .regex(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i) - .optional() - }) - .strict(); +const bodySchema = z.strictObject({ + logoUrl: z.url(), + logoWidth: z.coerce.number().min(1), + logoHeight: z.coerce.number().min(1), + resourceTitle: z.string(), + resourceSubtitle: z.string().optional(), + orgTitle: z.string().optional(), + orgSubtitle: z.string().optional(), + primaryColor: z + .string() + .regex(/^#([0-9a-f]{6}|[0-9a-f]{3})$/i) + .optional() +}); export type UpdateLoginPageBrandingBody = z.infer; diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx index 95b7994d..04a6cbcb 100644 --- a/src/components/AuthPageBrandingForm.tsx +++ b/src/components/AuthPageBrandingForm.tsx @@ -50,29 +50,26 @@ export type AuthPageCustomizationProps = { }; const AuthPageFormSchema = z.object({ - logoUrl: z - .string() - .url() - .refine( - async (url) => { - try { - const response = await fetch(url); - return ( - response.status === 200 && - (response.headers.get("content-type") ?? "").startsWith( - "image/" - ) - ); - } catch (error) { - return false; - } - }, - { - message: "Invalid logo URL, must be a valid image URL" + logoUrl: z.url().refine( + async (url) => { + try { + const response = await fetch(url); + return ( + response.status === 200 && + (response.headers.get("content-type") ?? "").startsWith( + "image/" + ) + ); + } catch (error) { + return false; } - ), - logoWidth: z.coerce.number().min(1), - logoHeight: z.coerce.number().min(1), + }, + { + error: "Invalid logo URL, must be a valid image URL" + } + ), + logoWidth: z.coerce.number().min(1), + logoHeight: z.coerce.number().min(1), orgTitle: z.string().optional(), orgSubtitle: z.string().optional(), resourceTitle: z.string(), @@ -272,7 +269,7 @@ export default function AuthPageBrandingForm({ )} /> - + @@ -300,7 +297,7 @@ export default function AuthPageBrandingForm({ <> -
+
+ + + + {t( "brandingOrgDescription", @@ -320,9 +321,6 @@ export default function AuthPageBrandingForm({ } )} - - - )} @@ -337,6 +335,10 @@ export default function AuthPageBrandingForm({ "brandingOrgSubtitle" )} + + + + {t( "brandingOrgDescription", @@ -346,9 +348,6 @@ export default function AuthPageBrandingForm({ } )} - - - )} @@ -359,7 +358,7 @@ export default function AuthPageBrandingForm({ -
+
{t("brandingResourceTitle")} + + + + {t( "brandingResourceDescription", @@ -377,9 +380,6 @@ export default function AuthPageBrandingForm({ } )} - - - )} @@ -394,6 +394,9 @@ export default function AuthPageBrandingForm({ "brandingResourceSubtitle" )} + + + {t( "brandingResourceDescription", @@ -403,9 +406,6 @@ export default function AuthPageBrandingForm({ } )} - - - )} From 30f3ab11b2c598cd68df1aa61a9cc11bea1abe54 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 18 Nov 2025 02:26:25 +0100 Subject: [PATCH 039/153] =?UTF-8?q?=F0=9F=9A=9A=20rename=20`SecurityFeatur?= =?UTF-8?q?esAlert`=20to=20`PaidFeaturesAlert`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...rityFeaturesAlert.tsx => PaidFeaturesAlert.tsx} | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) rename src/components/{SecurityFeaturesAlert.tsx => PaidFeaturesAlert.tsx} (61%) diff --git a/src/components/SecurityFeaturesAlert.tsx b/src/components/PaidFeaturesAlert.tsx similarity index 61% rename from src/components/SecurityFeaturesAlert.tsx rename to src/components/PaidFeaturesAlert.tsx index 2531659b..30ba7d76 100644 --- a/src/components/SecurityFeaturesAlert.tsx +++ b/src/components/PaidFeaturesAlert.tsx @@ -2,17 +2,14 @@ import { Alert, AlertDescription } from "@app/components/ui/alert"; import { build } from "@server/build"; import { useTranslations } from "next-intl"; -import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; -import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; -export function SecurityFeaturesAlert() { +export function PaidFeaturesAlert() { const t = useTranslations(); - const { isUnlocked } = useLicenseStatusContext(); - const subscriptionStatus = useSubscriptionStatusContext(); - + const { hasSaasSubscription, hasEnterpriseLicense } = usePaidStatus(); return ( <> - {build === "saas" && !subscriptionStatus?.isSubscribed() ? ( + {build === "saas" && !hasSaasSubscription ? ( {t("subscriptionRequiredToUse")} @@ -20,7 +17,7 @@ export function SecurityFeaturesAlert() { ) : null} - {build === "enterprise" && !isUnlocked() ? ( + {build === "enterprise" && !hasEnterpriseLicense ? ( {t("licenseRequiredToUse")} @@ -30,4 +27,3 @@ export function SecurityFeaturesAlert() { ); } - From c5914dc0c00291ce9dd7c873db4d3d78c7e6a3fb Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 18 Nov 2025 02:26:49 +0100 Subject: [PATCH 040/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Also=20check=20for?= =?UTF-8?q?=20active=20subscription=20in=20paid=20status=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/usePaidStatus.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hooks/usePaidStatus.ts b/src/hooks/usePaidStatus.ts index 6b11a6fc..d8173e6e 100644 --- a/src/hooks/usePaidStatus.ts +++ b/src/hooks/usePaidStatus.ts @@ -9,7 +9,9 @@ export function usePaidStatus() { // Check if features are disabled due to licensing/subscription const hasEnterpriseLicense = build === "enterprise" && isUnlocked(); const hasSaasSubscription = - build === "saas" && subscription?.isSubscribed(); + build === "saas" && + subscription?.isSubscribed() && + subscription.isActive(); return { hasEnterpriseLicense, From 3ba65a3311c5c2a1d05fcd78233f79941c90e13e Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 18 Nov 2025 02:35:11 +0100 Subject: [PATCH 041/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20check=20for=20disa?= =?UTF-8?q?bled=20features=20in=20general=20org=20settings=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[orgId]/settings/general/page.tsx | 45 ++++++----------------- 1 file changed, 12 insertions(+), 33 deletions(-) diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 84f876d0..7928dc47 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -49,9 +49,10 @@ import { useUserContext } from "@app/hooks/useUserContext"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; import { SwitchInput } from "@app/components/SwitchInput"; -import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; +import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; // Session length options in hours const SESSION_LENGTH_OPTIONS = [ @@ -113,16 +114,7 @@ export default function GeneralPage() { const { user } = useUserContext(); const t = useTranslations(); const { env } = useEnvContext(); - const { isUnlocked } = useLicenseStatusContext(); - const subscription = useSubscriptionStatusContext(); - - // Check if security features are disabled due to licensing/subscription - const isSecurityFeatureDisabled = () => { - const isEnterpriseNotLicensed = build === "enterprise" && !isUnlocked(); - const isSaasNotSubscribed = - build === "saas" && !subscription?.isSubscribed(); - return isEnterpriseNotLicensed || isSaasNotSubscribed; - }; + const { isPaidUser, hasSaasSubscription } = usePaidStatus(); const [loadingDelete, setLoadingDelete] = useState(false); const [loadingSave, setLoadingSave] = useState(false); @@ -398,9 +390,7 @@ export default function GeneralPage() { {LOG_RETENTION_OPTIONS.filter( (option) => { if ( - build == - "saas" && - !subscription?.subscribed && + hasSaasSubscription && option.value > 30 ) { @@ -428,19 +418,15 @@ export default function GeneralPage() { )} /> - {build != "oss" && ( + {build !== "oss" && ( <> - + { - const isDisabled = - (build == "saas" && - !subscription?.subscribed) || - (build == "enterprise" && - !isUnlocked()); + const isDisabled = !isPaidUser; return ( @@ -506,11 +492,7 @@ export default function GeneralPage() { control={form.control} name="settingsLogRetentionDaysAction" render={({ field }) => { - const isDisabled = - (build == "saas" && - !subscription?.subscribed) || - (build == "enterprise" && - !isUnlocked()); + const isDisabled = !isPaidUser; return ( @@ -590,13 +572,12 @@ export default function GeneralPage() { - + { - const isDisabled = - isSecurityFeatureDisabled(); + const isDisabled = !isPaidUser; return ( @@ -643,8 +624,7 @@ export default function GeneralPage() { control={form.control} name="maxSessionLengthHours" render={({ field }) => { - const isDisabled = - isSecurityFeatureDisabled(); + const isDisabled = !isPaidUser; return ( @@ -730,8 +710,7 @@ export default function GeneralPage() { control={form.control} name="passwordExpiryDays" render={({ field }) => { - const isDisabled = - isSecurityFeatureDisabled(); + const isDisabled = !isPaidUser; return ( From 8c30995228c448d0c745206a91796866aa6e919b Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 18 Nov 2025 02:38:08 +0100 Subject: [PATCH 042/153] =?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/app/[orgId]/settings/general/page.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 7928dc47..54d22188 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -102,12 +102,11 @@ const LOG_RETENTION_OPTIONS = [ { label: "logRetention14Days", value: 14 }, { label: "logRetention30Days", value: 30 }, { label: "logRetention90Days", value: 90 }, - ...(build != "saas" ? [{ label: "logRetentionForever", value: -1 }] : []) + ...(build !== "saas" ? [{ label: "logRetentionForever", value: -1 }] : []) ]; export default function GeneralPage() { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const { orgUser } = userOrgUserContext(); const router = useRouter(); const { org } = useOrgContext(); const api = createApiClient(useEnvContext()); From e00c3f219321975fcf8a74b47bbfe0805ba2218b Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 18 Nov 2025 02:46:22 +0100 Subject: [PATCH 043/153] =?UTF-8?q?=F0=9F=9B=82=20=20check=20for=20subscri?= =?UTF-8?q?ption=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AuthPageBrandingForm.tsx | 28 +++++++++++++++------ src/components/private/AuthPageSettings.tsx | 26 +++++++++++-------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx index 04a6cbcb..136e97f1 100644 --- a/src/components/AuthPageBrandingForm.tsx +++ b/src/components/AuthPageBrandingForm.tsx @@ -43,6 +43,7 @@ import { } from "./Credenza"; import { usePaidStatus } from "@app/hooks/usePaidStatus"; import { build } from "@server/build"; +import { PaidFeaturesAlert } from "./PaidFeaturesAlert"; export type AuthPageCustomizationProps = { orgId: string; @@ -86,7 +87,7 @@ export default function AuthPageBrandingForm({ }: AuthPageCustomizationProps) { const env = useEnvContext(); const api = createApiClient(env); - const { hasSaasSubscription } = usePaidStatus(); + const { isPaidUser } = usePaidStatus(); const router = useRouter(); @@ -117,14 +118,15 @@ export default function AuthPageBrandingForm({ branding?.resourceSubtitle ?? `Choose your preferred authentication method for {{resourceName}}`, primaryColor: branding?.primaryColor ?? `#f36117` // default pangolin primary color - } + }, + disabled: !isPaidUser }); async function updateBranding() { const isValid = await form.trigger(); const brandingData = form.getValues(); - if (!isValid) return; + if (!isValid || !isPaidUser) return; try { const updateRes = await api.put( `/org/${orgId}/login-page-branding`, @@ -154,6 +156,8 @@ export default function AuthPageBrandingForm({ } async function deleteBranding() { + if (!isPaidUser) return; + try { const updateRes = await api.delete( `/org/${orgId}/login-page-branding` @@ -194,6 +198,8 @@ export default function AuthPageBrandingForm({ + + @@ -293,7 +299,7 @@ export default function AuthPageBrandingForm({
- {hasSaasSubscription && ( + {build === "saas" && ( <> @@ -446,7 +452,7 @@ export default function AuthPageBrandingForm({ type="submit" form="confirm-delete-branding-form" loading={isDeletingBranding} - disabled={isDeletingBranding} + disabled={isDeletingBranding || !isPaidUser} > {t("authPageBrandingDeleteConfirm")} @@ -460,7 +466,11 @@ export default function AuthPageBrandingForm({ variant="destructive" type="button" loading={isUpdatingBranding || isDeletingBranding} - disabled={isUpdatingBranding || isDeletingBranding} + disabled={ + isUpdatingBranding || + isDeletingBranding || + !isPaidUser + } onClick={() => { setIsDeleteModalOpen(true); }} @@ -474,7 +484,11 @@ export default function AuthPageBrandingForm({ type="submit" form="auth-page-branding-form" loading={isUpdatingBranding || isDeletingBranding} - disabled={isUpdatingBranding || isDeletingBranding} + disabled={ + isUpdatingBranding || + isDeletingBranding || + !isPaidUser + } > {t("saveAuthPageBranding")} diff --git a/src/components/private/AuthPageSettings.tsx b/src/components/private/AuthPageSettings.tsx index 4235368b..aff6662a 100644 --- a/src/components/private/AuthPageSettings.tsx +++ b/src/components/private/AuthPageSettings.tsx @@ -43,8 +43,8 @@ import DomainPicker from "@app/components/DomainPicker"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; import { InfoPopup } from "@app/components/ui/info-popup"; import { Alert, AlertDescription } from "@app/components/ui/alert"; -import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; import { build } from "@server/build"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; // Auth page form schema const AuthPageFormSchema = z.object({ @@ -74,7 +74,7 @@ function AuthPageSettings({ const t = useTranslations(); const { env } = useEnvContext(); - const subscription = useSubscriptionStatusContext(); + const { hasSaasSubscription } = usePaidStatus(); // Auth page domain state const [loginPage, setLoginPage] = useState(defaultLoginPage); @@ -176,10 +176,7 @@ function AuthPageSettings({ try { // Handle auth page domain if (data.authPageDomainId) { - if ( - build === "enterprise" || - (build === "saas" && subscription?.subscribed) - ) { + if (build === "enterprise" || hasSaasSubscription) { const sanitizedSubdomain = data.authPageSubdomain ? finalizeSubdomainSanitize(data.authPageSubdomain) : ""; @@ -284,7 +281,7 @@ function AuthPageSettings({ - {build === "saas" && !subscription?.subscribed ? ( + {!hasSaasSubscription ? ( {t("orgAuthPageDisabled")}{" "} @@ -368,6 +365,7 @@ function AuthPageSettings({ onClick={() => setEditDomainOpen(true) } + disabled={!hasSaasSubscription} > {form.watch("authPageDomainId") ? t("changeDomain") @@ -381,6 +379,9 @@ function AuthPageSettings({ onClick={ clearAuthPageDomain } + disabled={ + !hasSaasSubscription + } > @@ -398,8 +399,7 @@ function AuthPageSettings({ {env.flags.usePangolinDns && (build === "enterprise" || - (build === "saas" && - subscription?.subscribed)) && + !hasSaasSubscription) && loginPage?.domainId && loginPage?.fullDomain && !hasUnsavedChanges && ( @@ -425,7 +425,11 @@ function AuthPageSettings({ type="submit" form="auth-page-settings-form" loading={isSubmitting} - disabled={isSubmitting || !hasUnsavedChanges} + disabled={ + isSubmitting || + !hasUnsavedChanges || + !hasSaasSubscription + } > {t("saveAuthPageDomain")} @@ -474,7 +478,7 @@ function AuthPageSettings({ handleDomainSelection(selectedDomain); } }} - disabled={!selectedDomain} + disabled={!selectedDomain || !hasSaasSubscription} > {t("selectDomain")} From e867de023aa9a2fdcf9f32dd058ab021865bbfa7 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 18 Nov 2025 03:14:20 +0100 Subject: [PATCH 044/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20load=20branding=20?= =?UTF-8?q?only=20if=20correctly=20subscribed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/auth/resource/[resourceGuid]/page.tsx | 32 ++++++------------- ...bscriptionStatus.ts => isOrgSubscribed.ts} | 5 +-- 2 files changed, 13 insertions(+), 24 deletions(-) rename src/lib/api/{getSubscriptionStatus.ts => isOrgSubscribed.ts} (76%) diff --git a/src/app/auth/resource/[resourceGuid]/page.tsx b/src/app/auth/resource/[resourceGuid]/page.tsx index 4ff33734..e6d87075 100644 --- a/src/app/auth/resource/[resourceGuid]/page.tsx +++ b/src/app/auth/resource/[resourceGuid]/page.tsx @@ -27,6 +27,7 @@ import { GetOrgTierResponse } from "@server/routers/billing/types"; import { TierId } from "@server/lib/billing/tiers"; import { CheckOrgUserAccessResponse } from "@server/routers/org"; import OrgPolicyRequired from "@app/components/OrgPolicyRequired"; +import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed"; export const dynamic = "force-dynamic"; @@ -65,22 +66,7 @@ export default async function ResourceAuthPage(props: { ); } - let subscriptionStatus: GetOrgTierResponse | null = null; - if (build === "saas") { - try { - const getSubscription = cache(() => - priv.get>( - `/org/${authInfo.orgId}/billing/tier` - ) - ); - const subRes = await getSubscription(); - subscriptionStatus = subRes.data.data; - } catch {} - } - const subscribed = - build === "enterprise" - ? true - : subscriptionStatus?.tier === TierId.STANDARD; + const subscribed = await isOrgSubscribed(authInfo.orgId); const allHeaders = await headers(); const host = allHeaders.get("host"); @@ -254,7 +240,7 @@ export default async function ResourceAuthPage(props: { resourceId={authInfo.resourceId} skipToIdpId={authInfo.skipToIdpId} redirectUrl={redirectUrl} - orgId={build == "saas" ? authInfo.orgId : undefined} + orgId={build === "saas" ? authInfo.orgId : undefined} /> ); } @@ -262,11 +248,13 @@ export default async function ResourceAuthPage(props: { let branding: LoadLoginPageBrandingResponse | null = null; try { - const res = await priv.get< - AxiosResponse - >(`/login-page-branding?orgId=${authInfo.orgId}`); - if (res.status === 200) { - branding = res.data.data; + if (subscribed) { + const res = await priv.get< + AxiosResponse + >(`/login-page-branding?orgId=${authInfo.orgId}`); + if (res.status === 200) { + branding = res.data.data; + } } } catch (error) {} diff --git a/src/lib/api/getSubscriptionStatus.ts b/src/lib/api/isOrgSubscribed.ts similarity index 76% rename from src/lib/api/getSubscriptionStatus.ts rename to src/lib/api/isOrgSubscribed.ts index e9b05e40..251b9219 100644 --- a/src/lib/api/getSubscriptionStatus.ts +++ b/src/lib/api/isOrgSubscribed.ts @@ -4,7 +4,7 @@ import { cache } from "react"; import { getCachedSubscription } from "./getCachedSubscription"; import type { GetOrgTierResponse } from "@server/routers/billing/types"; -export const isSubscribed = cache(async (orgId: string) => { +export const isOrgSubscribed = cache(async (orgId: string) => { let subscriptionStatus: GetOrgTierResponse | null = null; try { const subRes = await getCachedSubscription(orgId); @@ -14,7 +14,8 @@ export const isSubscribed = cache(async (orgId: string) => { const subscribed = build === "enterprise" ? true - : subscriptionStatus?.tier === TierId.STANDARD; + : subscriptionStatus?.tier === TierId.STANDARD && + subscriptionStatus.active; return subscribed; }); From dc4f9a9bd1dfdd3f52eb9b39ac6d699fa1ea30c6 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 18 Nov 2025 03:32:05 +0100 Subject: [PATCH 045/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20check=20for=20lice?= =?UTF-8?q?nce=20when=20checking=20for=20subscription?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/auth/(private)/org/page.tsx | 13 ++---------- src/lib/api/isOrgSubscribed.ts | 31 +++++++++++++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/app/auth/(private)/org/page.tsx b/src/app/auth/(private)/org/page.tsx index 71d31c01..06910f6a 100644 --- a/src/app/auth/(private)/org/page.tsx +++ b/src/app/auth/(private)/org/page.tsx @@ -30,6 +30,7 @@ import { GetOrgTierResponse } from "@server/routers/billing/types"; import { TierId } from "@server/lib/billing/tiers"; import { getCachedSubscription } from "@app/lib/api/getCachedSubscription"; import { replacePlaceholder } from "@app/lib/replacePlaceholder"; +import { isOrgSubscribed } from "@app/lib/api/isOrgSubscribed"; export const dynamic = "force-dynamic"; @@ -77,17 +78,7 @@ export default async function OrgAuthPage(props: { redirect(env.app.dashboardUrl); } - let subscriptionStatus: GetOrgTierResponse | null = null; - if (build === "saas") { - try { - const subRes = await getCachedSubscription(loginPage.orgId); - subscriptionStatus = subRes.data.data; - } catch {} - } - const subscribed = - build === "enterprise" - ? true - : subscriptionStatus?.tier === TierId.STANDARD; + const subscribed = await isOrgSubscribed(loginPage.orgId); if (build === "saas" && !subscribed) { console.log( diff --git a/src/lib/api/isOrgSubscribed.ts b/src/lib/api/isOrgSubscribed.ts index 251b9219..9440330b 100644 --- a/src/lib/api/isOrgSubscribed.ts +++ b/src/lib/api/isOrgSubscribed.ts @@ -2,20 +2,29 @@ import { build } from "@server/build"; import { TierId } from "@server/lib/billing/tiers"; import { cache } from "react"; import { getCachedSubscription } from "./getCachedSubscription"; -import type { GetOrgTierResponse } from "@server/routers/billing/types"; +import { priv } from "."; +import { AxiosResponse } from "axios"; +import { GetLicenseStatusResponse } from "@server/routers/license/types"; export const isOrgSubscribed = cache(async (orgId: string) => { - let subscriptionStatus: GetOrgTierResponse | null = null; - try { - const subRes = await getCachedSubscription(orgId); - subscriptionStatus = subRes.data.data; - } catch {} + let subscribed = false; - const subscribed = - build === "enterprise" - ? true - : subscriptionStatus?.tier === TierId.STANDARD && - subscriptionStatus.active; + if (build === "enterprise") { + try { + const licenseStatusRes = + await priv.get>( + "/license/status" + ); + subscribed = licenseStatusRes.data.data.isLicenseValid; + } catch (error) {} + } else if (build === "saas") { + try { + const subRes = await getCachedSubscription(orgId); + subscribed = + subRes.data.data.tier === TierId.STANDARD && + subRes.data.data.active; + } catch {} + } return subscribed; }); From ff089ec6d7556e8d9f5f78c7c9d3a70542966465 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 18 Nov 2025 03:48:41 +0100 Subject: [PATCH 046/153] =?UTF-8?q?=F0=9F=93=A6update=20lockfile?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 39 +++++---------------------------------- 1 file changed, 5 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index 45ff4321..a3e3c391 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1644,7 +1644,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "license": "MIT", - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -4074,7 +4073,6 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -7241,7 +7239,6 @@ "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -7447,7 +7444,6 @@ "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7458,7 +7454,6 @@ "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.25.0" }, @@ -8893,7 +8888,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.6.tgz", "integrity": "sha512-gB1sljYjcobZKxjPbKSa31FUTyr+ROaBdoH+wSSs9Dk+yDCmMs+TkTV3PybRRVLC7ax7q0erJ9LvRWnMktnRAw==", "license": "MIT", - "peer": true, "dependencies": { "@tanstack/query-core": "5.90.6" }, @@ -8999,7 +8993,6 @@ "integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -9086,7 +9079,6 @@ "integrity": "sha512-LuIQOcb6UmnF7C1PCFmEU1u2hmiHL43fgFQX67sN3H4Z+0Yk0Neo++mFsBjhOAuLzvlQeqAAkeDOZrJs9rzumQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -9180,7 +9172,6 @@ "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -9216,7 +9207,6 @@ "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -9250,7 +9240,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -9261,7 +9250,6 @@ "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -9405,7 +9393,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.3.tgz", "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -10079,7 +10066,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -10609,7 +10595,6 @@ "integrity": "sha512-mXpa5jnIKKHeoGzBrUJrc65cXFKcILGZpU3FXR0pradUEm9MA7UZz02qfEejaMcm9iXrSOCenwwYMJ/tZ1y5Ig==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -10722,7 +10707,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -11723,7 +11707,8 @@ "version": "3.1.7", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==", - "license": "(MPL-2.0 OR Apache-2.0)" + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true }, "node_modules/domutils": { "version": "3.2.2", @@ -12863,7 +12848,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -12960,7 +12944,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -13138,7 +13121,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -13447,7 +13429,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -16057,6 +16038,7 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.54.0.tgz", "integrity": "sha512-hx45SEUoLatgWxHKCmlLJH81xBo0uXP4sRkESUpmDQevfi+e7K1VuiSprK6UpQ8u4zOcKNiH0pMvHvlMWA/4cw==", "license": "MIT", + "peer": true, "dependencies": { "dompurify": "3.1.7", "marked": "14.0.0" @@ -16067,6 +16049,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" }, @@ -16189,7 +16172,6 @@ "resolved": "https://registry.npmjs.org/next/-/next-15.5.6.tgz", "integrity": "sha512-zTxsnI3LQo3c9HSdSf91O1jMNsEzIXDShXd4wVdg9y5shwLqBXi4ZtUUJyB86KGVSJLZx0PFONvO54aheGX8QQ==", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "15.5.6", "@swc/helpers": "0.5.15", @@ -18636,7 +18618,6 @@ "version": "4.0.3", "inBundle": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -19621,7 +19602,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.9.1", "pg-pool": "^3.10.1", @@ -19798,7 +19778,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -20256,7 +20235,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -20287,7 +20265,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -21063,7 +21040,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.0.tgz", "integrity": "sha512-xXBqsWGKrY46ZqaHDo+ZUYiMUgi8suYu5kdrS20EG8KiL7VRQitEbNjm+UcrDYrNi1YLyfpmAeGjCZYXLT9YBw==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -21557,7 +21533,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -22777,8 +22752,7 @@ "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -23792,7 +23766,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -24306,7 +24279,6 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", @@ -24613,7 +24585,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From 744305ab39ef90d7cd6f22ceba75aba37be9c90d Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 5 Dec 2025 00:02:13 +0100 Subject: [PATCH 047/153] =?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/LogAnalyticsData.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/LogAnalyticsData.tsx b/src/components/LogAnalyticsData.tsx index 8a5a7401..d1008a51 100644 --- a/src/components/LogAnalyticsData.tsx +++ b/src/components/LogAnalyticsData.tsx @@ -93,11 +93,12 @@ export function LogAnalyticsData(props: AnalyticsContentProps) { }) ); - const percentBlocked = stats - ? new Intl.NumberFormat(navigator.language, { - maximumFractionDigits: 2 - }).format((stats.totalBlocked / stats.totalRequests) * 100) - : null; + const percentBlocked = + stats && stats.totalRequests > 0 + ? new Intl.NumberFormat(navigator.language, { + maximumFractionDigits: 2 + }).format((stats.totalBlocked / stats.totalRequests) * 100) + : null; const totalRequests = stats ? new Intl.NumberFormat(navigator.language, { maximumFractionDigits: 0 From d89f5279bf0a03361737e1dc6169ff60ab49d776 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 5 Dec 2025 01:08:02 +0100 Subject: [PATCH 048/153] =?UTF-8?q?=E2=99=BB=EF=B8=8Faddress=20PR=20feedba?= =?UTF-8?q?ck?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[orgId]/settings/general/layout.tsx | 2 +- src/lib/queries.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/[orgId]/settings/general/layout.tsx b/src/app/[orgId]/settings/general/layout.tsx index 812b9491..2914573f 100644 --- a/src/app/[orgId]/settings/general/layout.tsx +++ b/src/app/[orgId]/settings/general/layout.tsx @@ -53,7 +53,7 @@ export default async function GeneralSettingsPage({ exact: true } ]; - if (build === "saas") { + if (build !== "oss") { navItems.push({ title: t("authPage"), href: `/{orgId}/settings/general/auth-page` diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 0364e291..d2c167b8 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -65,7 +65,7 @@ export const productUpdatesQueries = { } return false; }, - enabled: enabled && (build === "oss" || build === "enterprise") // disabled in cloud version + enabled: enabled && build !== "saas" // disabled in cloud version // because we don't need to listen for new versions there }) }; From a0a369dc43ebd8c11723a3b265642b99ecfc5aef Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 5 Dec 2025 23:10:10 +0100 Subject: [PATCH 049/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20reverse?= =?UTF-8?q?=20proxy=20targets=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/proxy/[niceId]/proxy/page.tsx | 433 +++++++++--------- src/lib/queries.ts | 12 + 2 files changed, 217 insertions(+), 228 deletions(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx index ba3499b5..450c43fa 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, use } from "react"; +import HealthCheckDialog from "@/components/HealthCheckDialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -11,11 +11,34 @@ import { SelectValue } from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; -import { AxiosResponse } from "axios"; -import { ListTargetsResponse } from "@server/routers/target/listTargets"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; +import { ContainersSelector } from "@app/components/ContainersSelector"; +import { HeadersInput } from "@app/components/HeadersInput"; +import { + PathMatchDisplay, + PathMatchModal, + PathRewriteDisplay, + PathRewriteModal +} from "@app/components/PathMatchRenameModal"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { SwitchInput } from "@app/components/SwitchInput"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; +import { Badge } from "@app/components/ui/badge"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; import { Form, FormControl, @@ -25,17 +48,11 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; -import { CreateTargetResponse } from "@server/routers/target"; import { - ColumnDef, - getFilteredRowModel, - getSortedRowModel, - getPaginationRowModel, - getCoreRowModel, - useReactTable, - flexRender, - Row -} from "@tanstack/react-table"; + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; import { Table, TableBody, @@ -44,89 +61,62 @@ import { TableHeader, TableRow } from "@app/components/ui/table"; -import { toast } from "@app/hooks/useToast"; -import { useResourceContext } from "@app/hooks/useResourceContext"; -import { ArrayElement } from "@server/types/ArrayElement"; -import { formatAxiosError } from "@app/lib/api/formatAxiosError"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { createApiClient } from "@app/lib/api"; -import { GetSiteResponse, ListSitesResponse } from "@server/routers/site"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionHeader, - SettingsSectionTitle, - SettingsSectionDescription, - SettingsSectionBody, - SettingsSectionForm -} from "@app/components/Settings"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { useRouter } from "next/navigation"; -import { isTargetValid } from "@server/lib/validators"; -import { tlsNameSchema } from "@server/lib/schemas"; -import { - CheckIcon, - ChevronsUpDown, - Settings, - Heart, - Check, - CircleCheck, - CircleX, - ArrowRight, - Plus, - MoveRight, - ArrowUp, - Info, - ArrowDown, - AlertTriangle -} from "lucide-react"; -import { ContainersSelector } from "@app/components/ContainersSelector"; -import { useTranslations } from "next-intl"; -import { build } from "@server/build"; -import HealthCheckDialog from "@/components/HealthCheckDialog"; -import { DockerManager, DockerState } from "@app/lib/docker"; -import { Container } from "@server/routers/site"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@app/components/ui/popover"; -import { cn } from "@app/lib/cn"; -import { CaretSortIcon } from "@radix-ui/react-icons"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList -} from "@app/components/ui/command"; -import { parseHostTarget } from "@app/lib/parseHostTarget"; -import { HeadersInput } from "@app/components/HeadersInput"; -import { - PathMatchDisplay, - PathMatchModal, - PathRewriteDisplay, - PathRewriteModal -} from "@app/components/PathMatchRenameModal"; -import { Badge } from "@app/components/ui/badge"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; -import { Alert, AlertDescription } from "@app/components/ui/alert"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useResourceContext } from "@app/hooks/useResourceContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient } from "@app/lib/api"; +import { formatAxiosError } from "@app/lib/api/formatAxiosError"; +import { cn } from "@app/lib/cn"; +import { DockerManager, DockerState } from "@app/lib/docker"; +import { parseHostTarget } from "@app/lib/parseHostTarget"; +import { orgQueries, resourceQueries } from "@app/lib/queries"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { CaretSortIcon } from "@radix-ui/react-icons"; +import { tlsNameSchema } from "@server/lib/schemas"; +import { isTargetValid } from "@server/lib/validators"; +import { CreateTargetResponse } from "@server/routers/target"; +import { ListTargetsResponse } from "@server/routers/target/listTargets"; +import { ArrayElement } from "@server/types/ArrayElement"; +import { useQuery } from "@tanstack/react-query"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable +} from "@tanstack/react-table"; +import { AxiosResponse } from "axios"; +import { + AlertTriangle, + CheckIcon, + CircleCheck, + CircleX, + Info, + Plus, + Settings +} from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { use, useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; const addTargetSchema = z .object({ ip: z.string().refine(isTargetValid), method: z.string().nullable(), port: z.coerce.number().int().positive(), - siteId: z.int() - .positive({ - error: "You must select a site for a target." - }), + siteId: z.int().positive({ + error: "You must select a site for a target." + }), path: z.string().optional().nullable(), pathMatchType: z .enum(["exact", "prefix", "regex"]) @@ -202,7 +192,7 @@ type LocalTarget = Omit< "protocol" >; -export default function ReverseProxyTargets(props: { +export default function ReverseProxyTargetsPage(props: { params: Promise<{ resourceId: number; orgId: string }>; }) { const params = use(props.params); @@ -213,9 +203,19 @@ export default function ReverseProxyTargets(props: { const api = createApiClient(useEnvContext()); + const { data: remoteTargets, isLoading: isLoadingTargets } = useQuery( + resourceQueries.resourceTargets({ + resourceId: resource.resourceId + }) + ); + const { data: sites = [], isLoading: isLoadingSites } = useQuery( + orgQueries.sites({ + orgId: params.orgId + }) + ); + const [targets, setTargets] = useState([]); const [targetsToRemove, setTargetsToRemove] = useState([]); - const [sites, setSites] = useState([]); const [dockerStates, setDockerStates] = useState>( new Map() ); @@ -259,8 +259,6 @@ export default function ReverseProxyTargets(props: { const [targetsLoading, setTargetsLoading] = useState(false); const [proxySettingsLoading, setProxySettingsLoading] = useState(false); - const [pageLoading, setPageLoading] = useState(true); - const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); const [isAdvancedMode, setIsAdvancedMode] = useState(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("proxy-advanced-mode"); @@ -313,10 +311,6 @@ export default function ReverseProxyTargets(props: { ) }); - type ProxySettingsValues = z.infer; - type TlsSettingsValues = z.infer; - type TargetsSettingsValues = z.infer; - const tlsSettingsForm = useForm({ resolver: zodResolver(tlsSettingsSchema), defaultValues: { @@ -343,86 +337,19 @@ export default function ReverseProxyTargets(props: { }); useEffect(() => { - const fetchTargets = async () => { - try { - const res = await api.get>( - `/resource/${resource.resourceId}/targets` - ); - - if (res.status === 200) { - setTargets(res.data.data.targets); - } - } catch (err) { - console.error(err); - toast({ - variant: "destructive", - title: t("targetErrorFetch"), - description: formatAxiosError( - err, - t("targetErrorFetchDescription") - ) - }); - } finally { - setPageLoading(false); + if (!isLoadingSites && sites) { + const newtSites = sites.filter((site) => site.type === "newt"); + for (const site of newtSites) { + initializeDockerForSite(site.siteId); } - }; - fetchTargets(); + } + }, [isLoadingSites, sites]); - const fetchSites = async () => { - const res = await api - .get< - AxiosResponse - >(`/org/${params.orgId}/sites`) - .catch((e) => { - toast({ - variant: "destructive", - title: t("sitesErrorFetch"), - description: formatAxiosError( - e, - t("sitesErrorFetchDescription") - ) - }); - }); - - if (res?.status === 200) { - setSites(res.data.data.sites); - - // Initialize Docker for newt sites - const newtSites = res.data.data.sites.filter( - (site) => site.type === "newt" - ); - for (const site of newtSites) { - initializeDockerForSite(site.siteId); - } - - // Sites loaded successfully - } - }; - fetchSites(); - - // const fetchSite = async () => { - // try { - // const res = await api.get>( - // `/site/${resource.siteId}` - // ); - // - // if (res.status === 200) { - // setSite(res.data.data); - // } - // } catch (err) { - // console.error(err); - // toast({ - // variant: "destructive", - // title: t("siteErrorFetch"), - // description: formatAxiosError( - // err, - // t("siteErrorFetchDescription") - // ) - // }); - // } - // }; - // fetchSite(); - }, []); + useEffect(() => { + if (!isLoadingTargets && remoteTargets) { + setTargets(remoteTargets); + } + }, [isLoadingTargets, remoteTargets]); // Save advanced mode preference to localStorage useEffect(() => { @@ -546,11 +473,11 @@ export default function ReverseProxyTargets(props: { prev.map((t) => t.targetId === target.targetId ? { - ...t, - targetId: response.data.data.targetId, - new: false, - updated: false - } + ...t, + targetId: response.data.data.targetId, + new: false, + updated: false + } : t ) ); @@ -607,16 +534,16 @@ export default function ReverseProxyTargets(props: { const newTarget: LocalTarget = { ...data, - path: isHttp ? (data.path || null) : null, - pathMatchType: isHttp ? (data.pathMatchType || null) : null, - rewritePath: isHttp ? (data.rewritePath || null) : null, - rewritePathType: isHttp ? (data.rewritePathType || null) : null, + path: isHttp ? data.path || null : null, + pathMatchType: isHttp ? data.pathMatchType || null : null, + rewritePath: isHttp ? data.rewritePath || null : null, + rewritePathType: isHttp ? data.rewritePathType || null : null, siteType: site?.type || null, enabled: true, targetId: new Date().getTime(), new: true, resourceId: resource.resourceId, - priority: isHttp ? (data.priority || 100) : 100, + priority: isHttp ? data.priority || 100 : 100, hcEnabled: false, hcPath: null, hcMethod: null, @@ -631,7 +558,7 @@ export default function ReverseProxyTargets(props: { hcStatus: null, hcMode: null, hcUnhealthyInterval: null, - hcTlsServerName: null, + hcTlsServerName: null }; setTargets([...targets, newTarget]); @@ -653,11 +580,11 @@ export default function ReverseProxyTargets(props: { targets.map((target) => target.targetId === targetId ? { - ...target, - ...data, - updated: true, - siteType: site ? site.type : target.siteType - } + ...target, + ...data, + updated: true, + siteType: site ? site.type : target.siteType + } : target ) ); @@ -668,10 +595,10 @@ export default function ReverseProxyTargets(props: { targets.map((target) => target.targetId === targetId ? { - ...target, - ...config, - updated: true - } + ...target, + ...config, + updated: true + } : target ) ); @@ -733,7 +660,7 @@ export default function ReverseProxyTargets(props: { hcStatus: target.hcStatus || null, hcUnhealthyInterval: target.hcUnhealthyInterval || null, hcMode: target.hcMode || null, - hcTlsServerName: target.hcTlsServerName, + hcTlsServerName: target.hcTlsServerName }; // Only include path-related fields for HTTP resources @@ -877,7 +804,7 @@ export default function ReverseProxyTargets(props: { const healthCheckColumn: ColumnDef = { accessorKey: "healthCheck", - header: () => ({t("healthCheck")}), + header: () => {t("healthCheck")}, cell: ({ row }) => { const status = row.original.hcHealth || "unknown"; const isEnabled = row.original.hcEnabled; @@ -949,7 +876,7 @@ export default function ReverseProxyTargets(props: { const matchPathColumn: ColumnDef = { accessorKey: "path", - header: () => ({t("matchPath")}), + header: () => {t("matchPath")}, cell: ({ row }) => { const hasPathMatch = !!( row.original.path || row.original.pathMatchType @@ -1011,7 +938,7 @@ export default function ReverseProxyTargets(props: { const addressColumn: ColumnDef = { accessorKey: "address", - header: () => ({t("address")}), + header: () => {t("address")}, cell: ({ row }) => { const selectedSite = sites.find( (site) => site.siteId === row.original.siteId @@ -1064,7 +991,7 @@ export default function ReverseProxyTargets(props: { className={cn( "w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent", !row.original.siteId && - "text-muted-foreground" + "text-muted-foreground" )} > @@ -1132,8 +1059,12 @@ export default function ReverseProxyTargets(props: { {row.original.method || "http"} - http - https + + http + + + https + h2c @@ -1225,7 +1156,7 @@ export default function ReverseProxyTargets(props: { const rewritePathColumn: ColumnDef = { accessorKey: "rewritePath", - header: () => ({t("rewritePath")}), + header: () => {t("rewritePath")}, cell: ({ row }) => { const hasRewritePath = !!( row.original.rewritePath || row.original.rewritePathType @@ -1295,7 +1226,7 @@ export default function ReverseProxyTargets(props: { const enabledColumn: ColumnDef = { accessorKey: "enabled", - header: () => ({t("enabled")}), + header: () => {t("enabled")}, cell: ({ row }) => (
= { id: "actions", - header: () => ({t("actions")}), + header: () => {t("actions")}, cell: ({ row }) => (
)} + + + + - {resource.http && ( - - - - {t("proxyAdditional")} - - - {t("proxyAdditionalDescription")} - - - - -
- - {!env.flags.usePangolinDns && ( - ( - - - { - field.onChange( - val - ); - }} - /> - - - )} - /> - )} - ( - - - {t("targetTlsSni")} - - - - - - {t( - "targetTlsSniDescription" - )} - - - - )} - /> - - -
- - -
- - ( - - - { - field.onChange(val); - }} - /> - - - )} - /> - - -
- - -
- - ( - - - {t("proxyCustomHeader")} - - - - - - {t( - "proxyCustomHeaderDescription" - )} - - - - )} - /> - ( - - - {t("customHeaders")} - - - { - field.onChange( - value - ); - }} - rows={4} - /> - - - {t( - "customHeadersDescription" - )} - - - - )} - /> - - -
-
-
- )} - - {!resource.http && resource.protocol == "tcp" && ( - - - - {t("proxyProtocol")} - - - {t("proxyProtocolDescription")} - - - - -
- - ( - - - { - field.onChange(val); - }} - /> - - - )} - /> - - {proxySettingsForm.watch( - "proxyProtocol" - ) && ( - <> - ( - - - {t( - "proxyProtocolVersion" - )} - - - - - - {t( - "versionDescription" - )} - - - )} - /> - - - - - - {t("warning")}: - {" "} - {t("proxyProtocolWarning")} - - - - )} - - -
-
-
- )} - -
- -
- {selectedTargetForHealthCheck && ( )} - + ); } -function isIPInSubnet(subnet: string, ip: string): boolean { - const [subnetIP, maskBits] = subnet.split("/"); - const mask = parseInt(maskBits); +function ProxyResourceHttpForm({ + resource, + updateResource +}: Pick) { + const t = useTranslations(); - if (mask < 0 || mask > 32) { - throw new Error("subnetMaskErrorInvalid"); - } + const tlsSettingsSchema = z.object({ + ssl: z.boolean(), + tlsServerName: z + .string() + .optional() + .refine( + (data) => { + if (data) { + return tlsNameSchema.safeParse(data).success; + } + return true; + }, + { + message: t("proxyErrorTls") + } + ) + }); - // Convert IP addresses to binary numbers - const subnetNum = ipToNumber(subnetIP); - const ipNum = ipToNumber(ip); - - // Calculate subnet mask - const maskNum = mask === 32 ? -1 : ~((1 << (32 - mask)) - 1); - - // Check if the IP is in the subnet - return (subnetNum & maskNum) === (ipNum & maskNum); -} - -function ipToNumber(ip: string): number { - // Validate IP address format - const parts = ip.split("."); - - if (parts.length !== 4) { - throw new Error("ipAddressErrorInvalidFormat"); - } - - // Convert IP octets to 32-bit number - return parts.reduce((num, octet) => { - const oct = parseInt(octet); - if (isNaN(oct) || oct < 0 || oct > 255) { - throw new Error("ipAddressErrorInvalidOctet"); + const tlsSettingsForm = useForm({ + resolver: zodResolver(tlsSettingsSchema), + defaultValues: { + ssl: resource.ssl, + tlsServerName: resource.tlsServerName || "" } - return (num << 8) + oct; - }, 0); + }); + + const proxySettingsSchema = z.object({ + setHostHeader: z + .string() + .optional() + .refine( + (data) => { + if (data) { + return tlsNameSchema.safeParse(data).success; + } + return true; + }, + { + message: t("proxyErrorInvalidHeader") + } + ), + headers: z + .array(z.object({ name: z.string(), value: z.string() })) + .nullable(), + proxyProtocol: z.boolean().optional(), + proxyProtocolVersion: z.int().min(1).max(2).optional() + }); + + const proxySettingsForm = useForm({ + resolver: zodResolver(proxySettingsSchema), + defaultValues: { + setHostHeader: resource.setHostHeader || "", + headers: resource.headers, + proxyProtocol: resource.proxyProtocol || false, + proxyProtocolVersion: resource.proxyProtocolVersion || 1 + } + }); + + const { env } = useEnvContext(); + const api = createApiClient({ env }); + + const targetsSettingsForm = useForm({ + resolver: zodResolver(targetsSettingsSchema), + defaultValues: { + stickySession: resource.stickySession + } + }); + + const router = useRouter(); + const [, formAction, isSubmitting] = useActionState( + saveResourceHttpSettings, + null + ); + + async function saveResourceHttpSettings() { + const isValidTLS = await tlsSettingsForm.trigger(); + const isValidProxy = await proxySettingsForm.trigger(); + const targetSettingsForm = await targetsSettingsForm.trigger(); + if (!isValidTLS || !isValidProxy || !targetSettingsForm) return; + + try { + // Gather all settings + const stickySessionData = targetsSettingsForm.getValues(); + const tlsData = tlsSettingsForm.getValues(); + const proxyData = proxySettingsForm.getValues(); + + // Combine into one payload + const payload = { + stickySession: stickySessionData.stickySession, + ssl: tlsData.ssl, + tlsServerName: tlsData.tlsServerName || null, + setHostHeader: proxyData.setHostHeader || null, + headers: proxyData.headers || null + }; + + // Single API call to update all settings + await api.post(`/resource/${resource.resourceId}`, payload); + + // Update local resource context + updateResource({ + ...resource, + stickySession: stickySessionData.stickySession, + ssl: tlsData.ssl, + tlsServerName: tlsData.tlsServerName || null, + setHostHeader: proxyData.setHostHeader || null, + headers: proxyData.headers || null + }); + + toast({ + title: t("settingsUpdated"), + description: t("settingsUpdatedDescription") + }); + + router.refresh(); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: t("settingsErrorUpdate"), + description: formatAxiosError( + err, + t("settingsErrorUpdateDescription") + ) + }); + } + } + + return ( + + + + {t("proxyAdditional")} + + + {t("proxyAdditionalDescription")} + + + + +
+ + {!env.flags.usePangolinDns && ( + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + )} + ( + + + {t("targetTlsSni")} + + + + + + {t("targetTlsSniDescription")} + + + + )} + /> + + +
+ + +
+ + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + + +
+ + +
+ + ( + + + {t("proxyCustomHeader")} + + + + + + {t("proxyCustomHeaderDescription")} + + + + )} + /> + ( + + + {t("customHeaders")} + + + { + field.onChange(value); + }} + rows={4} + /> + + + {t("customHeadersDescription")} + + + + )} + /> + + +
+
+ +
+
+
+ ); +} + +function ProxyResourceProtocolForm({ + resource, + updateResource +}: Pick) { + const t = useTranslations(); + + const api = createApiClient(useEnvContext()); + + const proxySettingsSchema = z.object({ + setHostHeader: z + .string() + .optional() + .refine( + (data) => { + if (data) { + return tlsNameSchema.safeParse(data).success; + } + return true; + }, + { + message: t("proxyErrorInvalidHeader") + } + ), + headers: z + .array(z.object({ name: z.string(), value: z.string() })) + .nullable(), + proxyProtocol: z.boolean().optional(), + proxyProtocolVersion: z.int().min(1).max(2).optional() + }); + + const proxySettingsForm = useForm({ + resolver: zodResolver(proxySettingsSchema), + defaultValues: { + setHostHeader: resource.setHostHeader || "", + headers: resource.headers, + proxyProtocol: resource.proxyProtocol || false, + proxyProtocolVersion: resource.proxyProtocolVersion || 1 + } + }); + + const router = useRouter(); + + const [, formAction, isSubmitting] = useActionState( + saveProtocolSettings, + null + ); + + async function saveProtocolSettings() { + const isValid = proxySettingsForm.trigger(); + if (!isValid) return; + + try { + // For TCP/UDP resources, save proxy protocol settings + const proxyData = proxySettingsForm.getValues(); + + const payload = { + proxyProtocol: proxyData.proxyProtocol || false, + proxyProtocolVersion: proxyData.proxyProtocolVersion || 1 + }; + + await api.post(`/resource/${resource.resourceId}`, payload); + + updateResource({ + ...resource, + proxyProtocol: proxyData.proxyProtocol || false, + proxyProtocolVersion: proxyData.proxyProtocolVersion || 1 + }); + + toast({ + title: t("settingsUpdated"), + description: t("settingsUpdatedDescription") + }); + + router.refresh(); + } catch (err) { + console.error(err); + toast({ + variant: "destructive", + title: t("settingsErrorUpdate"), + description: formatAxiosError( + err, + t("settingsErrorUpdateDescription") + ) + }); + } + } + + return ( + + + + {t("proxyProtocol")} + + + {t("proxyProtocolDescription")} + + + + +
+ + ( + + + { + field.onChange(val); + }} + /> + + + )} + /> + + {proxySettingsForm.watch("proxyProtocol") && ( + <> + ( + + + {t("proxyProtocolVersion")} + + + + + + {t("versionDescription")} + + + )} + /> + + + + + {t("warning")}:{" "} + {t("proxyProtocolWarning")} + + + + )} + + +
+
+ +
+
+
+ ); } diff --git a/src/contexts/resourceContext.ts b/src/contexts/resourceContext.ts index bb5501a6..84fa31ac 100644 --- a/src/contexts/resourceContext.ts +++ b/src/contexts/resourceContext.ts @@ -2,7 +2,7 @@ import { GetResourceAuthInfoResponse } from "@server/routers/resource"; import { GetResourceResponse } from "@server/routers/resource/getResource"; import { createContext } from "react"; -interface ResourceContextType { +export interface ResourceContextType { resource: GetResourceResponse; authInfo: GetResourceAuthInfoResponse; updateResource: (updatedResource: Partial) => void; From 72bc26f0f84c08a3ee98f56fe15e2df05e8531c0 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 6 Dec 2025 01:14:15 +0100 Subject: [PATCH 051/153] =?UTF-8?q?=F0=9F=92=AC=20update=20texts=20to=20be?= =?UTF-8?q?=20more=20specific?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 3 +++ .../settings/resources/proxy/[niceId]/proxy/page.tsx | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 3f20f7e6..0d5e80ea 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1308,6 +1308,9 @@ "accountSetupSuccess": "Account setup completed! Welcome to Pangolin!", "documentation": "Documentation", "saveAllSettings": "Save All Settings", + "saveResourceTargets": "Save Targets", + "saveResourceHttp": "Save Additional fields", + "saveProxyProtocol": "Save Proxy protocol settings", "settingsUpdated": "Settings updated", "settingsUpdatedDescription": "Settings updated successfully", "settingsErrorUpdate": "Failed to update settings", diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx index f9c8c8c2..8cee3da0 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx @@ -1138,7 +1138,7 @@ function ProxyResourceTargetsForm({ loading={isSubmitting} type="submit" > - {t("save")} + {t("saveResourceTargets")} @@ -1486,7 +1486,7 @@ function ProxyResourceHttpForm({ loading={isSubmitting} type="submit" > - {t("save")} + {t("saveResourceHttp")} @@ -1687,7 +1687,7 @@ function ProxyResourceProtocolForm({ loading={isSubmitting} type="submit" > - {t("save")} + {t("saveProxyProtocol")} From 78369b6f6adfda90aed1d91b525970973c82a700 Mon Sep 17 00:00:00 2001 From: David Reed Date: Wed, 10 Dec 2025 11:13:04 -0800 Subject: [PATCH 052/153] Add OIDC authentication error response support --- server/routers/idp/validateOidcCallback.ts | 70 ++++++++- .../auth/idp/[idpId]/oidc/callback/page.tsx | 16 ++- src/components/ValidateOidcToken.tsx | 136 +++++++++++++++--- 3 files changed, 192 insertions(+), 30 deletions(-) diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index f6b21ff6..bd633707 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -192,11 +192,71 @@ export async function validateOidcCallback( state }); - const tokens = await client.validateAuthorizationCode( - ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl), - code, - codeVerifier - ); + let tokens: arctic.OAuth2Tokens; + try { + tokens = await client.validateAuthorizationCode( + ensureTrailingSlash(existingIdp.idpOidcConfig.tokenUrl), + code, + codeVerifier + ); + } catch (err: unknown) { + if (err instanceof arctic.OAuth2RequestError) { + logger.warn("OIDC provider rejected the authorization code", { + error: err.code, + description: err.description, + uri: err.uri, + state: err.state + }); + return next( + createHttpError( + HttpCode.UNAUTHORIZED, + err.description || + `OIDC provider rejected the request (${err.code})` + ) + ); + } + + if (err instanceof arctic.UnexpectedResponseError) { + logger.error( + "OIDC provider returned an unexpected response during token exchange", + { status: err.status } + ); + return next( + createHttpError( + HttpCode.BAD_GATEWAY, + "Received an unexpected response from the identity provider while exchanging the authorization code." + ) + ); + } + + if (err instanceof arctic.UnexpectedErrorResponseBodyError) { + logger.error( + "OIDC provider returned an unexpected error payload during token exchange", + { status: err.status, data: err.data } + ); + return next( + createHttpError( + HttpCode.BAD_GATEWAY, + "Identity provider returned an unexpected error payload while exchanging the authorization code." + ) + ); + } + + if (err instanceof arctic.ArcticFetchError) { + logger.error( + "Failed to reach OIDC provider while exchanging authorization code", + { error: err.message } + ); + return next( + createHttpError( + HttpCode.BAD_GATEWAY, + "Unable to reach the identity provider while exchanging the authorization code. Please try again." + ) + ); + } + + throw err; + } const idToken = tokens.idToken(); logger.debug("ID token", { idToken }); diff --git a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx index a2432e3e..811f4402 100644 --- a/src/app/auth/idp/[idpId]/oidc/callback/page.tsx +++ b/src/app/auth/idp/[idpId]/oidc/callback/page.tsx @@ -14,8 +14,11 @@ export const dynamic = "force-dynamic"; export default async function Page(props: { params: Promise<{ orgId: string; idpId: string }>; searchParams: Promise<{ - code: string; - state: string; + code?: string; + state?: string; + error?: string; + error_description?: string; + error_uri?: string; }>; }) { const params = await props.params; @@ -59,6 +62,14 @@ export default async function Page(props: { } } + const providerError = searchParams.error + ? { + error: searchParams.error, + description: searchParams.error_description, + uri: searchParams.error_uri + } + : undefined; + return ( <> ); diff --git a/src/components/ValidateOidcToken.tsx b/src/components/ValidateOidcToken.tsx index d4d9678d..900a99ac 100644 --- a/src/components/ValidateOidcToken.tsx +++ b/src/components/ValidateOidcToken.tsx @@ -26,6 +26,11 @@ type ValidateOidcTokenParams = { stateCookie: string | undefined; idp: { name: string }; loginPageId?: number; + providerError?: { + error: string; + description?: string | null; + uri?: string | null; + }; }; export default function ValidateOidcToken(props: ValidateOidcTokenParams) { @@ -35,14 +40,65 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [isProviderError, setIsProviderError] = useState(false); const { licenseStatus, isLicenseViolation } = useLicenseStatusContext(); const t = useTranslations(); useEffect(() => { - async function validate() { + let isCancelled = false; + + async function runValidation() { setLoading(true); + setIsProviderError(false); + + if (props.providerError?.error) { + const providerMessage = + props.providerError.description || + t("idpErrorOidcProviderRejected", { + error: props.providerError.error, + defaultValue: + "The identity provider returned an error: {error}." + }); + const suffix = props.providerError.uri + ? ` (${props.providerError.uri})` + : ""; + if (!isCancelled) { + setIsProviderError(true); + setError(`${providerMessage}${suffix}`); + setLoading(false); + } + return; + } + + if (!props.code) { + if (!isCancelled) { + setIsProviderError(false); + setError( + t("idpErrorOidcMissingCode", { + defaultValue: + "The identity provider did not return an authorization code." + }) + ); + setLoading(false); + } + return; + } + + if (!props.expectedState || !props.stateCookie) { + if (!isCancelled) { + setIsProviderError(false); + setError( + t("idpErrorOidcMissingState", { + defaultValue: + "The login request is missing state information. Please restart the login process." + }) + ); + setLoading(false); + } + return; + } console.log(t("idpOidcTokenValidating"), { code: props.code, @@ -57,22 +113,28 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { try { const response = await validateOidcUrlCallbackProxy( props.idpId, - props.code || "", - props.expectedState || "", - props.stateCookie || "", + props.code, + props.expectedState, + props.stateCookie, props.loginPageId ); if (response.error) { - setError(response.message); - setLoading(false); + if (!isCancelled) { + setIsProviderError(false); + setError(response.message); + setLoading(false); + } return; } const data = response.data; if (!data) { - setError("Unable to validate OIDC token"); - setLoading(false); + if (!isCancelled) { + setIsProviderError(false); + setError("Unable to validate OIDC token"); + setLoading(false); + } return; } @@ -82,8 +144,11 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { router.push(env.app.dashboardUrl); } - setLoading(false); - await new Promise((resolve) => setTimeout(resolve, 100)); + if (!isCancelled) { + setIsProviderError(false); + setLoading(false); + await new Promise((resolve) => setTimeout(resolve, 100)); + } if (redirectUrl.startsWith("http")) { window.location.href = data.redirectUrl; // this is validated by the parent using this component @@ -92,18 +157,39 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { } } catch (e: any) { console.error(e); - setError( - t("idpErrorOidcTokenValidating", { - defaultValue: "An unexpected error occurred. Please try again." - }) - ); + if (!isCancelled) { + setIsProviderError(false); + setError( + t("idpErrorOidcTokenValidating", { + defaultValue: + "An unexpected error occurred. Please try again." + }) + ); + } } finally { - setLoading(false); + if (!isCancelled) { + setLoading(false); + } } } - validate(); - }, []); + runValidation(); + + return () => { + isCancelled = true; + }; + }, [ + env.app.dashboardUrl, + isLicenseViolation, + props.code, + props.expectedState, + props.idpId, + props.loginPageId, + props.providerError, + props.stateCookie, + router, + t + ]); return (
@@ -133,12 +219,16 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { - - {t("idpErrorConnectingTo", { - name: props.idp.name - })} + + {isProviderError + ? error + : t("idpErrorConnectingTo", { + name: props.idp.name + })} - {error} + {!isProviderError && ( + {error} + )} )} From ce6b609ca2ce1d0115bd4564742450230452e5a3 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 10 Dec 2025 21:15:26 +0100 Subject: [PATCH 053/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20domain=20?= =?UTF-8?q?picker=20component=20to=20accept=20default=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/DomainPicker.tsx | 252 ++++++++++++++------------------ src/lib/queries.ts | 12 ++ 2 files changed, 118 insertions(+), 146 deletions(-) diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 2d17e39f..8fc6d583 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -1,8 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; +import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Command, @@ -13,45 +11,40 @@ import { CommandList, CommandSeparator } from "@/components/ui/command"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { - AlertCircle, - CheckCircle2, - Building2, - Zap, - Check, - ChevronsUpDown, - ArrowUpDown -} from "lucide-react"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { createApiClient, formatAxiosError } from "@/lib/api"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { useEnvContext } from "@/hooks/useEnvContext"; import { toast } from "@/hooks/useToast"; -import { ListDomainsResponse } from "@server/routers/domain/listDomains"; -import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types"; -import { AxiosResponse } from "axios"; +import { createApiClient } from "@/lib/api"; import { cn } from "@/lib/cn"; -import { useTranslations } from "next-intl"; -import { build } from "@server/build"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { - sanitizeInputRaw, finalizeSubdomainSanitize, - validateByDomainType, - isValidSubdomainStructure + isValidSubdomainStructure, + sanitizeInputRaw, + validateByDomainType } from "@/lib/subdomain-utils"; +import { orgQueries } from "@app/lib/queries"; +import { build } from "@server/build"; +import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types"; +import { useQuery } from "@tanstack/react-query"; +import { AxiosResponse } from "axios"; +import { + AlertCircle, + Building2, + Check, + CheckCircle2, + ChevronsUpDown, + Zap +} from "lucide-react"; +import { useTranslations } from "next-intl"; import { toUnicode } from "punycode"; - -type OrganizationDomain = { - domainId: string; - baseDomain: string; - verified: boolean; - type: "ns" | "cname" | "wildcard"; -}; +import { useCallback, useEffect, useMemo, useState } from "react"; type AvailableOption = { domainNamespaceId: string; @@ -69,7 +62,7 @@ type DomainOption = { domainNamespaceId?: string; }; -interface DomainPicker2Props { +interface DomainPickerProps { orgId: string; onDomainChange?: (domainInfo: { domainId: string; @@ -81,116 +74,115 @@ interface DomainPicker2Props { }) => void; cols?: number; hideFreeDomain?: boolean; + defaultSubdomain?: string; + defaultBaseDomain?: string; } -export default function DomainPicker2({ +export default function DomainPicker({ orgId, onDomainChange, cols = 2, - hideFreeDomain = false -}: DomainPicker2Props) { + hideFreeDomain = false, + defaultSubdomain, + defaultBaseDomain +}: DomainPickerProps) { const { env } = useEnvContext(); const api = createApiClient({ env }); const t = useTranslations(); + const { data = [], isLoading: loadingDomains } = useQuery( + orgQueries.domains({ orgId }) + ); + + console.log({ defaultSubdomain, defaultBaseDomain }); + if (!env.flags.usePangolinDns) { hideFreeDomain = true; } - const [subdomainInput, setSubdomainInput] = useState(""); + const [subdomainInput, setSubdomainInput] = useState( + defaultSubdomain ?? "" + ); const [selectedBaseDomain, setSelectedBaseDomain] = useState(null); const [availableOptions, setAvailableOptions] = useState( [] ); - const [organizationDomains, setOrganizationDomains] = useState< - OrganizationDomain[] - >([]); - const [loadingDomains, setLoadingDomains] = useState(false); + + // memoized to prevent reruning the effect that selects the initial domain indefinitely + // removing this will break and cause an infinite rerender + const organizationDomains = useMemo(() => { + return data + .filter( + (domain) => + domain.type === "ns" || + domain.type === "cname" || + domain.type === "wildcard" + ) + .map((domain) => ({ + ...domain, + baseDomain: toUnicode(domain.baseDomain), + type: domain.type as "ns" | "cname" | "wildcard" + })); + }, [data]); + const [open, setOpen] = useState(false); // Provided domain search states const [userInput, setUserInput] = useState(""); const [isChecking, setIsChecking] = useState(false); - const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [providedDomainsShown, setProvidedDomainsShown] = useState(3); const [selectedProvidedDomain, setSelectedProvidedDomain] = useState(null); useEffect(() => { - const loadOrganizationDomains = async () => { - setLoadingDomains(true); - try { - const response = await api.get< - AxiosResponse - >(`/org/${orgId}/domains`); - if (response.status === 200) { - const domains = response.data.data.domains - .filter( - (domain) => - domain.type === "ns" || - domain.type === "cname" || - domain.type === "wildcard" - ) - .map((domain) => ({ - ...domain, - baseDomain: toUnicode(domain.baseDomain), - type: domain.type as "ns" | "cname" | "wildcard" - })); - setOrganizationDomains(domains); + if (!loadingDomains) { + if (organizationDomains.length > 0) { + // Select the first organization domain or the one provided from props + const firstOrgDomain = + organizationDomains.find( + (domain) => domain.baseDomain === defaultBaseDomain + ) ?? organizationDomains[0]; + const domainOption: DomainOption = { + id: `org-${firstOrgDomain.domainId}`, + domain: firstOrgDomain.baseDomain, + type: "organization", + verified: firstOrgDomain.verified, + domainType: firstOrgDomain.type, + domainId: firstOrgDomain.domainId + }; + setSelectedBaseDomain(domainOption); - // Auto-select first available domain - if (domains.length > 0) { - // Select the first organization domain - const firstOrgDomain = domains[0]; - const domainOption: DomainOption = { - id: `org-${firstOrgDomain.domainId}`, - domain: firstOrgDomain.baseDomain, - type: "organization", - verified: firstOrgDomain.verified, - domainType: firstOrgDomain.type, - domainId: firstOrgDomain.domainId - }; - setSelectedBaseDomain(domainOption); - - onDomainChange?.({ - domainId: firstOrgDomain.domainId, - type: "organization", - subdomain: undefined, - fullDomain: firstOrgDomain.baseDomain, - baseDomain: firstOrgDomain.baseDomain - }); - } else if ( - (build === "saas" || build === "enterprise") && - !hideFreeDomain - ) { - // If no organization domains, select the provided domain option - const domainOptionText = - build === "enterprise" - ? t("domainPickerProvidedDomain") - : t("domainPickerFreeProvidedDomain"); - const freeDomainOption: DomainOption = { - id: "provided-search", - domain: domainOptionText, - type: "provided-search" - }; - setSelectedBaseDomain(freeDomainOption); - } - } - } catch (error) { - console.error("Failed to load organization domains:", error); - toast({ - variant: "destructive", - title: t("domainPickerError"), - description: t("domainPickerErrorLoadDomains") + onDomainChange?.({ + domainId: firstOrgDomain.domainId, + type: "organization", + subdomain: undefined, + fullDomain: firstOrgDomain.baseDomain, + baseDomain: firstOrgDomain.baseDomain }); - } finally { - setLoadingDomains(false); + } else if ( + (build === "saas" || build === "enterprise") && + !hideFreeDomain + ) { + // If no organization domains, select the provided domain option + const domainOptionText = + build === "enterprise" + ? t("domainPickerProvidedDomain") + : t("domainPickerFreeProvidedDomain"); + const freeDomainOption: DomainOption = { + id: "provided-search", + domain: domainOptionText, + type: "provided-search" + }; + setSelectedBaseDomain(freeDomainOption); } - }; - - loadOrganizationDomains(); - }, [orgId, api, hideFreeDomain]); + } + }, [ + hideFreeDomain, + loadingDomains, + organizationDomains, + defaultBaseDomain + ]); const checkAvailability = useCallback( async (input: string) => { @@ -256,37 +248,6 @@ export default function DomainPicker2({ } }, [userInput, debouncedCheckAvailability, selectedBaseDomain]); - const generateDropdownOptions = (): DomainOption[] => { - const options: DomainOption[] = []; - - organizationDomains.forEach((orgDomain) => { - options.push({ - id: `org-${orgDomain.domainId}`, - domain: orgDomain.baseDomain, - type: "organization", - verified: orgDomain.verified, - domainType: orgDomain.type, - domainId: orgDomain.domainId - }); - }); - - if ((build === "saas" || build === "enterprise") && !hideFreeDomain) { - const domainOptionText = - build === "enterprise" - ? t("domainPickerProvidedDomain") - : t("domainPickerFreeProvidedDomain"); - options.push({ - id: "provided-search", - domain: domainOptionText, - type: "provided-search" - }); - } - - return options; - }; - - const dropdownOptions = generateDropdownOptions(); - const finalizeSubdomain = (sub: string, base: DomainOption): string => { const sanitized = finalizeSubdomainSanitize(sub); @@ -440,8 +401,7 @@ export default function DomainPicker2({ selectedBaseDomain?.type === "provided-search"; const sortedAvailableOptions = [...availableOptions].sort((a, b) => { - const comparison = a.fullDomain.localeCompare(b.fullDomain); - return sortOrder === "asc" ? comparison : -comparison; + return a.fullDomain.localeCompare(b.fullDomain); }); const displayedProvidedOptions = sortedAvailableOptions.slice( @@ -518,16 +478,16 @@ export default function DomainPicker2({ className="w-full justify-between" > {selectedBaseDomain ? ( -
+
{selectedBaseDomain.type === "organization" ? null : ( - + )} {selectedBaseDomain.domain} {selectedBaseDomain.verified && ( - + )}
) : ( diff --git a/src/lib/queries.ts b/src/lib/queries.ts index de3bf023..9e43adf7 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -17,6 +17,7 @@ import { durationToMs } from "./durationToMs"; import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs"; import type { ListResourceNamesResponse } from "@server/routers/resource"; import type { ListTargetsResponse } from "@server/routers/target"; +import type { ListDomainsResponse } from "@server/routers/domain"; export type ProductUpdate = { link: string | null; @@ -140,6 +141,17 @@ export const orgQueries = { >(`/org/${orgId}/sites`, { signal }); return res.data.data.sites; } + }), + + domains: ({ orgId }: { orgId: string }) => + queryOptions({ + queryKey: ["ORG", orgId, "DOMAINS"] as const, + queryFn: async ({ signal, meta }) => { + const res = await meta!.api.get< + AxiosResponse + >(`/org/${orgId}/domains`, { signal }); + return res.data.data.domains; + } }) }; From 4e842a660a37874cc9770ef1ac157c031b0f6070 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 10 Dec 2025 21:15:42 +0100 Subject: [PATCH 054/153] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20refactor=20proxy?= =?UTF-8?q?=20resource=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/proxy/[niceId]/general/page.tsx | 668 ++++++++---------- .../resources/proxy/[niceId]/layout.tsx | 3 +- 2 files changed, 286 insertions(+), 385 deletions(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx index fa9a6976..6712f201 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx @@ -1,8 +1,5 @@ "use client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { formatAxiosError } from "@app/lib/api"; import { Button } from "@/components/ui/button"; import { Form, @@ -15,31 +12,10 @@ import { } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { useResourceContext } from "@app/hooks/useResourceContext"; -import { ListSitesResponse } from "@server/routers/site"; -import { useEffect, useState } from "react"; -import { AxiosResponse } from "axios"; -import { useParams, useRouter } from "next/navigation"; -import { useForm } from "react-hook-form"; -import { toast } from "@app/hooks/useToast"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionHeader, - SettingsSectionTitle, - SettingsSectionDescription, - SettingsSectionBody, - SettingsSectionForm, - SettingsSectionFooter -} from "@app/components/Settings"; -import { useOrgContext } from "@app/hooks/useOrgContext"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { Label } from "@app/components/ui/label"; -import { ListDomainsResponse } from "@server/routers/domain"; -import { UpdateResourceResponse } from "@server/routers/resource"; -import { SwitchInput } from "@app/components/SwitchInput"; -import { useTranslations } from "next-intl"; -import { Checkbox } from "@app/components/ui/checkbox"; +import { formatAxiosError } from "@app/lib/api"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + import { Credenza, CredenzaBody, @@ -51,26 +27,37 @@ import { CredenzaTitle } from "@app/components/Credenza"; import DomainPicker from "@app/components/DomainPicker"; -import { Globe } from "lucide-react"; -import { build } from "@server/build"; +import { + SettingsContainer, + SettingsSection, + SettingsSectionBody, + SettingsSectionDescription, + SettingsSectionFooter, + SettingsSectionForm, + SettingsSectionHeader, + SettingsSectionTitle +} from "@app/components/Settings"; +import { SwitchInput } from "@app/components/SwitchInput"; +import { Label } from "@app/components/ui/label"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient } from "@app/lib/api"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; -import { DomainRow } from "@app/components/DomainsTable"; +import { UpdateResourceResponse } from "@server/routers/resource"; +import { AxiosResponse } from "axios"; +import { Globe } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useParams, useRouter } from "next/navigation"; import { toASCII, toUnicode } from "punycode"; -import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; -import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; -import { useUserContext } from "@app/hooks/useUserContext"; +import { useActionState, useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; export default function GeneralForm() { - const [formKey, setFormKey] = useState(0); const params = useParams(); const { resource, updateResource } = useResourceContext(); - const { org } = useOrgContext(); const router = useRouter(); const t = useTranslations(); const [editDomainOpen, setEditDomainOpen] = useState(false); - const { licenseStatus } = useLicenseStatusContext(); - const subscriptionStatus = useSubscriptionStatusContext(); - const { user } = useUserContext(); const { env } = useEnvContext(); @@ -78,18 +65,30 @@ export default function GeneralForm() { const api = createApiClient({ env }); - const [sites, setSites] = useState([]); - const [saveLoading, setSaveLoading] = useState(false); - const [transferLoading, setTransferLoading] = useState(false); - const [open, setOpen] = useState(false); - const [baseDomains, setBaseDomains] = useState< - ListDomainsResponse["domains"] - >([]); - - const [loadingPage, setLoadingPage] = useState(true); const [resourceFullDomain, setResourceFullDomain] = useState( `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}` ); + + console.log({ resource }); + + const [defaultSubdomain, defaultBaseDomain] = useMemo(() => { + const resourceUrl = new URL(resourceFullDomain); + const domain = resourceUrl.hostname; + + const allDomainParts = domain.split("."); + let sub = undefined; + let base = domain; + + if (allDomainParts.length >= 3) { + // 3 parts: [subdomain, domain, tld] + const [first, ...rest] = allDomainParts; + sub = first; + base = rest.join("."); + } + + return [sub, base]; + }, [resourceFullDomain]); + const [selectedDomain, setSelectedDomain] = useState<{ domainId: string; subdomain?: string; @@ -105,7 +104,6 @@ export default function GeneralForm() { niceId: z.string().min(1).max(255).optional(), domainId: z.string().optional(), proxyPort: z.int().min(1).max(65535).optional() - // enableProxy: z.boolean().optional() }) .refine( (data) => { @@ -124,8 +122,6 @@ export default function GeneralForm() { } ); - type GeneralFormValues = z.infer; - const form = useForm({ resolver: zodResolver(GeneralFormSchema), defaultValues: { @@ -135,58 +131,17 @@ export default function GeneralForm() { subdomain: resource.subdomain ? resource.subdomain : undefined, domainId: resource.domainId || undefined, proxyPort: resource.proxyPort || undefined - // enableProxy: resource.enableProxy || false }, mode: "onChange" }); - useEffect(() => { - const fetchSites = async () => { - const res = await api.get>( - `/org/${orgId}/sites/` - ); - setSites(res.data.data.sites); - }; + const [, formAction, saveLoading] = useActionState(onSubmit, null); - const fetchDomains = async () => { - const res = await api - .get< - AxiosResponse - >(`/org/${orgId}/domains/`) - .catch((e) => { - toast({ - variant: "destructive", - title: t("domainErrorFetch"), - description: formatAxiosError( - e, - t("domainErrorFetchDescription") - ) - }); - }); + async function onSubmit() { + const isValid = await form.trigger(); + if (!isValid) return; - if (res?.status === 200) { - const rawDomains = res.data.data.domains as DomainRow[]; - const domains = rawDomains.map((domain) => ({ - ...domain, - baseDomain: toUnicode(domain.baseDomain) - })); - setBaseDomains(domains); - setFormKey((key) => key + 1); - } - }; - - const load = async () => { - await fetchDomains(); - await fetchSites(); - - setLoadingPage(false); - }; - - load(); - }, []); - - async function onSubmit(data: GeneralFormValues) { - setSaveLoading(true); + const data = form.getValues(); const res = await api .post>( @@ -200,9 +155,6 @@ export default function GeneralForm() { : undefined, domainId: data.domainId, proxyPort: data.proxyPort - // ...(!resource.http && { - // enableProxy: data.enableProxy - // }) } ) .catch((e) => { @@ -226,9 +178,6 @@ export default function GeneralForm() { subdomain: data.subdomain, fullDomain: resource.fullDomain, proxyPort: data.proxyPort - // ...(!resource.http && { - // enableProxy: data.enableProxy - // }) }); toast({ @@ -240,306 +189,257 @@ export default function GeneralForm() { router.replace( `/${updated.orgId}/settings/resources/proxy/${data.niceId}/general` ); - } else { - router.refresh(); } - setSaveLoading(false); + router.refresh(); } - - setSaveLoading(false); } return ( - !loadingPage && ( - <> - - - - - {t("resourceGeneral")} - - - {t("resourceGeneralDescription")} - - + <> + + + + + {t("resourceGeneral")} + + + {t("resourceGeneralDescription")} + + - - -
- - ( - -
- - + + + + ( + +
+ + + form.setValue( + "enabled", val - ) => - form.setValue( - "enabled", - val + ) + } + /> + +
+ +
+ )} + /> + + ( + + + {t("name")} + + + + + + + )} + /> + + ( + + + {t("identifier")} + + + + + + + )} + /> + + {!resource.http && ( + <> + ( + + + {t( + "resourcePortNumber" + )} + + + + field.onChange( + e.target + .value + ? parseInt( + e + .target + .value + ) + : undefined ) } /> -
- -
- )} - /> - - ( - - - {t("name")} - - - - - - - )} - /> - - ( - - - {t("identifier")} - - - + + {t( + "resourcePortNumberDescription" )} - className="flex-1" - /> - - - - )} - /> + + + )} + /> + + )} - {!resource.http && ( - <> - ( - - - {t( - "resourcePortNumber" - )} - - - - field.onChange( - e - .target - .value - ? parseInt( - e - .target - .value - ) - : undefined - ) - } - /> - - - - {t( - "resourcePortNumberDescription" - )} - - - )} - /> - - {/* {build == "oss" && ( - ( - - - - -
- - {t( - "resourceEnableProxy" - )} - - - {t( - "resourceEnableProxyDescription" - )} - -
-
- )} - /> - )} */} - - )} - - {resource.http && ( -
- -
- - - {resourceFullDomain} - - -
+ {resource.http && ( +
+ +
+ + + {resourceFullDomain} + +
- )} - - - - +
+ )} + + + + - - - - - + + + + + - setEditDomainOpen(setOpen)} - > - - - Edit Domain - - Select a domain for your resource - - - - { - const selected = { - domainId: res.domainId, - subdomain: res.subdomain, - fullDomain: res.fullDomain, - baseDomain: res.baseDomain - }; - setSelectedDomain(selected); - }} - /> - - - - - - + + - - - - - ) + setEditDomainOpen(false); + } + }} + > + Select Domain + + + + + ); } diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx index c453b577..f410b4c8 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/layout.tsx @@ -13,9 +13,10 @@ import { GetOrgResponse } from "@server/routers/org"; import OrgProvider from "@app/providers/OrgProvider"; import { cache } from "react"; import ResourceInfoBox from "@app/components/ResourceInfoBox"; -import { GetSiteResponse } from "@server/routers/site"; import { getTranslations } from "next-intl/server"; +export const dynamic = "force-dynamic"; + interface ResourceLayoutProps { children: React.ReactNode; params: Promise<{ niceId: string; orgId: string }>; From fbd3802e468c5220b67edcd9d880968b4e9fb2ba Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 10 Dec 2025 21:15:26 +0100 Subject: [PATCH 055/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20domain=20?= =?UTF-8?q?picker=20component=20to=20accept=20default=20values?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/DomainPicker.tsx | 252 ++++++++++++++------------------ src/lib/queries.ts | 12 ++ 2 files changed, 118 insertions(+), 146 deletions(-) diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 2d17e39f..8fc6d583 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -1,8 +1,6 @@ "use client"; -import { useState, useEffect, useCallback } from "react"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; +import { Alert, AlertDescription } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { Command, @@ -13,45 +11,40 @@ import { CommandList, CommandSeparator } from "@/components/ui/command"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; -import { - AlertCircle, - CheckCircle2, - Building2, - Zap, - Check, - ChevronsUpDown, - ArrowUpDown -} from "lucide-react"; -import { Alert, AlertDescription } from "@/components/ui/alert"; -import { createApiClient, formatAxiosError } from "@/lib/api"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { useEnvContext } from "@/hooks/useEnvContext"; import { toast } from "@/hooks/useToast"; -import { ListDomainsResponse } from "@server/routers/domain/listDomains"; -import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types"; -import { AxiosResponse } from "axios"; +import { createApiClient } from "@/lib/api"; import { cn } from "@/lib/cn"; -import { useTranslations } from "next-intl"; -import { build } from "@server/build"; -import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { - sanitizeInputRaw, finalizeSubdomainSanitize, - validateByDomainType, - isValidSubdomainStructure + isValidSubdomainStructure, + sanitizeInputRaw, + validateByDomainType } from "@/lib/subdomain-utils"; +import { orgQueries } from "@app/lib/queries"; +import { build } from "@server/build"; +import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types"; +import { useQuery } from "@tanstack/react-query"; +import { AxiosResponse } from "axios"; +import { + AlertCircle, + Building2, + Check, + CheckCircle2, + ChevronsUpDown, + Zap +} from "lucide-react"; +import { useTranslations } from "next-intl"; import { toUnicode } from "punycode"; - -type OrganizationDomain = { - domainId: string; - baseDomain: string; - verified: boolean; - type: "ns" | "cname" | "wildcard"; -}; +import { useCallback, useEffect, useMemo, useState } from "react"; type AvailableOption = { domainNamespaceId: string; @@ -69,7 +62,7 @@ type DomainOption = { domainNamespaceId?: string; }; -interface DomainPicker2Props { +interface DomainPickerProps { orgId: string; onDomainChange?: (domainInfo: { domainId: string; @@ -81,116 +74,115 @@ interface DomainPicker2Props { }) => void; cols?: number; hideFreeDomain?: boolean; + defaultSubdomain?: string; + defaultBaseDomain?: string; } -export default function DomainPicker2({ +export default function DomainPicker({ orgId, onDomainChange, cols = 2, - hideFreeDomain = false -}: DomainPicker2Props) { + hideFreeDomain = false, + defaultSubdomain, + defaultBaseDomain +}: DomainPickerProps) { const { env } = useEnvContext(); const api = createApiClient({ env }); const t = useTranslations(); + const { data = [], isLoading: loadingDomains } = useQuery( + orgQueries.domains({ orgId }) + ); + + console.log({ defaultSubdomain, defaultBaseDomain }); + if (!env.flags.usePangolinDns) { hideFreeDomain = true; } - const [subdomainInput, setSubdomainInput] = useState(""); + const [subdomainInput, setSubdomainInput] = useState( + defaultSubdomain ?? "" + ); const [selectedBaseDomain, setSelectedBaseDomain] = useState(null); const [availableOptions, setAvailableOptions] = useState( [] ); - const [organizationDomains, setOrganizationDomains] = useState< - OrganizationDomain[] - >([]); - const [loadingDomains, setLoadingDomains] = useState(false); + + // memoized to prevent reruning the effect that selects the initial domain indefinitely + // removing this will break and cause an infinite rerender + const organizationDomains = useMemo(() => { + return data + .filter( + (domain) => + domain.type === "ns" || + domain.type === "cname" || + domain.type === "wildcard" + ) + .map((domain) => ({ + ...domain, + baseDomain: toUnicode(domain.baseDomain), + type: domain.type as "ns" | "cname" | "wildcard" + })); + }, [data]); + const [open, setOpen] = useState(false); // Provided domain search states const [userInput, setUserInput] = useState(""); const [isChecking, setIsChecking] = useState(false); - const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [providedDomainsShown, setProvidedDomainsShown] = useState(3); const [selectedProvidedDomain, setSelectedProvidedDomain] = useState(null); useEffect(() => { - const loadOrganizationDomains = async () => { - setLoadingDomains(true); - try { - const response = await api.get< - AxiosResponse - >(`/org/${orgId}/domains`); - if (response.status === 200) { - const domains = response.data.data.domains - .filter( - (domain) => - domain.type === "ns" || - domain.type === "cname" || - domain.type === "wildcard" - ) - .map((domain) => ({ - ...domain, - baseDomain: toUnicode(domain.baseDomain), - type: domain.type as "ns" | "cname" | "wildcard" - })); - setOrganizationDomains(domains); + if (!loadingDomains) { + if (organizationDomains.length > 0) { + // Select the first organization domain or the one provided from props + const firstOrgDomain = + organizationDomains.find( + (domain) => domain.baseDomain === defaultBaseDomain + ) ?? organizationDomains[0]; + const domainOption: DomainOption = { + id: `org-${firstOrgDomain.domainId}`, + domain: firstOrgDomain.baseDomain, + type: "organization", + verified: firstOrgDomain.verified, + domainType: firstOrgDomain.type, + domainId: firstOrgDomain.domainId + }; + setSelectedBaseDomain(domainOption); - // Auto-select first available domain - if (domains.length > 0) { - // Select the first organization domain - const firstOrgDomain = domains[0]; - const domainOption: DomainOption = { - id: `org-${firstOrgDomain.domainId}`, - domain: firstOrgDomain.baseDomain, - type: "organization", - verified: firstOrgDomain.verified, - domainType: firstOrgDomain.type, - domainId: firstOrgDomain.domainId - }; - setSelectedBaseDomain(domainOption); - - onDomainChange?.({ - domainId: firstOrgDomain.domainId, - type: "organization", - subdomain: undefined, - fullDomain: firstOrgDomain.baseDomain, - baseDomain: firstOrgDomain.baseDomain - }); - } else if ( - (build === "saas" || build === "enterprise") && - !hideFreeDomain - ) { - // If no organization domains, select the provided domain option - const domainOptionText = - build === "enterprise" - ? t("domainPickerProvidedDomain") - : t("domainPickerFreeProvidedDomain"); - const freeDomainOption: DomainOption = { - id: "provided-search", - domain: domainOptionText, - type: "provided-search" - }; - setSelectedBaseDomain(freeDomainOption); - } - } - } catch (error) { - console.error("Failed to load organization domains:", error); - toast({ - variant: "destructive", - title: t("domainPickerError"), - description: t("domainPickerErrorLoadDomains") + onDomainChange?.({ + domainId: firstOrgDomain.domainId, + type: "organization", + subdomain: undefined, + fullDomain: firstOrgDomain.baseDomain, + baseDomain: firstOrgDomain.baseDomain }); - } finally { - setLoadingDomains(false); + } else if ( + (build === "saas" || build === "enterprise") && + !hideFreeDomain + ) { + // If no organization domains, select the provided domain option + const domainOptionText = + build === "enterprise" + ? t("domainPickerProvidedDomain") + : t("domainPickerFreeProvidedDomain"); + const freeDomainOption: DomainOption = { + id: "provided-search", + domain: domainOptionText, + type: "provided-search" + }; + setSelectedBaseDomain(freeDomainOption); } - }; - - loadOrganizationDomains(); - }, [orgId, api, hideFreeDomain]); + } + }, [ + hideFreeDomain, + loadingDomains, + organizationDomains, + defaultBaseDomain + ]); const checkAvailability = useCallback( async (input: string) => { @@ -256,37 +248,6 @@ export default function DomainPicker2({ } }, [userInput, debouncedCheckAvailability, selectedBaseDomain]); - const generateDropdownOptions = (): DomainOption[] => { - const options: DomainOption[] = []; - - organizationDomains.forEach((orgDomain) => { - options.push({ - id: `org-${orgDomain.domainId}`, - domain: orgDomain.baseDomain, - type: "organization", - verified: orgDomain.verified, - domainType: orgDomain.type, - domainId: orgDomain.domainId - }); - }); - - if ((build === "saas" || build === "enterprise") && !hideFreeDomain) { - const domainOptionText = - build === "enterprise" - ? t("domainPickerProvidedDomain") - : t("domainPickerFreeProvidedDomain"); - options.push({ - id: "provided-search", - domain: domainOptionText, - type: "provided-search" - }); - } - - return options; - }; - - const dropdownOptions = generateDropdownOptions(); - const finalizeSubdomain = (sub: string, base: DomainOption): string => { const sanitized = finalizeSubdomainSanitize(sub); @@ -440,8 +401,7 @@ export default function DomainPicker2({ selectedBaseDomain?.type === "provided-search"; const sortedAvailableOptions = [...availableOptions].sort((a, b) => { - const comparison = a.fullDomain.localeCompare(b.fullDomain); - return sortOrder === "asc" ? comparison : -comparison; + return a.fullDomain.localeCompare(b.fullDomain); }); const displayedProvidedOptions = sortedAvailableOptions.slice( @@ -518,16 +478,16 @@ export default function DomainPicker2({ className="w-full justify-between" > {selectedBaseDomain ? ( -
+
{selectedBaseDomain.type === "organization" ? null : ( - + )} {selectedBaseDomain.domain} {selectedBaseDomain.verified && ( - + )}
) : ( diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 2a19f3f9..73e80df8 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -16,6 +16,7 @@ import { remote } from "./api"; import { durationToMs } from "./durationToMs"; import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs"; import type { ListResourceNamesResponse } from "@server/routers/resource"; +import type { ListDomainsResponse } from "@server/routers/domain"; export type ProductUpdate = { link: string | null; @@ -139,6 +140,17 @@ export const orgQueries = { >(`/org/${orgId}/sites`, { signal }); return res.data.data.sites; } + }), + + domains: ({ orgId }: { orgId: string }) => + queryOptions({ + queryKey: ["ORG", orgId, "DOMAINS"] as const, + queryFn: async ({ signal, meta }) => { + const res = await meta!.api.get< + AxiosResponse + >(`/org/${orgId}/domains`, { signal }); + return res.data.data.domains; + } }) }; From de684b212fbe3cbca259781f2d09a429b506261b Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 10 Dec 2025 21:26:46 +0100 Subject: [PATCH 056/153] =?UTF-8?q?=F0=9F=94=87=20remove=20console.log?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/DomainPicker.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 8fc6d583..625b566e 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -94,8 +94,6 @@ export default function DomainPicker({ orgQueries.domains({ orgId }) ); - console.log({ defaultSubdomain, defaultBaseDomain }); - if (!env.flags.usePangolinDns) { hideFreeDomain = true; } From aab0471b6b23e0205feb5144e0ea8a1070ead0c7 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 10 Dec 2025 21:26:55 +0100 Subject: [PATCH 057/153] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20fix=20typescrip?= =?UTF-8?q?t=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/lib/rebuildClientAssociations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/rebuildClientAssociations.ts b/server/lib/rebuildClientAssociations.ts index 549dbffe..e0867dc5 100644 --- a/server/lib/rebuildClientAssociations.ts +++ b/server/lib/rebuildClientAssociations.ts @@ -1085,7 +1085,7 @@ async function handleMessagesForClientSites( continue; } - await holepunchSiteAdd( + await initPeerAddHandshake( // this will kick off the add peer process for the client client.clientId, { From 6fc54bcc9e20a337ee856a8a1bdb72521d338621 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Thu, 11 Dec 2025 22:51:02 +0100 Subject: [PATCH 058/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20set=20default=20va?= =?UTF-8?q?lue=20on=20domain=20picker=20modal=20in=20proxy=20resource=20pa?= =?UTF-8?q?ge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/proxy/[niceId]/general/page.tsx | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx index fa9a6976..7d4db07c 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx @@ -16,7 +16,7 @@ import { import { Input } from "@/components/ui/input"; import { useResourceContext } from "@app/hooks/useResourceContext"; import { ListSitesResponse } from "@server/routers/site"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { AxiosResponse } from "axios"; import { useParams, useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; @@ -90,6 +90,25 @@ export default function GeneralForm() { const [resourceFullDomain, setResourceFullDomain] = useState( `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}` ); + + const [defaultSubdomain, defaultBaseDomain] = useMemo(() => { + const resourceUrl = new URL(resourceFullDomain); + const domain = resourceUrl.hostname; + + const allDomainParts = domain.split("."); + let sub = undefined; + let base = domain; + + if (allDomainParts.length >= 3) { + // 3 parts: [subdomain, domain, tld] + const [first, ...rest] = allDomainParts; + sub = first; + base = rest.join("."); + } + + return [sub, base]; + }, [resourceFullDomain]); + const [selectedDomain, setSelectedDomain] = useState<{ domainId: string; subdomain?: string; @@ -488,6 +507,8 @@ export default function GeneralForm() { { const selected = { domainId: res.domainId, From 124ba208de648bfae22057b3556b668be0d0e16f Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 12 Dec 2025 21:40:49 +0100 Subject: [PATCH 059/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20use=20react=20quer?= =?UTF-8?q?ty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../proxy/[niceId]/authentication/page.tsx | 318 +++++++++--------- src/lib/queries.ts | 28 +- 2 files changed, 177 insertions(+), 169 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 27f4fd73..34087076 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -1,21 +1,22 @@ "use client"; -import { useEffect, useState } from "react"; -import { ListRolesResponse } from "@server/routers/role"; -import { toast } from "@app/hooks/useToast"; -import { useOrgContext } from "@app/hooks/useOrgContext"; -import { useResourceContext } from "@app/hooks/useResourceContext"; -import { AxiosResponse } from "axios"; -import { formatAxiosError } from "@app/lib/api"; +import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm"; +import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm"; import { - GetResourceWhitelistResponse, - ListResourceRolesResponse, - ListResourceUsersResponse -} from "@server/routers/resource"; + 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 { z } from "zod"; -import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; +import { CheckboxWithLabel } from "@app/components/ui/checkbox"; import { Form, FormControl, @@ -25,32 +26,7 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; -import { ListUsersResponse } from "@server/routers/user"; -import { Binary, Key, Bot } from "lucide-react"; -import SetResourcePasswordForm from "components/SetResourcePasswordForm"; -import SetResourcePincodeForm from "@app/components/SetResourcePincodeForm"; -import SetResourceHeaderAuthForm from "@app/components/SetResourceHeaderAuthForm"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { - SettingsContainer, - SettingsSection, - SettingsSectionTitle, - SettingsSectionHeader, - SettingsSectionDescription, - SettingsSectionBody, - SettingsSectionFooter, - SettingsSectionForm -} from "@app/components/Settings"; -import { SwitchInput } from "@app/components/SwitchInput"; import { InfoPopup } from "@app/components/ui/info-popup"; -import { Tag, TagInput } from "@app/components/tags/tag-input"; -import { useRouter } from "next/navigation"; -import { UserType } from "@server/types/UserTypes"; -import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; -import { InfoIcon } from "lucide-react"; -import { useTranslations } from "next-intl"; -import { CheckboxWithLabel } from "@app/components/ui/checkbox"; import { Select, SelectContent, @@ -58,10 +34,33 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; -import { Separator } from "@app/components/ui/separator"; -import { build } from "@server/build"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import { useResourceContext } from "@app/hooks/useResourceContext"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; -import { TierId } from "@server/lib/billing/tiers"; +import { toast } from "@app/hooks/useToast"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { orgQueries, resourceQueries } from "@app/lib/queries"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { build } from "@server/build"; +import { + GetResourceWhitelistResponse, + ListResourceRolesResponse, + ListResourceUsersResponse +} from "@server/routers/resource"; +import { ListRolesResponse } from "@server/routers/role"; +import { ListUsersResponse } from "@server/routers/user"; +import { UserType } from "@server/types/UserTypes"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { AxiosResponse } from "axios"; +import SetResourcePasswordForm from "components/SetResourcePasswordForm"; +import type { text } from "express"; +import { Binary, Bot, InfoIcon, Key } from "lucide-react"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import { useEffect, useMemo, useRef, useState, useTransition } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; const UsersRolesFormSchema = z.object({ roles: z.array( @@ -100,14 +99,83 @@ export default function ResourceAuthenticationPage() { const subscription = useSubscriptionStatusContext(); - const [pageLoading, setPageLoading] = useState(true); + const queryClient = useQueryClient(); + const { data: resourceRoles = [], isLoading: isLoadingResourceRoles } = + useQuery( + resourceQueries.resourceRoles({ + resourceId: resource.resourceId + }) + ); + const { data: resourceUsers = [], isLoading: isLoadingResourceUsers } = + useQuery( + resourceQueries.resourceUsers({ + resourceId: resource.resourceId + }) + ); - const [allRoles, setAllRoles] = useState<{ id: string; text: string }[]>( - [] + const { data: whitelist = [], isLoading: isLoadingWhiteList } = useQuery( + resourceQueries.resourceWhitelist({ + resourceId: resource.resourceId + }) ); - const [allUsers, setAllUsers] = useState<{ id: string; text: string }[]>( - [] + + 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 + }) + ); + + const pageLoading = + isLoadingOrgRoles || + isLoadingOrgUsers || + isLoadingResourceRoles || + isLoadingResourceUsers || + isLoadingWhiteList || + isLoadingOrgIdps; + + 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: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` + })); + }, [orgUsers]); + + const allIdps = useMemo(() => { + if (build === "saas") { + if (subscription?.subscribed) { + return orgIdps.map((idp) => ({ + id: idp.idpId, + text: idp.name + })); + } + } else { + return orgIdps.map((idp) => ({ + id: idp.idpId, + text: idp.name + })); + } + return []; + }, [orgIdps]); + const [activeRolesTagIndex, setActiveRolesTagIndex] = useState< number | null >(null); @@ -131,10 +199,10 @@ export default function ResourceAuthenticationPage() { const [selectedIdpId, setSelectedIdpId] = useState( resource.skipToIdpId || null ); - const [allIdps, setAllIdps] = useState<{ id: number; text: string }[]>([]); const [loadingSaveUsersRoles, setLoadingSaveUsersRoles] = useState(false); - const [loadingSaveWhitelist, setLoadingSaveWhitelist] = useState(false); + const [loadingSaveWhitelist, startSaveWhitelistTransition] = + useTransition(); const [loadingRemoveResourcePassword, setLoadingRemoveResourcePassword] = useState(false); @@ -159,126 +227,35 @@ export default function ResourceAuthenticationPage() { defaultValues: { emails: [] } }); + const hasInitializedRef = useRef(false); + useEffect(() => { - const fetchData = async () => { - try { - const [ - rolesResponse, - resourceRolesResponse, - usersResponse, - resourceUsersResponse, - whitelist, - idpsResponse - ] = await Promise.all([ - api.get>( - `/org/${org?.org.orgId}/roles` - ), - api.get>( - `/resource/${resource.resourceId}/roles` - ), - api.get>( - `/org/${org?.org.orgId}/users` - ), - api.get>( - `/resource/${resource.resourceId}/users` - ), - api.get>( - `/resource/${resource.resourceId}/whitelist` - ), - api.get< - AxiosResponse<{ - idps: { idpId: number; name: string }[]; - }> - >(build === "saas" ? `/org/${org?.org.orgId}/idp` : "/idp") - ]); + if (!pageLoading || hasInitializedRef.current) return; - setAllRoles( - rolesResponse.data.data.roles - .map((role) => ({ - id: role.roleId.toString(), - text: role.name - })) - .filter((role) => role.text !== "Admin") - ); - - usersRolesForm.setValue( - "roles", - resourceRolesResponse.data.data.roles - .map((i) => ({ - id: i.roleId.toString(), - text: i.name - })) - .filter((role) => role.text !== "Admin") - ); - - setAllUsers( - usersResponse.data.data.users.map((user) => ({ - id: user.id.toString(), - text: `${user.email || user.username}${user.type !== UserType.Internal ? ` (${user.idpName})` : ""}` - })) - ); - - usersRolesForm.setValue( - "users", - resourceUsersResponse.data.data.users.map((i) => ({ - id: i.userId.toString(), - text: `${i.email || i.username}${i.type !== UserType.Internal ? ` (${i.idpName})` : ""}` - })) - ); - - whitelistForm.setValue( - "emails", - whitelist.data.data.whitelist.map((w) => ({ - id: w.email, - text: w.email - })) - ); - - if (build === "saas") { - if (subscription?.subscribed) { - setAllIdps( - idpsResponse.data.data.idps.map((idp) => ({ - id: idp.idpId, - text: idp.name - })) - ); - } - } else { - setAllIdps( - idpsResponse.data.data.idps.map((idp) => ({ - id: idp.idpId, - text: idp.name - })) - ); - } - - if ( - autoLoginEnabled && - !selectedIdpId && - idpsResponse.data.data.idps.length > 0 - ) { - setSelectedIdpId(idpsResponse.data.data.idps[0].idpId); - } - - setPageLoading(false); - } catch (e) { - console.error(e); - toast({ - variant: "destructive", - title: t("resourceErrorAuthFetch"), - description: formatAxiosError( - e, - t("resourceErrorAuthFetchDescription") - ) - }); - } - }; - - fetchData(); - }, []); + usersRolesForm.setValue("roles", allRoles); + usersRolesForm.setValue("users", allUsers); + whitelistForm.setValue( + "emails", + whitelist.map((w) => ({ + id: w.email, + text: w.email + })) + ); + if (autoLoginEnabled && !selectedIdpId && orgIdps.length > 0) { + setSelectedIdpId(orgIdps[0].idpId); + } + hasInitializedRef.current = true; + }, [ + pageLoading, + allRoles, + allUsers, + whitelist, + autoLoginEnabled, + selectedIdpId, + orgIdps + ]); async function saveWhitelist() { - setLoadingSaveWhitelist(true); try { await api.post(`/resource/${resource.resourceId}`, { emailWhitelistEnabled: whitelistEnabled @@ -299,6 +276,11 @@ export default function ResourceAuthenticationPage() { description: t("resourceWhitelistSaveDescription") }); router.refresh(); + await queryClient.invalidateQueries( + resourceQueries.resourceWhitelist({ + resourceId: resource.resourceId + }) + ); } catch (e) { console.error(e); toast({ @@ -309,8 +291,6 @@ export default function ResourceAuthenticationPage() { t("resourceErrorWhitelistSaveDescription") ) }); - } finally { - setLoadingSaveWhitelist(false); } } @@ -984,7 +964,9 @@ export default function ResourceAuthenticationPage() { - - + ); } + +type OneTimePasswordFormSectionProps = Pick< + ResourceContextType, + "resource" | "updateResource" +>; + +function OneTimePasswordFormSection({ + resource, + updateResource +}: OneTimePasswordFormSectionProps) { + const { env } = useEnvContext(); + const [whitelistEnabled, setWhitelistEnabled] = useState( + 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); + + 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")} + + + )} + /> + + + )} +
+
+ + + +
+ ); +} From 9cee3d9c798bda790d498e981a542acf55459913 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 12 Dec 2025 23:35:24 +0100 Subject: [PATCH 063/153] =?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 --- .../proxy/[niceId]/authentication/page.tsx | 56 ------------------- 1 file changed, 56 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 15c8e197..8de6aa13 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -44,18 +44,9 @@ import { createApiClient, formatAxiosError } from "@app/lib/api"; import { orgQueries, resourceQueries } from "@app/lib/queries"; import { zodResolver } from "@hookform/resolvers/zod"; import { build } from "@server/build"; -import { - GetResourceWhitelistResponse, - ListResourceRolesResponse, - ListResourceUsersResponse -} from "@server/routers/resource"; -import { ListRolesResponse } from "@server/routers/role"; -import { ListUsersResponse } from "@server/routers/user"; import { UserType } from "@server/types/UserTypes"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { AxiosResponse } from "axios"; import SetResourcePasswordForm from "components/SetResourcePasswordForm"; -import type { text } from "express"; import { Binary, Bot, InfoIcon, Key } from "lucide-react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; @@ -191,15 +182,7 @@ export default function ResourceAuthenticationPage() { number | null >(null); - const [activeEmailTagIndex, setActiveEmailTagIndex] = useState< - number | null - >(null); - const [ssoEnabled, setSsoEnabled] = useState(resource.sso); - // const [blockAccess, setBlockAccess] = useState(resource.blockAccess); - const [whitelistEnabled, setWhitelistEnabled] = useState( - resource.emailWhitelistEnabled - ); const [autoLoginEnabled, setAutoLoginEnabled] = useState( resource.skipToIdpId !== null && resource.skipToIdpId !== undefined @@ -274,45 +257,6 @@ export default function ResourceAuthenticationPage() { orgIdps ]); - 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") - ) - }); - } - } - const [, submitUserRolesForm, loadingSaveUsersRoles] = useActionState( onSubmitUsersRoles, null From 4366ca5836f38d1e7136a6dbbd8976bad014d224 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sat, 13 Dec 2025 10:57:24 -0500 Subject: [PATCH 064/153] add spacing to delete modal --- .../settings/(private)/remote-exit-nodes/ExitNodesTable.tsx | 2 +- src/app/[orgId]/settings/general/page.tsx | 4 ++-- src/app/admin/license/page.tsx | 2 +- src/app/admin/users/AdminUsersTable.tsx | 2 +- src/components/AdminIdpTable.tsx | 2 +- src/components/AdminUsersTable.tsx | 2 +- src/components/ApiKeysTable.tsx | 2 +- src/components/ClientResourcesTable.tsx | 2 +- src/components/DomainsTable.tsx | 2 +- src/components/InvitationsTable.tsx | 2 +- src/components/MachineClientsTable.tsx | 2 +- src/components/OrgApiKeysTable.tsx | 2 +- src/components/ProxyResourcesTable.tsx | 2 +- src/components/SitesTable.tsx | 2 +- src/components/UserDevicesTable.tsx | 2 +- src/components/UsersTable.tsx | 2 +- src/components/ViewDevicesDialog.tsx | 2 +- src/components/private/OrgIdpTable.tsx | 2 +- 18 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx index a38f3b86..e5250bea 100644 --- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx +++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx @@ -304,7 +304,7 @@ export default function ExitNodesTable({ setSelectedNode(null); }} dialog={ -
+

{t("remoteExitNodeQuestionRemove")}

{t("remoteExitNodeMessageRemove")}

diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index e391922f..97dd4a03 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -289,7 +289,7 @@ export default function GeneralPage() { setIsDeleteModalOpen(val); }} dialog={ -
+

{t("orgQuestionRemove")}

{t("orgMessageRemove")}

@@ -303,7 +303,7 @@ export default function GeneralPage() { open={isSecurityPolicyConfirmOpen} setOpen={setIsSecurityPolicyConfirmOpen} dialog={ -
+

{t("securityPolicyChangeDescription")}

} diff --git a/src/app/admin/license/page.tsx b/src/app/admin/license/page.tsx index ac6d3e67..bef4f173 100644 --- a/src/app/admin/license/page.tsx +++ b/src/app/admin/license/page.tsx @@ -315,7 +315,7 @@ export default function LicensePage() { setSelectedLicenseKey(null); }} dialog={ -
+

{t("licenseQuestionRemove")}

{t("licenseMessageRemove")} diff --git a/src/app/admin/users/AdminUsersTable.tsx b/src/app/admin/users/AdminUsersTable.tsx index efcf9484..1c7d1b7f 100644 --- a/src/app/admin/users/AdminUsersTable.tsx +++ b/src/app/admin/users/AdminUsersTable.tsx @@ -243,7 +243,7 @@ export default function UsersTable({ users }: Props) { setSelected(null); }} dialog={ -

+

{t("userQuestionRemove")}

{t("userMessageRemove")}

diff --git a/src/components/AdminIdpTable.tsx b/src/components/AdminIdpTable.tsx index 76a0fdd7..75a7c545 100644 --- a/src/components/AdminIdpTable.tsx +++ b/src/components/AdminIdpTable.tsx @@ -196,7 +196,7 @@ export default function IdpTable({ idps }: Props) { setSelectedIdp(null); }} dialog={ -
+

{t("idpQuestionRemove", { name: selectedIdp.name diff --git a/src/components/AdminUsersTable.tsx b/src/components/AdminUsersTable.tsx index 9c741cee..327d4752 100644 --- a/src/components/AdminUsersTable.tsx +++ b/src/components/AdminUsersTable.tsx @@ -313,7 +313,7 @@ export default function UsersTable({ users }: Props) { setSelected(null); }} dialog={ -

+

{t("userQuestionRemove", { selectedUser: diff --git a/src/components/ApiKeysTable.tsx b/src/components/ApiKeysTable.tsx index c3202277..8987fa2c 100644 --- a/src/components/ApiKeysTable.tsx +++ b/src/components/ApiKeysTable.tsx @@ -182,7 +182,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) { setSelected(null); }} dialog={ -

+

{t("apiKeysQuestionRemove")}

{t("apiKeysMessageRemove")}

diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index a5e257c7..3f65c762 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -284,7 +284,7 @@ export default function ClientResourcesTable({ setSelectedInternalResource(null); }} dialog={ -
+

{t("resourceQuestionRemove")}

{t("resourceMessageRemove")}

diff --git a/src/components/DomainsTable.tsx b/src/components/DomainsTable.tsx index 331425c3..ff23df67 100644 --- a/src/components/DomainsTable.tsx +++ b/src/components/DomainsTable.tsx @@ -304,7 +304,7 @@ export default function DomainsTable({ domains, orgId }: Props) { setSelectedDomain(null); }} dialog={ -
+

{t("domainQuestionRemove")}

{t("domainMessageRemove")}

diff --git a/src/components/InvitationsTable.tsx b/src/components/InvitationsTable.tsx index 1b218ea3..0d2d3e9b 100644 --- a/src/components/InvitationsTable.tsx +++ b/src/components/InvitationsTable.tsx @@ -182,7 +182,7 @@ export default function InvitationsTable({ setSelectedInvitation(null); }} dialog={ -
+

{t("inviteQuestionRemove")}

{t("inviteMessageRemove")}

diff --git a/src/components/MachineClientsTable.tsx b/src/components/MachineClientsTable.tsx index 7ac10eb7..67ed2e08 100644 --- a/src/components/MachineClientsTable.tsx +++ b/src/components/MachineClientsTable.tsx @@ -354,7 +354,7 @@ export default function MachineClientsTable({ setSelectedClient(null); }} dialog={ -
+

{t("deleteClientQuestion")}

{t("clientMessageRemove")}

diff --git a/src/components/OrgApiKeysTable.tsx b/src/components/OrgApiKeysTable.tsx index 2f50e571..72509bc4 100644 --- a/src/components/OrgApiKeysTable.tsx +++ b/src/components/OrgApiKeysTable.tsx @@ -189,7 +189,7 @@ export default function OrgApiKeysTable({ setSelected(null); }} dialog={ -
+

{t("apiKeysQuestionRemove")}

{t("apiKeysMessageRemove")}

diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index 6f39c099..bd4bb4e1 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -535,7 +535,7 @@ export default function ProxyResourcesTable({ setSelectedResource(null); }} dialog={ -
+

{t("resourceQuestionRemove")}

{t("resourceMessageRemove")}

diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index 3a8085c2..46aeb79f 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -412,7 +412,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) { setSelectedSite(null); }} dialog={ -
+

{t("siteQuestionRemove")}

{t("siteMessageRemove")}

diff --git a/src/components/UserDevicesTable.tsx b/src/components/UserDevicesTable.tsx index 88a0d4a8..71321bf8 100644 --- a/src/components/UserDevicesTable.tsx +++ b/src/components/UserDevicesTable.tsx @@ -401,7 +401,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { setSelectedClient(null); }} dialog={ -
+

{t("deleteClientQuestion")}

{t("clientMessageRemove")}

diff --git a/src/components/UsersTable.tsx b/src/components/UsersTable.tsx index 55ee9505..0d0ff379 100644 --- a/src/components/UsersTable.tsx +++ b/src/components/UsersTable.tsx @@ -258,7 +258,7 @@ export default function UsersTable({ users: u }: UsersTableProps) { setSelectedUser(null); }} dialog={ -
+

{t("userQuestionOrgRemove")}

{t("userMessageOrgRemove")}

diff --git a/src/components/ViewDevicesDialog.tsx b/src/components/ViewDevicesDialog.tsx index 33e9d87f..70c55ded 100644 --- a/src/components/ViewDevicesDialog.tsx +++ b/src/components/ViewDevicesDialog.tsx @@ -224,7 +224,7 @@ export default function ViewDevicesDialog({ } }} dialog={ -
+

{t("deviceQuestionRemove") || "Are you sure you want to delete this device?"} diff --git a/src/components/private/OrgIdpTable.tsx b/src/components/private/OrgIdpTable.tsx index 8730053e..f5fdfe40 100644 --- a/src/components/private/OrgIdpTable.tsx +++ b/src/components/private/OrgIdpTable.tsx @@ -177,7 +177,7 @@ export default function IdpTable({ idps, orgId }: Props) { setSelectedIdp(null); }} dialog={ -

+

{t("idpQuestionRemove")}

{t("idpMessageRemove")}

From 9f55d6b20aa4c3cd47b1503e125261d2b84a5733 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 13 Dec 2025 11:19:27 -0500 Subject: [PATCH 065/153] Try to fix issue not sending newt commands --- server/lib/ip.ts | 10 +++-- server/lib/rebuildClientAssociations.ts | 52 ++++++++++--------------- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/server/lib/ip.ts b/server/lib/ip.ts index 02683edc..9c412801 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -432,7 +432,12 @@ export function generateRemoteSubnets( ): string[] { const remoteSubnets = allSiteResources .filter((sr) => { - if (sr.mode === "cidr") return true; + if (sr.mode === "cidr") { + // check if its a valid CIDR using zod + const cidrSchema = z.union([z.cidrv4(), z.cidrv6()]); + const parseResult = cidrSchema.safeParse(sr.destination); + return parseResult.success; + } if (sr.mode === "host") { // check if its a valid IP using zod const ipSchema = z.union([z.ipv4(), z.ipv6()]); @@ -456,13 +461,12 @@ export function generateRemoteSubnets( export type Alias = { alias: string | null; aliasAddress: string | null }; export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] { - let aliasConfigs = allSiteResources + return allSiteResources .filter((sr) => sr.alias && sr.aliasAddress && sr.mode == "host") .map((sr) => ({ alias: sr.alias, aliasAddress: sr.aliasAddress })); - return aliasConfigs; } export type SubnetProxyTarget = { diff --git a/server/lib/rebuildClientAssociations.ts b/server/lib/rebuildClientAssociations.ts index e0867dc5..db6368da 100644 --- a/server/lib/rebuildClientAssociations.ts +++ b/server/lib/rebuildClientAssociations.ts @@ -955,24 +955,9 @@ export async function rebuildClientAssociationsFromClient( /////////// Send messages /////////// - // Get the olm for this client - const [olm] = await trx - .select({ olmId: olms.olmId }) - .from(olms) - .where(eq(olms.clientId, client.clientId)) - .limit(1); - - if (!olm) { - logger.warn( - `Olm not found for client ${client.clientId}, skipping peer updates` - ); - return; - } - // Handle messages for sites being added await handleMessagesForClientSites( client, - olm.olmId, sitesToAdd, sitesToRemove, trx @@ -996,11 +981,26 @@ async function handleMessagesForClientSites( userId: string | null; orgId: string; }, - olmId: string, sitesToAdd: number[], sitesToRemove: number[], trx: Transaction | typeof db = db ): Promise { + // Get the olm for this client + const [olm] = await trx + .select({ olmId: olms.olmId }) + .from(olms) + .where(eq(olms.clientId, client.clientId)) + .limit(1); + + if (!olm) { + logger.warn( + `Olm not found for client ${client.clientId}, skipping peer updates` + ); + return; + } + + const olmId = olm.olmId; + if (!client.subnet || !client.pubKey) { logger.warn( `Client ${client.clientId} missing subnet or pubKey, skipping peer updates` @@ -1021,9 +1021,9 @@ async function handleMessagesForClientSites( .leftJoin(newts, eq(sites.siteId, newts.siteId)) .where(inArray(sites.siteId, allSiteIds)); - let newtJobs: Promise[] = []; - let olmJobs: Promise[] = []; - let exitNodeJobs: Promise[] = []; + const newtJobs: Promise[] = []; + const olmJobs: Promise[] = []; + const exitNodeJobs: Promise[] = []; for (const siteData of sitesData) { const site = siteData.sites; @@ -1130,18 +1130,8 @@ async function handleMessagesForClientResources( resourcesToRemove: number[], trx: Transaction | typeof db = db ): Promise { - // Group resources by site - const resourcesBySite = new Map(); - - for (const resource of allNewResources) { - if (!resourcesBySite.has(resource.siteId)) { - resourcesBySite.set(resource.siteId, []); - } - resourcesBySite.get(resource.siteId)!.push(resource); - } - - let proxyJobs: Promise[] = []; - let olmJobs: Promise[] = []; + const proxyJobs: Promise[] = []; + const olmJobs: Promise[] = []; // Handle additions if (resourcesToAdd.length > 0) { From 143175bde7a8b302d8f363139392c6d40dcfc453 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 13 Dec 2025 11:34:58 -0500 Subject: [PATCH 066/153] Update react-dom --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index c6e60cef..b3a18c31 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,7 +90,7 @@ "qrcode.react": "4.2.0", "react": "19.2.3", "react-day-picker": "9.12.0", - "react-dom": "19.2.1", + "react-dom": "19.2.3", "react-easy-sort": "1.8.0", "react-hook-form": "7.68.0", "react-icons": "5.5.0", @@ -19768,16 +19768,16 @@ } }, "node_modules/react-dom": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", - "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", "peer": true, "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.1" + "react": "^19.2.3" } }, "node_modules/react-easy-sort": { diff --git a/package.json b/package.json index 5609b688..2aebc439 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "qrcode.react": "4.2.0", "react": "19.2.3", "react-day-picker": "9.12.0", - "react-dom": "19.2.1", + "react-dom": "19.2.3", "react-easy-sort": "1.8.0", "react-hook-form": "7.68.0", "react-icons": "5.5.0", From 1aaad43871d9702c041e2e1486b7407486d2cf72 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 13 Dec 2025 11:36:53 -0500 Subject: [PATCH 067/153] Format --- server/lib/rebuildClientAssociations.ts | 7 +----- src/components/DataTablePagination.tsx | 32 ++++++++++--------------- src/components/ui/data-table.tsx | 8 +++++-- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/server/lib/rebuildClientAssociations.ts b/server/lib/rebuildClientAssociations.ts index db6368da..625e5793 100644 --- a/server/lib/rebuildClientAssociations.ts +++ b/server/lib/rebuildClientAssociations.ts @@ -956,12 +956,7 @@ export async function rebuildClientAssociationsFromClient( /////////// Send messages /////////// // Handle messages for sites being added - await handleMessagesForClientSites( - client, - sitesToAdd, - sitesToRemove, - trx - ); + await handleMessagesForClientSites(client, sitesToAdd, sitesToRemove, trx); // Handle subnet proxy target updates for resources await handleMessagesForClientResources( diff --git a/src/components/DataTablePagination.tsx b/src/components/DataTablePagination.tsx index ba40eff4..4abcf1c5 100644 --- a/src/components/DataTablePagination.tsx +++ b/src/components/DataTablePagination.tsx @@ -43,13 +43,15 @@ export function DataTablePagination({ // Use controlled values if provided, otherwise fall back to table state const pageSize = controlledPageSize ?? table.getState().pagination.pageSize; - const pageIndex = controlledPageIndex ?? table.getState().pagination.pageIndex; - + const pageIndex = + controlledPageIndex ?? table.getState().pagination.pageIndex; + // Calculate page boundaries based on controlled state // For server-side pagination, use totalCount if available for accurate page count - const pageCount = isServerPagination && totalCount !== undefined - ? Math.ceil(totalCount / pageSize) - : table.getPageCount(); + const pageCount = + isServerPagination && totalCount !== undefined + ? Math.ceil(totalCount / pageSize) + : table.getPageCount(); const canNextPage = pageIndex < pageCount - 1; const canPreviousPage = pageIndex > 0; @@ -125,9 +127,7 @@ export function DataTablePagination({ disabled={disabled} > - + {[10, 20, 30, 40, 50, 100].map((pageSize) => ( @@ -156,9 +156,7 @@ export function DataTablePagination({ variant="outline" className="hidden h-8 w-8 p-0 lg:flex" onClick={() => handlePageNavigation("first")} - disabled={ - !canPreviousPage || isLoading || disabled - } + disabled={!canPreviousPage || isLoading || disabled} > {t("paginatorToFirst")} @@ -167,9 +165,7 @@ export function DataTablePagination({ variant="outline" className="h-8 w-8 p-0" onClick={() => handlePageNavigation("previous")} - disabled={ - !canPreviousPage || isLoading || disabled - } + disabled={!canPreviousPage || isLoading || disabled} > {t("paginatorToPrevious")} @@ -180,9 +176,7 @@ export function DataTablePagination({ variant="outline" className="h-8 w-8 p-0" onClick={() => handlePageNavigation("next")} - disabled={ - !canNextPage || isLoading || disabled - } + disabled={!canNextPage || isLoading || disabled} > {t("paginatorToNext")} @@ -191,9 +185,7 @@ export function DataTablePagination({ variant="outline" className="hidden h-8 w-8 p-0 lg:flex" onClick={() => handlePageNavigation("last")} - disabled={ - !canNextPage || isLoading || disabled - } + disabled={!canNextPage || isLoading || disabled} > {t("paginatorToLast")} diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index 1d2bc97f..41529692 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -296,12 +296,16 @@ export function DataTable({ const handleTabChange = (value: string) => { setActiveTab(value); // Reset to first page when changing tabs - setPagination(prev => ({ ...prev, pageIndex: 0 })); + setPagination((prev) => ({ ...prev, pageIndex: 0 })); }; // Enhanced pagination component that updates our local state const handlePageSizeChange = (newPageSize: number) => { - setPagination(prev => ({ ...prev, pageSize: newPageSize, pageIndex: 0 })); + setPagination((prev) => ({ + ...prev, + pageSize: newPageSize, + pageIndex: 0 + })); setPageSize(newPageSize); // Persist immediately when changed From c7747fd4b430ce4bbc07974b53f966dac561eac3 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sat, 13 Dec 2025 11:45:07 -0500 Subject: [PATCH 068/153] add license watermark --- messages/en-US.json | 5 ++++- src/app/admin/license/page.tsx | 3 ++- src/app/auth/layout.tsx | 22 +++++++++++++++++++++- src/components/LayoutSidebar.tsx | 15 ++++++++++++++- src/components/ResourceAuthPortal.tsx | 18 +++++++++++++++++- 5 files changed, 58 insertions(+), 5 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 7d5deded..b023ac75 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2272,5 +2272,8 @@ "remoteExitNodeRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the remote exit node. The remote exit node will need to be restarted with the new credentials.", "remoteExitNodeRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this remote exit node?", "remoteExitNodeRegenerateCredentialsWarning": "This will regenerate the credentials. The remote exit node will stay connected until you manually restart it and use the new credentials.", - "agent": "Agent" + "agent": "Agent", + "personalUseOnly": "Personal Use Only", + "loginPageLicenseWatermark": "This instance is licensed for personal use only.", + "instanceIsUnlicensed": "This instance is unlicensed." } diff --git a/src/app/admin/license/page.tsx b/src/app/admin/license/page.tsx index bef4f173..4e0586bd 100644 --- a/src/app/admin/license/page.tsx +++ b/src/app/admin/license/page.tsx @@ -360,7 +360,8 @@ export default function LicensePage() {
- {t("licensed")} + {t("licensed") + + `${licenseStatus?.tier === "personal" ? ` (${t("personalUseOnly")})` : ""}`}
) : ( diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx index 70439824..6a72006b 100644 --- a/src/app/auth/layout.tsx +++ b/src/app/auth/layout.tsx @@ -23,6 +23,7 @@ export default async function AuthLayout({ children }: AuthLayoutProps) { const t = await getTranslations(); let hideFooter = false; + let licenseStatus: GetLicenseStatusResponse | null = null; if (build == "enterprise") { const licenseStatusRes = await cache( async () => @@ -30,10 +31,12 @@ export default async function AuthLayout({ children }: AuthLayoutProps) { "/license/status" ) )(); + licenseStatus = licenseStatusRes.data.data; if ( env.branding.hideAuthLayoutFooter && licenseStatusRes.data.data.isHostLicensed && - licenseStatusRes.data.data.isLicenseValid + licenseStatusRes.data.data.isLicenseValid && + licenseStatusRes.data.data.tier !== "personal" ) { hideFooter = true; } @@ -83,6 +86,23 @@ export default async function AuthLayout({ children }: AuthLayoutProps) { ? t("enterpriseEdition") : t("pangolinCloud")}
+ {build === "enterprise" && + licenseStatus?.isHostLicensed && + licenseStatus?.isLicenseValid && + licenseStatus?.tier === "personal" ? ( + <> + + {t("personalUseOnly")} + + ) : null} + {build === "enterprise" && + (!licenseStatus?.isHostLicensed || + !licenseStatus?.isLicenseValid) ? ( + <> + + {t("unlicensed")} + + ) : null} {build === "saas" && ( <> diff --git a/src/components/LayoutSidebar.tsx b/src/components/LayoutSidebar.tsx index 8006b6e8..45d0292b 100644 --- a/src/components/LayoutSidebar.tsx +++ b/src/components/LayoutSidebar.tsx @@ -25,6 +25,7 @@ import { useEffect, useState } from "react"; import { FaGithub } from "react-icons/fa"; import SidebarLicenseButton from "./SidebarLicenseButton"; import { SidebarSupportButton } from "./SidebarSupportButton"; +import { is } from "drizzle-orm"; const ProductUpdates = dynamic(() => import("./ProductUpdates"), { ssr: false @@ -52,7 +53,7 @@ export function LayoutSidebar({ const pathname = usePathname(); const isAdminPage = pathname?.startsWith("/admin"); const { user } = useUserContext(); - const { isUnlocked } = useLicenseStatusContext(); + const { isUnlocked, licenseStatus } = useLicenseStatusContext(); const { env } = useEnvContext(); const t = useTranslations(); @@ -226,6 +227,18 @@ export function LayoutSidebar({
+ {build === "enterprise" && + isUnlocked() && + licenseStatus?.tier === "personal" ? ( +
+ {t("personalUseOnly")} +
+ ) : null} + {build === "enterprise" && !isUnlocked() ? ( +
+ {t("unlicensed")} +
+ ) : null} {env?.app?.version && (
{ let colLength = 0; @@ -737,6 +737,22 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
)} + {build === "enterprise" && !isUnlocked() ? ( +
+ + {t("instanceIsUnlicensed")} + +
+ ) : null} + {build === "enterprise" && + isUnlocked() && + licenseStatus?.tier === "personal" ? ( +
+ + {t("loginPageLicenseWatermark")} + +
+ ) : null}
) : ( From deac26bad23f884be9d43c0a26561d3d4d29c279 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 13 Dec 2025 12:07:35 -0500 Subject: [PATCH 069/153] Bump version --- server/lib/consts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/consts.ts b/server/lib/consts.ts index d93cf224..d1f66a9e 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -2,7 +2,7 @@ import path from "path"; import { fileURLToPath } from "url"; // This is a placeholder value replaced by the build process -export const APP_VERSION = "1.13.0"; +export const APP_VERSION = "1.13.1"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); From 25fed237588f90fdb3f3f80921dc43cd83eece6e Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 13 Dec 2025 12:13:33 -0500 Subject: [PATCH 070/153] Speed up build --- .github/workflows/cicd.yml | 2 +- Makefile | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 8af8625d..c2129493 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -107,7 +107,7 @@ jobs: - name: Build and push Docker images (Docker Hub) run: | TAG=${{ env.TAG }} - make build-release tag=$TAG + make -j4 build-release tag=$TAG echo "Built & pushed to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}" shell: bash diff --git a/Makefile b/Makefile index 6c538a47..91ba1192 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,12 @@ major_tag := $(shell echo $(tag) | cut -d. -f1) minor_tag := $(shell echo $(tag) | cut -d. -f1,2) -build-release: + +.PHONY: build-release build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql + +build-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql + +build-sqlite: @if [ -z "$(tag)" ]; then \ echo "Error: tag is required. Usage: make build-release tag="; \ exit 1; \ @@ -16,6 +21,12 @@ build-release: --tag fosrl/pangolin:$(minor_tag) \ --tag fosrl/pangolin:$(tag) \ --push . + +build-postgresql: + @if [ -z "$(tag)" ]; then \ + echo "Error: tag is required. Usage: make build-release tag="; \ + exit 1; \ + fi docker buildx build \ --build-arg BUILD=oss \ --build-arg DATABASE=pg \ @@ -25,6 +36,12 @@ build-release: --tag fosrl/pangolin:postgresql-$(minor_tag) \ --tag fosrl/pangolin:postgresql-$(tag) \ --push . + +build-ee-sqlite: + @if [ -z "$(tag)" ]; then \ + echo "Error: tag is required. Usage: make build-release tag="; \ + exit 1; \ + fi docker buildx build \ --build-arg BUILD=enterprise \ --build-arg DATABASE=sqlite \ @@ -34,6 +51,12 @@ build-release: --tag fosrl/pangolin:ee-$(minor_tag) \ --tag fosrl/pangolin:ee-$(tag) \ --push . + +build-ee-postgresql: + @if [ -z "$(tag)" ]; then \ + echo "Error: tag is required. Usage: make build-release tag="; \ + exit 1; \ + fi docker buildx build \ --build-arg BUILD=enterprise \ --build-arg DATABASE=pg \ From f2d4c2f83cf339ab5a564125b63cf7e88917734f Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 13 Dec 2025 12:16:11 -0500 Subject: [PATCH 071/153] Remove duplicate target --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 91ba1192..1519aec7 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build build-pg build-release build-arm build-x86 test clean +.PHONY: build dev-build-sqlite dev-build-pg build-release build-arm build-x86 test clean major_tag := $(shell echo $(tag) | cut -d. -f1) minor_tag := $(shell echo $(tag) | cut -d. -f1,2) @@ -103,10 +103,10 @@ build-arm: build-x86: docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest . -build-sqlite: +dev-build-sqlite: docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest . -build-pg: +dev-build-pg: docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest . test: From a767a31c21595027a6285b4db446f5e60e7053b2 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 13 Dec 2025 12:28:44 -0500 Subject: [PATCH 072/153] Quiet log message --- server/private/lib/traefik/getTraefikConfig.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index 8060ccad..82568216 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -823,7 +823,7 @@ export async function getTraefikConfig( (cert) => cert.queriedDomain === lp.fullDomain ); if (!matchingCert) { - logger.warn( + logger.debug( `No matching certificate found for login page domain: ${lp.fullDomain}` ); continue; From 9b98acb553ce9f51e2768a9063dc466bb981abba Mon Sep 17 00:00:00 2001 From: Mateusz Gruszkiewicz Date: Sat, 13 Dec 2025 19:27:15 +0100 Subject: [PATCH 073/153] fix missing gpg dependency which is preventing docker from installing correctly --- install/containers.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install/containers.go b/install/containers.go index 9993e117..464186c2 100644 --- a/install/containers.go +++ b/install/containers.go @@ -73,7 +73,7 @@ func installDocker() error { case strings.Contains(osRelease, "ID=ubuntu"): installCmd = exec.Command("bash", "-c", fmt.Sprintf(` apt-get update && - apt-get install -y apt-transport-https ca-certificates curl && + apt-get install -y apt-transport-https ca-certificates curl gpg && curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && apt-get update && @@ -82,7 +82,7 @@ func installDocker() error { case strings.Contains(osRelease, "ID=debian"): installCmd = exec.Command("bash", "-c", fmt.Sprintf(` apt-get update && - apt-get install -y apt-transport-https ca-certificates curl && + apt-get install -y apt-transport-https ca-certificates curl gpg && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg && echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list && apt-get update && From 98c77ad7e25b0624a2450f54257e55a3e07bd479 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Sun, 14 Dec 2025 03:09:45 -0500 Subject: [PATCH 074/153] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a842ed3b..27105c70 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ [![Slack](https://img.shields.io/badge/chat-slack-yellow?style=flat-square&logo=slack)](https://pangolin.net/slack) [![Docker](https://img.shields.io/docker/pulls/fosrl/pangolin?style=flat-square)](https://hub.docker.com/r/fosrl/pangolin) ![Stars](https://img.shields.io/github/stars/fosrl/pangolin?style=flat-square) -[![YouTube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@fossorial-app) +[![YouTube](https://img.shields.io/badge/YouTube-red?logo=youtube&logoColor=white&style=flat-square)](https://www.youtube.com/@pangolin-net)
From 97631c068cfe78f12e0ae08a04a920201c0e51a9 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 14 Dec 2025 15:58:29 -0500 Subject: [PATCH 075/153] Clean key Ref #1806 --- server/routers/gerbil/getConfig.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts index ba3ab7ad..488ef75b 100644 --- a/server/routers/gerbil/getConfig.ts +++ b/server/routers/gerbil/getConfig.ts @@ -51,7 +51,10 @@ export async function getConfig( ); } - const exitNode = await createExitNode(publicKey, reachableAt); + // clean up the public key - keep only valid base64 characters (A-Z, a-z, 0-9, +, /, =) + const cleanedPublicKey = publicKey.replace(/[^A-Za-z0-9+/=]/g, ''); + + const exitNode = await createExitNode(cleanedPublicKey, reachableAt); if (!exitNode) { return next( From 474b9a685ddd86af4368bcc112b6fda4a67c1424 Mon Sep 17 00:00:00 2001 From: Varun Narravula Date: Sun, 14 Dec 2025 16:24:17 -0800 Subject: [PATCH 076/153] feat(setup): allow declaring a server setup token through env variable --- server/setup/ensureSetupToken.ts | 60 ++++++++++++++++++++++++++------ 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts index 64298029..87b86321 100644 --- a/server/setup/ensureSetupToken.ts +++ b/server/setup/ensureSetupToken.ts @@ -16,11 +16,23 @@ function generateToken(): string { return generateRandomString(random, alphabet, 32); } +function validateToken(token: string): boolean { + const tokenRegex = /^[a-z0-9]{32}$/; + return tokenRegex.test(token); +} + function generateId(length: number): string { const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; return generateRandomString(random, alphabet, length); } +function showSetupToken(token: string, source: string): void { + console.log(`=== SETUP TOKEN ${source} ===`); + console.log("Token:", token); + console.log("Use this token on the initial setup page"); + console.log("================================"); +} + export async function ensureSetupToken() { try { // Check if a server admin already exists @@ -38,17 +50,48 @@ export async function ensureSetupToken() { } // Check if a setup token already exists - const existingTokens = await db + const [existingToken] = await db .select() .from(setupTokens) .where(eq(setupTokens.used, false)); + const envSetupToken = process.env.PANGOLIN_SETUP_TOKEN; + console.debug("PANGOLIN_SETUP_TOKEN:", envSetupToken); + if (envSetupToken) { + if (!validateToken(envSetupToken)) { + throw new Error( + "invalid token format for PANGOLIN_SETUP_TOKEN" + ); + } + + if (existingToken?.token !== envSetupToken) { + console.warn( + "Overwriting existing token in DB since PANGOLIN_SETUP_TOKEN is set" + ); + + await db + .update(setupTokens) + .set({ token: envSetupToken }) + .where(eq(setupTokens.tokenId, existingToken.tokenId)); + } else { + const tokenId = generateId(15); + + await db.insert(setupTokens).values({ + tokenId: tokenId, + token: envSetupToken, + used: false, + dateCreated: moment().toISOString(), + dateUsed: null + }); + } + + showSetupToken(envSetupToken, "FROM ENVIRONMENT"); + return; + } + // If unused token exists, display it instead of creating a new one - if (existingTokens.length > 0) { - console.log("=== SETUP TOKEN EXISTS ==="); - console.log("Token:", existingTokens[0].token); - console.log("Use this token on the initial setup page"); - console.log("================================"); + if (existingToken) { + showSetupToken(existingToken.token, "EXISTS"); return; } @@ -64,10 +107,7 @@ export async function ensureSetupToken() { dateUsed: null }); - console.log("=== SETUP TOKEN GENERATED ==="); - console.log("Token:", token); - console.log("Use this token on the initial setup page"); - console.log("================================"); + showSetupToken(token, "GENERATED"); } catch (error) { console.error("Failed to ensure setup token:", error); throw error; From abe76e500286d683836d5df3b0d1884bbb165597 Mon Sep 17 00:00:00 2001 From: Varun Narravula Date: Mon, 15 Dec 2025 05:29:56 -0800 Subject: [PATCH 077/153] ci: parallelize test workflow --- .github/workflows/test.yml | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b0627cc..41d43bd9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,11 +12,12 @@ on: jobs: test: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + - name: Install Node + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: '22' @@ -57,8 +58,26 @@ jobs: echo "App failed to start" exit 1 + build-sqlite: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Copy config file + run: cp config/config.example.yml config/config.yml + - name: Build Docker image sqlite - run: make build-sqlite + run: make dev-build-sqlite + + build-postgres: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Copy config file + run: cp config/config.example.yml config/config.yml - name: Build Docker image pg - run: make build-pg + run: make dev-build-pg From 9125a7bccbd80c7d0d3f8bc3376a1903960ad234 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 15 Dec 2025 23:18:28 +0100 Subject: [PATCH 078/153] =?UTF-8?q?=F0=9F=9A=A7=20org=20settings=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 4 +- src/app/[orgId]/settings/general/page.tsx | 500 +++++++++++----------- 2 files changed, 261 insertions(+), 243 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 3601d6bd..ee1a7c20 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1853,6 +1853,8 @@ "enableTwoFactorAuthentication": "Enable two-factor authentication", "completeSecuritySteps": "Complete Security Steps", "securitySettings": "Security Settings", + "dangerSection": "Danger section", + "dangerSectionDescription": "Delete organization alongside all its sites, clients, resources, etc...", "securitySettingsDescription": "Configure security policies for the organization", "requireTwoFactorForAllUsers": "Require Two-Factor Authentication for All Users", "requireTwoFactorDescription": "When enabled, all internal users in this organization must have two-factor authentication enabled to access the organization.", @@ -2063,7 +2065,7 @@ "request": "Request", "requests": "Requests", "logs": "Logs", - "logsSettingsDescription": "Monitor logs collected from this orginization", + "logsSettingsDescription": "Monitor logs collected from this organization", "searchLogs": "Search logs...", "action": "Action", "actor": "Actor", diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 97dd4a03..576589a1 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -368,9 +368,9 @@ export default function GeneralPage() { /> - - +
+ {t("logRetention")} @@ -587,266 +587,258 @@ export default function GeneralPage() { )} -
- {build !== "oss" && ( - - - - {t("securitySettings")} - - - {t("securitySettingsDescription")} - - - - - - { - const isDisabled = - isSecurityFeatureDisabled(); + {build !== "oss" && ( + <> +
+ + + {t("securitySettings")} + + + {t("securitySettingsDescription")} + + + + + + { + const isDisabled = + isSecurityFeatureDisabled(); - return ( - -
+ return ( + +
+ + { + if ( + !isDisabled + ) { + form.setValue( + "requireTwoFactor", + val + ); + } + }} + /> + +
+ + + {t( + "requireTwoFactorDescription" + )} + +
+ ); + }} + /> + { + const isDisabled = + isSecurityFeatureDisabled(); + + return ( + + + {t( + "maxSessionLength" + )} + - { if ( !isDisabled ) { + const numValue = + value === + "null" + ? null + : parseInt( + value, + 10 + ); form.setValue( - "requireTwoFactor", - val + "maxSessionLengthHours", + numValue ); } }} - /> + disabled={ + isDisabled + } + > + + + + + {SESSION_LENGTH_OPTIONS.map( + ( + option + ) => ( + + {t( + option.labelKey + )} + + ) + )} + + -
- - - {t( - "requireTwoFactorDescription" - )} - -
- ); - }} - /> - { - const isDisabled = - isSecurityFeatureDisabled(); - - return ( - - - {t("maxSessionLength")} - - - - - - - {t( - "maxSessionLengthDescription" - )} - - - ); - }} - /> - { - const isDisabled = - isSecurityFeatureDisabled(); - - return ( - - - {t( - "passwordExpiryDays" - )} - - - - - - {t( - "editPasswordExpiryDescription" - )} - - - ); - }} - /> -
-
-
- )} + + {t( + "maxSessionLengthDescription" + )} + + + ); + }} + /> + { + const isDisabled = + isSecurityFeatureDisabled(); + + return ( + + + {t( + "passwordExpiryDays" + )} + + + + + + + {t( + "editPasswordExpiryDescription" + )} + + + ); + }} + /> + + + + )} + {build === "saas" && }
- {build !== "saas" && ( - - )}
+ + {build !== "saas" && ( + + + + {t("dangerSection")} + + + {t("dangerSectionDescription")} + + +
+ +
+
+ )} ); } From 872bb557c21673d7321f0a85e7a49e03139be0ad Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 15 Dec 2025 23:36:13 +0100 Subject: [PATCH 079/153] =?UTF-8?q?=F0=9F=92=84=20put=20save=20org=20setti?= =?UTF-8?q?ngs=20button=20into=20the=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[orgId]/settings/general/page.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 576589a1..24176dbb 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -832,23 +832,23 @@ export default function GeneralPage() { )} + +
+ +
{build === "saas" && } -
- -
- {build !== "saas" && ( From 0e3b6b90b7e55e8fd66f1937bead23813392fef2 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 15 Dec 2025 17:43:45 -0500 Subject: [PATCH 080/153] Send reply to email in support requests --- server/emails/sendEmail.ts | 2 ++ server/private/routers/misc/sendSupportEmail.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/server/emails/sendEmail.ts b/server/emails/sendEmail.ts index c8a0b077..32a5fb47 100644 --- a/server/emails/sendEmail.ts +++ b/server/emails/sendEmail.ts @@ -10,6 +10,7 @@ export async function sendEmail( from: string | undefined; to: string | undefined; subject: string; + replyTo?: string; } ) { if (!emailClient) { @@ -32,6 +33,7 @@ export async function sendEmail( address: opts.from }, to: opts.to, + replyTo: opts.replyTo, subject: opts.subject, html: emailHtml }); diff --git a/server/private/routers/misc/sendSupportEmail.ts b/server/private/routers/misc/sendSupportEmail.ts index 404a2501..cd37560d 100644 --- a/server/private/routers/misc/sendSupportEmail.ts +++ b/server/private/routers/misc/sendSupportEmail.ts @@ -66,6 +66,7 @@ export async function sendSupportEmail( { name: req.user?.email || "Support User", to: "support@pangolin.net", + replyTo: req.user?.email || undefined, from: config.getNoReplyEmail(), subject: `Support Request: ${subject}` } From 23a768878918453f33d8730eefc412fc040da7da Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Mon, 15 Dec 2025 23:51:06 +0100 Subject: [PATCH 081/153] =?UTF-8?q?=F0=9F=92=84=20more=20margin=20top?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx index 09e5d918..ada2defe 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx @@ -1131,7 +1131,7 @@ function ProxyResourceTargetsForm({ )} -
+ - {r.type !== "internal" && ( + {r.type === "internal" && ( { generatePasswordResetCode(r.id); From 778e6bf6238f3b4cb22e6f5d77ca293dee88cde3 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 16 Dec 2025 00:27:24 +0100 Subject: [PATCH 083/153] =?UTF-8?q?=F0=9F=92=84=20lower=20margin=20y?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[orgId]/settings/general/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index 24176dbb..ff8a103d 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -369,7 +369,7 @@ export default function GeneralPage() { -
+
@@ -590,7 +590,7 @@ export default function GeneralPage() { {build !== "oss" && ( <> -
+
{t("securitySettings")} From 0d14cb853e9248fe7fca179a2bb6cd6bd21b6fb2 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 16 Dec 2025 01:53:06 +0100 Subject: [PATCH 084/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20invalidate=20every?= =?UTF-8?q?thing=20&=20fix=20use=20effect=20condition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../proxy/[niceId]/authentication/page.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) 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 8de6aa13..7ace8450 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -217,7 +217,7 @@ export default function ResourceAuthenticationPage() { const hasInitializedRef = useRef(false); useEffect(() => { - if (!pageLoading || hasInitializedRef.current) return; + if (pageLoading || hasInitializedRef.current) return; usersRolesForm.setValue( "roles", @@ -307,6 +307,17 @@ export default function ResourceAuthenticationPage() { title: t("resourceAuthSettingsSave"), description: t("resourceAuthSettingsSaveDescription") }); + await queryClient.invalidateQueries({ + predicate(query) { + const resourceKey = resourceQueries.resourceClients({ + resourceId: resource.resourceId + }).queryKey; + return ( + query.queryKey[0] === resourceKey[0] && + query.queryKey[1] === resourceKey[1] + ); + } + }); router.refresh(); } catch (e) { console.error(e); From 8dad38775c29c8f8d6e15e091edfb00df343b889 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 16 Dec 2025 01:53:20 +0100 Subject: [PATCH 085/153] =?UTF-8?q?=F0=9F=90=9B=20use=20`/resource`=20inst?= =?UTF-8?q?ead=20of=20`/site-resource`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/queries.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 541091f4..513e2e4f 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -228,7 +228,7 @@ export const resourceQueries = { queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< AxiosResponse - >(`/site-resource/${resourceId}/users`, { signal }); + >(`/resource/${resourceId}/users`, { signal }); return res.data.data.users; } }), @@ -238,7 +238,7 @@ export const resourceQueries = { queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< AxiosResponse - >(`/site-resource/${resourceId}/roles`, { signal }); + >(`/resource/${resourceId}/roles`, { signal }); return res.data.data.roles; } @@ -249,7 +249,7 @@ export const resourceQueries = { queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< AxiosResponse - >(`/site-resource/${resourceId}/clients`, { signal }); + >(`/resource/${resourceId}/clients`, { signal }); return res.data.data.clients; } From e94f21bc059aa180cbb6bafd46ad8ebb66560ede Mon Sep 17 00:00:00 2001 From: Varun Narravula Date: Mon, 15 Dec 2025 05:29:56 -0800 Subject: [PATCH 086/153] ci: parallelize test workflow --- .github/workflows/test.yml | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b0627cc..41d43bd9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,11 +12,12 @@ on: jobs: test: runs-on: ubuntu-latest - steps: - - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - - uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 + - name: Install Node + uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 with: node-version: '22' @@ -57,8 +58,26 @@ jobs: echo "App failed to start" exit 1 + build-sqlite: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Copy config file + run: cp config/config.example.yml config/config.yml + - name: Build Docker image sqlite - run: make build-sqlite + run: make dev-build-sqlite + + build-postgres: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Copy config file + run: cp config/config.example.yml config/config.yml - name: Build Docker image pg - run: make build-pg + run: make dev-build-pg From c44c1a551877a655e4e296970deb9fb4a89a89be Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 15 Dec 2025 21:02:04 -0500 Subject: [PATCH 087/153] Add UI, update API, send to newt --- messages/en-US.json | 9 +- server/db/pg/schema/schema.ts | 4 +- server/db/sqlite/schema/schema.ts | 4 +- server/lib/ip.ts | 136 ++++++++- .../siteResource/createSiteResource.ts | 14 +- .../siteResource/listAllSiteResourcesByOrg.ts | 2 + .../siteResource/updateSiteResource.ts | 15 +- .../settings/resources/client/page.tsx | 4 +- src/components/ClientResourcesTable.tsx | 2 + .../CreateInternalResourceDialog.tsx | 242 +++++++++++++++ src/components/EditInternalResourceDialog.tsx | 279 ++++++++++++++++++ 11 files changed, 689 insertions(+), 22 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index ff316a98..ee26c280 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2275,5 +2275,12 @@ "agent": "Agent", "personalUseOnly": "Personal Use Only", "loginPageLicenseWatermark": "This instance is licensed for personal use only.", - "instanceIsUnlicensed": "This instance is unlicensed." + "instanceIsUnlicensed": "This instance is unlicensed.", + "portRestrictions": "Port Restrictions", + "allPorts": "All", + "custom": "Custom", + "allPortsAllowed": "All Ports Allowed", + "allPortsBlocked": "All Ports Blocked", + "tcpPortsDescription": "Specify which TCP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 80,443,8000-9000).", + "udpPortsDescription": "Specify which UDP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 53,123,500-600)." } diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 71877f2f..34562e7d 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -213,7 +213,9 @@ export const siteResources = pgTable("siteResources", { destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode enabled: boolean("enabled").notNull().default(true), alias: varchar("alias"), - aliasAddress: varchar("aliasAddress") + aliasAddress: varchar("aliasAddress"), + tcpPortRangeString: varchar("tcpPortRangeString"), + udpPortRangeString: varchar("udpPortRangeString") }); export const clientSiteResources = pgTable("clientSiteResources", { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 6e17cac4..ab754bc9 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -234,7 +234,9 @@ export const siteResources = sqliteTable("siteResources", { destination: text("destination").notNull(), // ip, cidr, hostname enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), alias: text("alias"), - aliasAddress: text("aliasAddress") + aliasAddress: text("aliasAddress"), + tcpPortRangeString: text("tcpPortRangeString"), + udpPortRangeString: text("udpPortRangeString") }); export const clientSiteResources = sqliteTable("clientSiteResources", { diff --git a/server/lib/ip.ts b/server/lib/ip.ts index 9c412801..2bf3e0e8 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -1,10 +1,4 @@ -import { - clientSitesAssociationsCache, - db, - SiteResource, - siteResources, - Transaction -} from "@server/db"; +import { db, SiteResource, siteResources, Transaction } from "@server/db"; import { clients, orgs, sites } from "@server/db"; import { and, eq, isNotNull } from "drizzle-orm"; import config from "@server/lib/config"; @@ -476,6 +470,7 @@ export type SubnetProxyTarget = { portRange?: { min: number; max: number; + protocol: "tcp" | "udp"; }[]; }; @@ -505,6 +500,10 @@ export function generateSubnetProxyTargets( } const clientPrefix = `${clientSite.subnet.split("/")[0]}/32`; + const portRange = [ + ...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"), + ...parsePortRangeString(siteResource.udpPortRangeString, "udp") + ]; if (siteResource.mode == "host") { let destination = siteResource.destination; @@ -515,7 +514,8 @@ export function generateSubnetProxyTargets( targets.push({ sourcePrefix: clientPrefix, - destPrefix: destination + destPrefix: destination, + portRange }); } @@ -524,13 +524,15 @@ export function generateSubnetProxyTargets( targets.push({ sourcePrefix: clientPrefix, destPrefix: `${siteResource.aliasAddress}/32`, - rewriteTo: destination + rewriteTo: destination, + portRange }); } } else if (siteResource.mode == "cidr") { targets.push({ sourcePrefix: clientPrefix, - destPrefix: siteResource.destination + destPrefix: siteResource.destination, + portRange }); } } @@ -542,3 +544,117 @@ export function generateSubnetProxyTargets( return targets; } + +// Custom schema for validating port range strings +// Format: "80,443,8000-9000" or "*" for all ports, or empty string +export const portRangeStringSchema = z + .string() + .optional() + .refine( + (val) => { + if (!val || val.trim() === "" || val.trim() === "*") { + return true; + } + + // Split by comma and validate each part + const parts = val.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part === "") { + return false; // empty parts not allowed + } + + // Check if it's a range (contains dash) + if (part.includes("-")) { + const [start, end] = part.split("-").map((p) => p.trim()); + + // Both parts must be present + if (!start || !end) { + return false; + } + + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + + // Must be valid numbers + if (isNaN(startPort) || isNaN(endPort)) { + return false; + } + + // Must be valid port range (1-65535) + if ( + startPort < 1 || + startPort > 65535 || + endPort < 1 || + endPort > 65535 + ) { + return false; + } + + // Start must be <= end + if (startPort > endPort) { + return false; + } + } else { + // Single port + const port = parseInt(part, 10); + + // Must be a valid number + if (isNaN(port)) { + return false; + } + + // Must be valid port range (1-65535) + if (port < 1 || port > 65535) { + return false; + } + } + } + + return true; + }, + { + message: + 'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535, and ranges must have start <= end.' + } + ); + +/** + * Parses a port range string into an array of port range objects + * @param portRangeStr - Port range string (e.g., "80,443,8000-9000", "*", or "") + * @param protocol - Protocol to use for all ranges (default: "tcp") + * @returns Array of port range objects with min, max, and protocol fields + */ +export function parsePortRangeString( + portRangeStr: string | undefined | null, + protocol: "tcp" | "udp" = "tcp" +): { min: number; max: number; protocol: "tcp" | "udp" }[] { + // Handle undefined or empty string - insert dummy value with port 0 + if (!portRangeStr || portRangeStr.trim() === "") { + return [{ min: 0, max: 0, protocol }]; + } + + // Handle wildcard - return empty array (all ports allowed) + if (portRangeStr.trim() === "*") { + return []; + } + + const result: { min: number; max: number; protocol: "tcp" | "udp" }[] = []; + const parts = portRangeStr.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part.includes("-")) { + // Range + const [start, end] = part.split("-").map((p) => p.trim()); + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + result.push({ min: startPort, max: endPort, protocol }); + } else { + // Single port + const port = parseInt(part, 10); + result.push({ min: port, max: port, protocol }); + } + } + + return result; +} diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index e9ce8e04..370037cb 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -10,7 +10,7 @@ import { userSiteResources } from "@server/db"; import { getUniqueSiteResourceName } from "@server/db/names"; -import { getNextAvailableAliasAddress } from "@server/lib/ip"; +import { getNextAvailableAliasAddress, portRangeStringSchema } from "@server/lib/ip"; import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations"; import response from "@server/lib/response"; import logger from "@server/logger"; @@ -45,7 +45,9 @@ const createSiteResourceSchema = z .optional(), userIds: z.array(z.string()), roleIds: z.array(z.int()), - clientIds: z.array(z.int()) + clientIds: z.array(z.int()), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema }) .strict() .refine( @@ -154,7 +156,9 @@ export async function createSiteResource( alias, userIds, roleIds, - clientIds + clientIds, + tcpPortRangeString, + udpPortRangeString } = parsedBody.data; // Verify the site exists and belongs to the org @@ -239,7 +243,9 @@ export async function createSiteResource( destination, enabled, alias, - aliasAddress + aliasAddress, + tcpPortRangeString, + udpPortRangeString }) .returning(); diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index f6975cd2..4fc96533 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -97,6 +97,8 @@ export async function listAllSiteResourcesByOrg( destination: siteResources.destination, enabled: siteResources.enabled, alias: siteResources.alias, + tcpPortRangeString: siteResources.tcpPortRangeString, + udpPortRangeString: siteResources.udpPortRangeString, siteName: sites.name, siteNiceId: sites.niceId, siteAddress: sites.address diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 92704adb..8ecd8bf0 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -23,7 +23,8 @@ import { updatePeerData, updateTargets } from "@server/routers/client/targets"; import { generateAliasConfig, generateRemoteSubnets, - generateSubnetProxyTargets + generateSubnetProxyTargets, + portRangeStringSchema } from "@server/lib/ip"; import { getClientSiteResourceAccess, @@ -55,7 +56,9 @@ const updateSiteResourceSchema = z .nullish(), userIds: z.array(z.string()), roleIds: z.array(z.int()), - clientIds: z.array(z.int()) + clientIds: z.array(z.int()), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema }) .strict() .refine( @@ -160,7 +163,9 @@ export async function updateSiteResource( enabled, userIds, roleIds, - clientIds + clientIds, + tcpPortRangeString, + udpPortRangeString } = parsedBody.data; const [site] = await db @@ -226,7 +231,9 @@ export async function updateSiteResource( mode: mode, destination: destination, enabled: enabled, - alias: alias && alias.trim() ? alias : null + alias: alias && alias.trim() ? alias : null, + tcpPortRangeString: tcpPortRangeString, + udpPortRangeString: udpPortRangeString }) .where( and( diff --git a/src/app/[orgId]/settings/resources/client/page.tsx b/src/app/[orgId]/settings/resources/client/page.tsx index 49ccb97f..1628e689 100644 --- a/src/app/[orgId]/settings/resources/client/page.tsx +++ b/src/app/[orgId]/settings/resources/client/page.tsx @@ -67,7 +67,9 @@ export default async function ClientResourcesPage( // destinationPort: siteResource.destinationPort, alias: siteResource.alias || null, siteNiceId: siteResource.siteNiceId, - niceId: siteResource.niceId + niceId: siteResource.niceId, + tcpPortRangeString: siteResource.tcpPortRangeString || null, + udpPortRangeString: siteResource.udpPortRangeString || null }; } ); diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index 3f65c762..8ca1cc21 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -41,6 +41,8 @@ export type InternalResourceRow = { // destinationPort: number | null; alias: string | null; niceId: string; + tcpPortRangeString: string | null; + udpPortRangeString: string | null; }; type ClientResourcesTableProps = { diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index 91ef26da..6aad4123 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -59,6 +59,82 @@ import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; +import { InfoPopup } from "@app/components/ui/info-popup"; + +// Helper to validate port range string format +const isValidPortRangeString = (val: string | undefined | null): boolean => { + if (!val || val.trim() === "" || val.trim() === "*") { + return true; + } + + const parts = val.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part === "") { + return false; + } + + if (part.includes("-")) { + const [start, end] = part.split("-").map((p) => p.trim()); + if (!start || !end) { + return false; + } + + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + + if (isNaN(startPort) || isNaN(endPort)) { + return false; + } + + if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) { + return false; + } + + if (startPort > endPort) { + return false; + } + } else { + const port = parseInt(part, 10); + if (isNaN(port)) { + return false; + } + if (port < 1 || port > 65535) { + return false; + } + } + } + + return true; +}; + +// Port range string schema for client-side validation +const portRangeStringSchema = z + .string() + .optional() + .nullable() + .refine( + (val) => isValidPortRangeString(val), + { + message: + 'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535.' + } + ); + +// Helper to determine the port mode from a port range string +type PortMode = "all" | "blocked" | "custom"; +const getPortModeFromString = (val: string | undefined | null): PortMode => { + if (val === "*") return "all"; + if (!val || val.trim() === "") return "blocked"; + return "custom"; +}; + +// Helper to get the port string for API from mode and custom value +const getPortStringFromMode = (mode: PortMode, customValue: string): string | undefined => { + if (mode === "all") return "*"; + if (mode === "blocked") return ""; + return customValue; +}; type Site = ListSitesResponse["sites"][0]; @@ -103,6 +179,8 @@ export default function CreateInternalResourceDialog({ // .max(65535, t("createInternalResourceDialogDestinationPortMax")) // .nullish(), alias: z.string().nullish(), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema, roles: z .array( z.object({ @@ -209,6 +287,12 @@ export default function CreateInternalResourceDialog({ number | null >(null); + // Port restriction UI state - default to "all" (*) for new resources + const [tcpPortMode, setTcpPortMode] = useState("all"); + const [udpPortMode, setUdpPortMode] = useState("all"); + const [tcpCustomPorts, setTcpCustomPorts] = useState(""); + const [udpCustomPorts, setUdpCustomPorts] = useState(""); + const availableSites = sites.filter( (site) => site.type === "newt" && site.subnet ); @@ -224,6 +308,8 @@ export default function CreateInternalResourceDialog({ destination: "", // destinationPort: undefined, alias: "", + tcpPortRangeString: "*", + udpPortRangeString: "*", roles: [], users: [], clients: [] @@ -232,6 +318,17 @@ export default function CreateInternalResourceDialog({ const mode = form.watch("mode"); + // Update form values when port mode or custom ports change + useEffect(() => { + const tcpValue = getPortStringFromMode(tcpPortMode, tcpCustomPorts); + form.setValue("tcpPortRangeString", tcpValue); + }, [tcpPortMode, tcpCustomPorts, form]); + + useEffect(() => { + const udpValue = getPortStringFromMode(udpPortMode, udpCustomPorts); + form.setValue("udpPortRangeString", udpValue); + }, [udpPortMode, udpCustomPorts, form]); + // Helper function to check if destination contains letters (hostname vs IP) const isHostname = (destination: string): boolean => { return /[a-zA-Z]/.test(destination); @@ -258,10 +355,17 @@ export default function CreateInternalResourceDialog({ destination: "", // destinationPort: undefined, alias: "", + tcpPortRangeString: "*", + udpPortRangeString: "*", roles: [], users: [], clients: [] }); + // Reset port mode state + setTcpPortMode("all"); + setUdpPortMode("all"); + setTcpCustomPorts(""); + setUdpCustomPorts(""); } }, [open]); @@ -304,6 +408,8 @@ export default function CreateInternalResourceDialog({ data.alias.trim() ? data.alias : undefined, + tcpPortRangeString: data.tcpPortRangeString, + udpPortRangeString: data.udpPortRangeString, roleIds: data.roles ? data.roles.map((r) => parseInt(r.id)) : [], @@ -727,6 +833,142 @@ export default function CreateInternalResourceDialog({
)} + {/* Port Restrictions Section */} +
+

+ {t("portRestrictions")} +

+
+ {/* TCP Ports */} + ( + +
+ + TCP + + +
+
+ + {tcpPortMode === "custom" ? ( + + + setTcpCustomPorts(e.target.value) + } + className="flex-1" + /> + + ) : ( + + )} +
+ +
+ )} + /> + + {/* UDP Ports */} + ( + +
+ + UDP + + +
+
+ + {udpPortMode === "custom" ? ( + + + setUdpCustomPorts(e.target.value) + } + className="flex-1" + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+ {/* Access Control Section */}

diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index f793d147..cfa60463 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -47,6 +47,82 @@ import { AxiosResponse } from "axios"; import { UserType } from "@server/types/UserTypes"; import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import { orgQueries, resourceQueries } from "@app/lib/queries"; +import { InfoPopup } from "@app/components/ui/info-popup"; + +// Helper to validate port range string format +const isValidPortRangeString = (val: string | undefined | null): boolean => { + if (!val || val.trim() === "" || val.trim() === "*") { + return true; + } + + const parts = val.split(",").map((p) => p.trim()); + + for (const part of parts) { + if (part === "") { + return false; + } + + if (part.includes("-")) { + const [start, end] = part.split("-").map((p) => p.trim()); + if (!start || !end) { + return false; + } + + const startPort = parseInt(start, 10); + const endPort = parseInt(end, 10); + + if (isNaN(startPort) || isNaN(endPort)) { + return false; + } + + if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) { + return false; + } + + if (startPort > endPort) { + return false; + } + } else { + const port = parseInt(part, 10); + if (isNaN(port)) { + return false; + } + if (port < 1 || port > 65535) { + return false; + } + } + } + + return true; +}; + +// Port range string schema for client-side validation +const portRangeStringSchema = z + .string() + .optional() + .nullable() + .refine( + (val) => isValidPortRangeString(val), + { + message: + 'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535.' + } + ); + +// Helper to determine the port mode from a port range string +type PortMode = "all" | "blocked" | "custom"; +const getPortModeFromString = (val: string | undefined | null): PortMode => { + if (val === "*") return "all"; + if (!val || val.trim() === "") return "blocked"; + return "custom"; +}; + +// Helper to get the port string for API from mode and custom value +const getPortStringFromMode = (mode: PortMode, customValue: string): string | undefined => { + if (mode === "all") return "*"; + if (mode === "blocked") return ""; + return customValue; +}; type InternalResourceData = { id: number; @@ -61,6 +137,8 @@ type InternalResourceData = { destination: string; // destinationPort?: number | null; alias?: string | null; + tcpPortRangeString?: string | null; + udpPortRangeString?: string | null; }; type EditInternalResourceDialogProps = { @@ -94,6 +172,8 @@ export default function EditInternalResourceDialog({ destination: z.string().min(1), // destinationPort: z.int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(), alias: z.string().nullish(), + tcpPortRangeString: portRangeStringSchema, + udpPortRangeString: portRangeStringSchema, roles: z .array( z.object({ @@ -255,6 +335,24 @@ export default function EditInternalResourceDialog({ number | null >(null); + // Port restriction UI state + const [tcpPortMode, setTcpPortMode] = useState( + getPortModeFromString(resource.tcpPortRangeString) + ); + const [udpPortMode, setUdpPortMode] = useState( + getPortModeFromString(resource.udpPortRangeString) + ); + const [tcpCustomPorts, setTcpCustomPorts] = useState( + resource.tcpPortRangeString && resource.tcpPortRangeString !== "*" + ? resource.tcpPortRangeString + : "" + ); + const [udpCustomPorts, setUdpCustomPorts] = useState( + resource.udpPortRangeString && resource.udpPortRangeString !== "*" + ? resource.udpPortRangeString + : "" + ); + const form = useForm({ resolver: zodResolver(formSchema), defaultValues: { @@ -265,6 +363,8 @@ export default function EditInternalResourceDialog({ destination: resource.destination || "", // destinationPort: resource.destinationPort ?? undefined, alias: resource.alias ?? null, + tcpPortRangeString: resource.tcpPortRangeString ?? "*", + udpPortRangeString: resource.udpPortRangeString ?? "*", roles: [], users: [], clients: [] @@ -273,6 +373,17 @@ export default function EditInternalResourceDialog({ const mode = form.watch("mode"); + // Update form values when port mode or custom ports change + useEffect(() => { + const tcpValue = getPortStringFromMode(tcpPortMode, tcpCustomPorts); + form.setValue("tcpPortRangeString", tcpValue); + }, [tcpPortMode, tcpCustomPorts, form]); + + useEffect(() => { + const udpValue = getPortStringFromMode(udpPortMode, udpCustomPorts); + form.setValue("udpPortRangeString", udpValue); + }, [udpPortMode, udpCustomPorts, form]); + // Helper function to check if destination contains letters (hostname vs IP) const isHostname = (destination: string): boolean => { return /[a-zA-Z]/.test(destination); @@ -327,6 +438,8 @@ export default function EditInternalResourceDialog({ data.alias.trim() ? data.alias : null, + tcpPortRangeString: data.tcpPortRangeString, + udpPortRangeString: data.udpPortRangeString, roleIds: (data.roles || []).map((r) => parseInt(r.id)), userIds: (data.users || []).map((u) => u.id), clientIds: (data.clients || []).map((c) => parseInt(c.id)) @@ -396,10 +509,25 @@ export default function EditInternalResourceDialog({ mode: resource.mode || "host", destination: resource.destination || "", alias: resource.alias ?? null, + tcpPortRangeString: resource.tcpPortRangeString ?? "*", + udpPortRangeString: resource.udpPortRangeString ?? "*", roles: [], users: [], clients: [] }); + // Reset port mode state + setTcpPortMode(getPortModeFromString(resource.tcpPortRangeString)); + setUdpPortMode(getPortModeFromString(resource.udpPortRangeString)); + setTcpCustomPorts( + resource.tcpPortRangeString && resource.tcpPortRangeString !== "*" + ? resource.tcpPortRangeString + : "" + ); + setUdpCustomPorts( + resource.udpPortRangeString && resource.udpPortRangeString !== "*" + ? resource.udpPortRangeString + : "" + ); previousResourceId.current = resource.id; } @@ -438,10 +566,25 @@ export default function EditInternalResourceDialog({ destination: resource.destination || "", // destinationPort: resource.destinationPort ?? undefined, alias: resource.alias ?? null, + tcpPortRangeString: resource.tcpPortRangeString ?? "*", + udpPortRangeString: resource.udpPortRangeString ?? "*", roles: [], users: [], clients: [] }); + // Reset port mode state + setTcpPortMode(getPortModeFromString(resource.tcpPortRangeString)); + setUdpPortMode(getPortModeFromString(resource.udpPortRangeString)); + setTcpCustomPorts( + resource.tcpPortRangeString && resource.tcpPortRangeString !== "*" + ? resource.tcpPortRangeString + : "" + ); + setUdpCustomPorts( + resource.udpPortRangeString && resource.udpPortRangeString !== "*" + ? resource.udpPortRangeString + : "" + ); // Reset previous resource ID to ensure clean state on next open previousResourceId.current = null; } @@ -674,6 +817,142 @@ export default function EditInternalResourceDialog({

)} + {/* Port Restrictions Section */} +
+

+ {t("portRestrictions")} +

+
+ {/* TCP Ports */} + ( + +
+ + TCP + + +
+
+ + {tcpPortMode === "custom" ? ( + + + setTcpCustomPorts(e.target.value) + } + className="flex-1" + /> + + ) : ( + + )} +
+ +
+ )} + /> + + {/* UDP Ports */} + ( + +
+ + UDP + + +
+
+ + {udpPortMode === "custom" ? ( + + + setUdpCustomPorts(e.target.value) + } + className="flex-1" + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+ {/* Access Control Section */}

From 10f143749625dc06786e6e3a4137a612b6054b35 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 15 Dec 2025 21:23:40 -0500 Subject: [PATCH 088/153] Small visual adjustments --- .../CreateInternalResourceDialog.tsx | 26 ++++++--------- src/components/EditInternalResourceDialog.tsx | 33 +++++++------------ src/components/tags/autocomplete.tsx | 2 +- 3 files changed, 22 insertions(+), 39 deletions(-) diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index 6aad4123..adbb7f61 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -48,9 +48,7 @@ import { createApiClient, formatAxiosError } from "@app/lib/api"; import { cn } from "@app/lib/cn"; import { orgQueries } from "@app/lib/queries"; import { zodResolver } from "@hookform/resolvers/zod"; -import { ListClientsResponse } from "@server/routers/client/listClients"; import { ListSitesResponse } from "@server/routers/site"; -import { ListUsersResponse } from "@server/routers/user"; import { UserType } from "@server/types/UserTypes"; import { useQuery } from "@tanstack/react-query"; import { AxiosResponse } from "axios"; @@ -59,7 +57,7 @@ import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import { InfoPopup } from "@app/components/ui/info-popup"; +// import { InfoPopup } from "@app/components/ui/info-popup"; // Helper to validate port range string format const isValidPortRangeString = (val: string | undefined | null): boolean => { @@ -838,7 +836,7 @@ export default function CreateInternalResourceDialog({

{t("portRestrictions")}

-
+
{/* TCP Ports */} (
- + TCP - -
-
+ />*/} { setUdpPortMode(value); }} > - + diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index cfa60463..b5c378f1 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Button } from "@app/components/ui/button"; import { Input } from "@app/components/ui/input"; import { @@ -36,18 +36,11 @@ import { toast } from "@app/hooks/useToast"; import { useTranslations } from "next-intl"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { ListRolesResponse } from "@server/routers/role"; -import { ListUsersResponse } from "@server/routers/user"; -import { ListSiteResourceRolesResponse } from "@server/routers/siteResource/listSiteResourceRoles"; -import { ListSiteResourceUsersResponse } from "@server/routers/siteResource/listSiteResourceUsers"; -import { ListSiteResourceClientsResponse } from "@server/routers/siteResource/listSiteResourceClients"; -import { ListClientsResponse } from "@server/routers/client/listClients"; import { Tag, TagInput } from "@app/components/tags/tag-input"; -import { AxiosResponse } from "axios"; import { UserType } from "@server/types/UserTypes"; import { useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import { orgQueries, resourceQueries } from "@app/lib/queries"; -import { InfoPopup } from "@app/components/ui/info-popup"; +// import { InfoPopup } from "@app/components/ui/info-popup"; // Helper to validate port range string format const isValidPortRangeString = (val: string | undefined | null): boolean => { @@ -822,7 +815,7 @@ export default function EditInternalResourceDialog({

{t("portRestrictions")}

-
+
{/* TCP Ports */} (
- + TCP - -
-
+ />*/} { setUdpPortMode(value); }} > - + diff --git a/src/components/tags/autocomplete.tsx b/src/components/tags/autocomplete.tsx index 3230f11b..ee865eb6 100644 --- a/src/components/tags/autocomplete.tsx +++ b/src/components/tags/autocomplete.tsx @@ -308,7 +308,7 @@ export const Autocomplete: React.FC = ({ role="option" aria-selected={isSelected} className={cn( - "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent", + "relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 hover:bg-accent", isSelected && "bg-accent text-accent-foreground", classStyleProps?.commandItem From 0c0ad7029fe0769d8213776f1162716f83f7ca95 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 15 Dec 2025 21:44:09 -0500 Subject: [PATCH 089/153] Batch and delay for large amounts of targets --- server/routers/client/targets.ts | 67 +++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts index b7b91925..653a2578 100644 --- a/server/routers/client/targets.ts +++ b/server/routers/client/targets.ts @@ -4,21 +4,48 @@ import { Alias, SubnetProxyTarget } from "@server/lib/ip"; import logger from "@server/logger"; import { eq } from "drizzle-orm"; +const BATCH_SIZE = 50; +const BATCH_DELAY_MS = 50; + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function chunkArray(array: T[], size: number): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += size) { + chunks.push(array.slice(i, i + size)); + } + return chunks; +} + export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) { - await sendToClient(newtId, { - type: `newt/wg/targets/add`, - data: targets - }); + const batches = chunkArray(targets, BATCH_SIZE); + for (let i = 0; i < batches.length; i++) { + if (i > 0) { + await sleep(BATCH_DELAY_MS); + } + await sendToClient(newtId, { + type: `newt/wg/targets/add`, + data: batches[i] + }); + } } export async function removeTargets( newtId: string, targets: SubnetProxyTarget[] ) { - await sendToClient(newtId, { - type: `newt/wg/targets/remove`, - data: targets - }); + const batches = chunkArray(targets, BATCH_SIZE); + for (let i = 0; i < batches.length; i++) { + if (i > 0) { + await sleep(BATCH_DELAY_MS); + } + await sendToClient(newtId, { + type: `newt/wg/targets/remove`, + data: batches[i] + }); + } } export async function updateTargets( @@ -28,12 +55,24 @@ export async function updateTargets( newTargets: SubnetProxyTarget[]; } ) { - await sendToClient(newtId, { - type: `newt/wg/targets/update`, - data: targets - }).catch((error) => { - logger.warn(`Error sending message:`, error); - }); + const oldBatches = chunkArray(targets.oldTargets, BATCH_SIZE); + const newBatches = chunkArray(targets.newTargets, BATCH_SIZE); + const maxBatches = Math.max(oldBatches.length, newBatches.length); + + for (let i = 0; i < maxBatches; i++) { + if (i > 0) { + await sleep(BATCH_DELAY_MS); + } + await sendToClient(newtId, { + type: `newt/wg/targets/update`, + data: { + oldTargets: oldBatches[i] || [], + newTargets: newBatches[i] || [] + } + }).catch((error) => { + logger.warn(`Error sending message:`, error); + }); + } } export async function addPeerData( From 1b4884afd8a4bf53743bb2585bf202603af5f510 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 15 Dec 2025 22:12:23 -0500 Subject: [PATCH 090/153] Make sure to push changes --- server/routers/siteResource/updateSiteResource.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 8ecd8bf0..fb2065c5 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -355,10 +355,14 @@ export async function handleMessagingForUpdatedSiteResource( const aliasChanged = existingSiteResource && existingSiteResource.alias !== updatedSiteResource.alias; + const portRangesChanged = + existingSiteResource && + (existingSiteResource.tcpPortRangeString !== updatedSiteResource.tcpPortRangeString || + existingSiteResource.udpPortRangeString !== updatedSiteResource.udpPortRangeString); // if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all - if (destinationChanged || aliasChanged) { + if (destinationChanged || aliasChanged || portRangesChanged) { const [newt] = await trx .select() .from(newts) @@ -372,7 +376,7 @@ export async function handleMessagingForUpdatedSiteResource( } // Only update targets on newt if destination changed - if (destinationChanged) { + if (destinationChanged || portRangesChanged) { const oldTargets = generateSubnetProxyTargets( existingSiteResource, mergedAllClients From 7f7f6eeaea3e09c14cf429e077d7c95bb02bf9c1 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 16 Dec 2025 10:42:32 -0500 Subject: [PATCH 091/153] Check the postgres string first Fixes #2092 --- Dockerfile | 16 +++++++++------- server/db/pg/driver.ts | 36 ++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/Dockerfile b/Dockerfile index fa2d71c0..c59490b6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,23 +43,25 @@ RUN test -f dist/server.mjs RUN npm run build:cli +# Prune dev dependencies and clean up to prepare for copy to runner +RUN npm prune --omit=dev && npm cache clean --force + FROM node:24-alpine AS runner WORKDIR /app -# Curl used for the health checks -# Python and build tools needed for better-sqlite3 native compilation -RUN apk add --no-cache curl tzdata python3 make g++ +# Only curl and tzdata needed at runtime - no build tools! +RUN apk add --no-cache curl tzdata -# COPY package.json package-lock.json ./ -COPY package*.json ./ - -RUN npm ci --omit=dev && npm cache clean --force +# Copy pre-built node_modules from builder (already pruned to production only) +# This includes the compiled native modules like better-sqlite3 +COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/dist ./dist COPY --from=builder /app/init ./dist/init +COPY --from=builder /app/package.json ./package.json COPY ./cli/wrapper.sh /usr/local/bin/pangctl RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts index 9456effb..2ee34da6 100644 --- a/server/db/pg/driver.ts +++ b/server/db/pg/driver.ts @@ -6,28 +6,28 @@ import { withReplicas } from "drizzle-orm/pg-core"; function createDb() { const config = readConfigFile(); - if (!config.postgres) { - // check the environment variables for postgres config - if (process.env.POSTGRES_CONNECTION_STRING) { - config.postgres = { - connection_string: process.env.POSTGRES_CONNECTION_STRING - }; - if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) { - const replicas = - process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split( - "," - ).map((conn) => ({ + // check the environment variables for postgres config first before the config file + if (process.env.POSTGRES_CONNECTION_STRING) { + config.postgres = { + connection_string: process.env.POSTGRES_CONNECTION_STRING + }; + if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) { + const replicas = + process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(",").map( + (conn) => ({ connection_string: conn.trim() - })); - config.postgres.replicas = replicas; - } - } else { - throw new Error( - "Postgres configuration is missing in the configuration file." - ); + }) + ); + config.postgres.replicas = replicas; } } + if (!config.postgres) { + throw new Error( + "Postgres configuration is missing in the configuration file." + ); + } + const connectionString = config.postgres?.connection_string; const replicaConnections = config.postgres?.replicas || []; From 6072ee93faad68b4ee520224a470f7caeb0eb840 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Tue, 16 Dec 2025 17:16:53 -0500 Subject: [PATCH 092/153] add remove invitation to integration api --- messages/en-US.json | 1 + server/routers/integration.ts | 8 ++++++++ server/routers/user/removeInvitation.ts | 12 ++++++++++++ src/components/PermissionsSelectBox.tsx | 1 + 4 files changed, 22 insertions(+) diff --git a/messages/en-US.json b/messages/en-US.json index ee26c280..148db379 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1035,6 +1035,7 @@ "updateOrgUser": "Update Org User", "createOrgUser": "Create Org User", "actionUpdateOrg": "Update Organization", + "actionRemoveInvitation": "Remove Invitation", "actionUpdateUser": "Update User", "actionGetUser": "Get User", "actionGetOrgUser": "Get Organization User", diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 878d61fa..6301bb6d 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -352,6 +352,14 @@ authenticated.post( user.inviteUser ); +authenticated.delete( + "/org/:orgId/invitations/:inviteId", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.removeInvitation), + logActionAudit(ActionsEnum.removeInvitation), + user.removeInvitation +); + authenticated.get( "/resource/:resourceId/roles", verifyApiKeyResourceAccess, diff --git a/server/routers/user/removeInvitation.ts b/server/routers/user/removeInvitation.ts index 6a000afc..ab6a96d2 100644 --- a/server/routers/user/removeInvitation.ts +++ b/server/routers/user/removeInvitation.ts @@ -8,12 +8,24 @@ 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 removeInvitationParamsSchema = z.strictObject({ orgId: z.string(), inviteId: z.string() }); +registry.registerPath({ + method: "delete", + path: "/org/{orgId}/invitations/{inviteId}", + description: "Remove an open invitation from an organization", + tags: [OpenAPITags.Org], + request: { + params: removeInvitationParamsSchema + }, + responses: {} +}); + export async function removeInvitation( req: Request, res: Response, diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index 49a10215..4862d780 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -27,6 +27,7 @@ function getActionsCategories(root: boolean) { [t("actionUpdateOrg")]: "updateOrg", [t("actionGetOrgUser")]: "getOrgUser", [t("actionInviteUser")]: "inviteUser", + [t("actionRemoveInvitation")]: "removeInvitation", [t("actionListInvitations")]: "listInvitations", [t("actionRemoveUser")]: "removeUser", [t("actionListUsers")]: "listUsers", From 3d5ae9dd5cdb28d1b7e35abb0b348b067d1d9e9e Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 16 Dec 2025 17:14:00 -0500 Subject: [PATCH 093/153] Disable icmp packets over private resources --- server/db/pg/schema/schema.ts | 3 +- server/db/sqlite/schema/schema.ts | 3 +- server/lib/ip.ts | 11 ++++-- .../siteResource/createSiteResource.ts | 9 +++-- .../siteResource/listAllSiteResourcesByOrg.ts | 1 + .../siteResource/updateSiteResource.ts | 17 +++++++--- .../settings/resources/client/page.tsx | 3 +- src/components/ClientResourcesTable.tsx | 1 + .../CreateInternalResourceDialog.tsx | 32 ++++++++++++++++- src/components/EditInternalResourceDialog.tsx | 34 ++++++++++++++++++- 10 files changed, 98 insertions(+), 16 deletions(-) diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 34562e7d..e8077754 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -215,7 +215,8 @@ export const siteResources = pgTable("siteResources", { alias: varchar("alias"), aliasAddress: varchar("aliasAddress"), tcpPortRangeString: varchar("tcpPortRangeString"), - udpPortRangeString: varchar("udpPortRangeString") + udpPortRangeString: varchar("udpPortRangeString"), + disableIcmp: boolean("disableIcmp").notNull().default(false) }); export const clientSiteResources = pgTable("clientSiteResources", { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index ab754bc9..de8ad8d0 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -236,7 +236,8 @@ export const siteResources = sqliteTable("siteResources", { alias: text("alias"), aliasAddress: text("aliasAddress"), tcpPortRangeString: text("tcpPortRangeString"), - udpPortRangeString: text("udpPortRangeString") + udpPortRangeString: text("udpPortRangeString"), + disableIcmp: integer("disableIcmp", { mode: "boolean" }) }); export const clientSiteResources = sqliteTable("clientSiteResources", { diff --git a/server/lib/ip.ts b/server/lib/ip.ts index 2bf3e0e8..21c148ac 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -466,6 +466,7 @@ export function generateAliasConfig(allSiteResources: SiteResource[]): Alias[] { export type SubnetProxyTarget = { sourcePrefix: string; // must be a cidr destPrefix: string; // must be a cidr + disableIcmp?: boolean; rewriteTo?: string; // must be a cidr portRange?: { min: number; @@ -504,6 +505,7 @@ export function generateSubnetProxyTargets( ...parsePortRangeString(siteResource.tcpPortRangeString, "tcp"), ...parsePortRangeString(siteResource.udpPortRangeString, "udp") ]; + const disableIcmp = siteResource.disableIcmp ?? false; if (siteResource.mode == "host") { let destination = siteResource.destination; @@ -515,7 +517,8 @@ export function generateSubnetProxyTargets( targets.push({ sourcePrefix: clientPrefix, destPrefix: destination, - portRange + portRange, + disableIcmp }); } @@ -525,14 +528,16 @@ export function generateSubnetProxyTargets( sourcePrefix: clientPrefix, destPrefix: `${siteResource.aliasAddress}/32`, rewriteTo: destination, - portRange + portRange, + disableIcmp }); } } else if (siteResource.mode == "cidr") { targets.push({ sourcePrefix: clientPrefix, destPrefix: siteResource.destination, - portRange + portRange, + disableIcmp }); } } diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index 370037cb..f2e343cd 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -47,7 +47,8 @@ const createSiteResourceSchema = z roleIds: z.array(z.int()), clientIds: z.array(z.int()), tcpPortRangeString: portRangeStringSchema, - udpPortRangeString: portRangeStringSchema + udpPortRangeString: portRangeStringSchema, + disableIcmp: z.boolean().optional() }) .strict() .refine( @@ -158,7 +159,8 @@ export async function createSiteResource( roleIds, clientIds, tcpPortRangeString, - udpPortRangeString + udpPortRangeString, + disableIcmp } = parsedBody.data; // Verify the site exists and belongs to the org @@ -245,7 +247,8 @@ export async function createSiteResource( alias, aliasAddress, tcpPortRangeString, - udpPortRangeString + udpPortRangeString, + disableIcmp }) .returning(); diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index 4fc96533..7b2e0233 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -99,6 +99,7 @@ export async function listAllSiteResourcesByOrg( alias: siteResources.alias, tcpPortRangeString: siteResources.tcpPortRangeString, udpPortRangeString: siteResources.udpPortRangeString, + disableIcmp: siteResources.disableIcmp, siteName: sites.name, siteNiceId: sites.niceId, siteAddress: sites.address diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index fb2065c5..376d9c0a 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -58,7 +58,8 @@ const updateSiteResourceSchema = z roleIds: z.array(z.int()), clientIds: z.array(z.int()), tcpPortRangeString: portRangeStringSchema, - udpPortRangeString: portRangeStringSchema + udpPortRangeString: portRangeStringSchema, + disableIcmp: z.boolean().optional() }) .strict() .refine( @@ -165,7 +166,8 @@ export async function updateSiteResource( roleIds, clientIds, tcpPortRangeString, - udpPortRangeString + udpPortRangeString, + disableIcmp } = parsedBody.data; const [site] = await db @@ -233,7 +235,8 @@ export async function updateSiteResource( enabled: enabled, alias: alias && alias.trim() ? alias : null, tcpPortRangeString: tcpPortRangeString, - udpPortRangeString: udpPortRangeString + udpPortRangeString: udpPortRangeString, + disableIcmp: disableIcmp }) .where( and( @@ -357,8 +360,12 @@ export async function handleMessagingForUpdatedSiteResource( existingSiteResource.alias !== updatedSiteResource.alias; const portRangesChanged = existingSiteResource && - (existingSiteResource.tcpPortRangeString !== updatedSiteResource.tcpPortRangeString || - existingSiteResource.udpPortRangeString !== updatedSiteResource.udpPortRangeString); + (existingSiteResource.tcpPortRangeString !== + updatedSiteResource.tcpPortRangeString || + existingSiteResource.udpPortRangeString !== + updatedSiteResource.udpPortRangeString || + existingSiteResource.disableIcmp !== + updatedSiteResource.disableIcmp); // if the existingSiteResource is undefined (new resource) we don't need to do anything here, the rebuild above handled it all diff --git a/src/app/[orgId]/settings/resources/client/page.tsx b/src/app/[orgId]/settings/resources/client/page.tsx index 1628e689..eacab1d2 100644 --- a/src/app/[orgId]/settings/resources/client/page.tsx +++ b/src/app/[orgId]/settings/resources/client/page.tsx @@ -69,7 +69,8 @@ export default async function ClientResourcesPage( siteNiceId: siteResource.siteNiceId, niceId: siteResource.niceId, tcpPortRangeString: siteResource.tcpPortRangeString || null, - udpPortRangeString: siteResource.udpPortRangeString || null + udpPortRangeString: siteResource.udpPortRangeString || null, + disableIcmp: siteResource.disableIcmp || false, }; } ); diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index 8ca1cc21..758b6e12 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -43,6 +43,7 @@ export type InternalResourceRow = { niceId: string; tcpPortRangeString: string | null; udpPortRangeString: string | null; + disableIcmp: boolean; }; type ClientResourcesTableProps = { diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx index adbb7f61..894315b8 100644 --- a/src/components/CreateInternalResourceDialog.tsx +++ b/src/components/CreateInternalResourceDialog.tsx @@ -42,6 +42,7 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; +import { Switch } from "@app/components/ui/switch"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; @@ -179,6 +180,7 @@ export default function CreateInternalResourceDialog({ alias: z.string().nullish(), tcpPortRangeString: portRangeStringSchema, udpPortRangeString: portRangeStringSchema, + disableIcmp: z.boolean().optional(), roles: z .array( z.object({ @@ -308,6 +310,7 @@ export default function CreateInternalResourceDialog({ alias: "", tcpPortRangeString: "*", udpPortRangeString: "*", + disableIcmp: false, roles: [], users: [], clients: [] @@ -355,6 +358,7 @@ export default function CreateInternalResourceDialog({ alias: "", tcpPortRangeString: "*", udpPortRangeString: "*", + disableIcmp: false, roles: [], users: [], clients: [] @@ -408,6 +412,7 @@ export default function CreateInternalResourceDialog({ : undefined, tcpPortRangeString: data.tcpPortRangeString, udpPortRangeString: data.udpPortRangeString, + disableIcmp: data.disableIcmp ?? false, roleIds: data.roles ? data.roles.map((r) => parseInt(r.id)) : [], @@ -836,7 +841,7 @@ export default function CreateInternalResourceDialog({

{t("portRestrictions")}

-
+
{/* TCP Ports */} )} /> + + {/* ICMP Toggle */} + ( + +
+ + ICMP + + + field.onChange(!checked)} + /> + + + {field.value ? t("blocked") : t("allowed")} + +
+ +
+ )} + />
diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index b5c378f1..cfd4fbc1 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -10,6 +10,7 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; +import { Switch } from "@app/components/ui/switch"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; @@ -132,6 +133,7 @@ type InternalResourceData = { alias?: string | null; tcpPortRangeString?: string | null; udpPortRangeString?: string | null; + disableIcmp?: boolean; }; type EditInternalResourceDialogProps = { @@ -167,6 +169,7 @@ export default function EditInternalResourceDialog({ alias: z.string().nullish(), tcpPortRangeString: portRangeStringSchema, udpPortRangeString: portRangeStringSchema, + disableIcmp: z.boolean().optional(), roles: z .array( z.object({ @@ -358,6 +361,7 @@ export default function EditInternalResourceDialog({ alias: resource.alias ?? null, tcpPortRangeString: resource.tcpPortRangeString ?? "*", udpPortRangeString: resource.udpPortRangeString ?? "*", + disableIcmp: resource.disableIcmp ?? false, roles: [], users: [], clients: [] @@ -433,6 +437,7 @@ export default function EditInternalResourceDialog({ : null, tcpPortRangeString: data.tcpPortRangeString, udpPortRangeString: data.udpPortRangeString, + disableIcmp: data.disableIcmp ?? false, roleIds: (data.roles || []).map((r) => parseInt(r.id)), userIds: (data.users || []).map((u) => u.id), clientIds: (data.clients || []).map((c) => parseInt(c.id)) @@ -504,6 +509,7 @@ export default function EditInternalResourceDialog({ alias: resource.alias ?? null, tcpPortRangeString: resource.tcpPortRangeString ?? "*", udpPortRangeString: resource.udpPortRangeString ?? "*", + disableIcmp: resource.disableIcmp ?? false, roles: [], users: [], clients: [] @@ -561,6 +567,7 @@ export default function EditInternalResourceDialog({ alias: resource.alias ?? null, tcpPortRangeString: resource.tcpPortRangeString ?? "*", udpPortRangeString: resource.udpPortRangeString ?? "*", + disableIcmp: resource.disableIcmp ?? false, roles: [], users: [], clients: [] @@ -815,7 +822,7 @@ export default function EditInternalResourceDialog({

{t("portRestrictions")}

-
+
{/* TCP Ports */} )} /> + + {/* ICMP Toggle */} + ( + +
+ + ICMP + + + field.onChange(!checked)} + /> + + + {field.value ? t("blocked") : t("allowed")} + +
+ +
+ )} + />
From 9ef7faace75bae1d025df36c11a3a8c4d7d777b6 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 16 Dec 2025 23:45:53 +0100 Subject: [PATCH 094/153] =?UTF-8?q?=F0=9F=9A=A7=20wip?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/[orgId]/settings/general/page.tsx | 518 ++++++++++++++++++++++ src/hooks/usePaidStatus.ts | 21 + 2 files changed, 539 insertions(+) create mode 100644 src/hooks/usePaidStatus.ts diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index ff8a103d..6bfb3013 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -53,6 +53,7 @@ import { SwitchInput } from "@app/components/SwitchInput"; import { SecurityFeaturesAlert } from "@app/components/SecurityFeaturesAlert"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { usePaidStatus } from "@app/hooks/usePaidStatus"; // Session length options in hours const SESSION_LENGTH_OPTIONS = [ @@ -875,3 +876,520 @@ export default function GeneralPage() { ); } + +function GeneralSectionForm() { + const { org } = useOrgContext(); + const form = useForm({ + resolver: zodResolver(GeneralFormSchema), + defaultValues: { + name: org?.org.name + }, + mode: "onChange" + }); + const t = useTranslations(); + const subscription = useSubscriptionStatusContext(); + const { isUnlocked } = useLicenseStatusContext(); + const { isPaidUser } = usePaidStatus(); + return ( + <> + + + {t("general")} + + {t("orgGeneralSettingsDescription")} + + + + + ( + + {t("name")} + + + + + + {t("orgDisplayName")} + + + )} + /> + ( + + {t("subnet")} + + + + + + {t("subnetDescription")} + + + )} + /> + + + +
+ +
+
+ + ); +} + +function LogRetentionSectionForm() { + const { org } = useOrgContext(); + const form = useForm({ + resolver: zodResolver(GeneralFormSchema), + defaultValues: { + name: org?.org.name + }, + mode: "onChange" + }); + const t = useTranslations(); + const subscription = useSubscriptionStatusContext(); + const { isUnlocked } = useLicenseStatusContext(); + const { isPaidUser } = usePaidStatus(); + + return ( + + + {t("logRetention")} + + {t("logRetentionDescription")} + + + + + ( + + + {t("logRetentionRequestLabel")} + + + + + + + )} + /> + + {build != "oss" && ( + <> + + + { + const isDisabled = + (build == "saas" && + !subscription?.subscribed) || + (build == "enterprise" && + !isUnlocked()); + + return ( + + + {t("logRetentionAccessLabel")} + + + + + + + ); + }} + /> + { + const isDisabled = + (build == "saas" && + !subscription?.subscribed) || + (build == "enterprise" && + !isUnlocked()); + + return ( + + + {t("logRetentionActionLabel")} + + + + + + + ); + }} + /> + + )} + + + + ); +} + +function SectionForm() { + const { org } = useOrgContext(); + const form = useForm({ + resolver: zodResolver(GeneralFormSchema), + defaultValues: { + name: org?.org.name + }, + mode: "onChange" + }); + const t = useTranslations(); + const subscription = useSubscriptionStatusContext(); + const { isUnlocked } = useLicenseStatusContext(); + const { isPaidUser } = usePaidStatus(); + + return ( + + {build !== "oss" && ( + <> +
+ + + {t("securitySettings")} + + + {t("securitySettingsDescription")} + + + + + + { + const isDisabled = + isSecurityFeatureDisabled(); + + return ( + +
+ + { + if (!isDisabled) { + form.setValue( + "requireTwoFactor", + val + ); + } + }} + /> + +
+ + + {t( + "requireTwoFactorDescription" + )} + +
+ ); + }} + /> + { + const isDisabled = + isSecurityFeatureDisabled(); + + return ( + + + {t("maxSessionLength")} + + + + + + + {t( + "maxSessionLengthDescription" + )} + + + ); + }} + /> + { + const isDisabled = + isSecurityFeatureDisabled(); + + return ( + + + {t("passwordExpiryDays")} + + + + + + + {t( + "editPasswordExpiryDescription" + )} + + + ); + }} + /> +
+
+ + )} +
+ ); +} diff --git a/src/hooks/usePaidStatus.ts b/src/hooks/usePaidStatus.ts new file mode 100644 index 00000000..d8173e6e --- /dev/null +++ b/src/hooks/usePaidStatus.ts @@ -0,0 +1,21 @@ +import { build } from "@server/build"; +import { useLicenseStatusContext } from "./useLicenseStatusContext"; +import { useSubscriptionStatusContext } from "./useSubscriptionStatusContext"; + +export function usePaidStatus() { + const { isUnlocked } = useLicenseStatusContext(); + const subscription = useSubscriptionStatusContext(); + + // Check if features are disabled due to licensing/subscription + const hasEnterpriseLicense = build === "enterprise" && isUnlocked(); + const hasSaasSubscription = + build === "saas" && + subscription?.isSubscribed() && + subscription.isActive(); + + return { + hasEnterpriseLicense, + hasSaasSubscription, + isPaidUser: hasEnterpriseLicense || hasSaasSubscription + }; +} From a21029582e65be51f2028abc4d78e65d968707b8 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 16 Dec 2025 18:24:06 -0500 Subject: [PATCH 095/153] Always send the relay port config --- server/routers/newt/handleNewtRegisterMessage.ts | 1 + server/routers/olm/getOlmToken.ts | 1 + server/routers/olm/handleOlmRelayMessage.ts | 4 +++- server/routers/olm/peers.ts | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index 77e49a20..c7f2131e 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -346,6 +346,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { type: "newt/wg/connect", data: { endpoint: `${exitNode.endpoint}:${exitNode.listenPort}`, + relayPort: config.getRawConfig().gerbil.clients_start_port, publicKey: exitNode.publicKey, serverIP: exitNode.address.split("/")[0], tunnelIP: siteSubnet.split("/")[0], diff --git a/server/routers/olm/getOlmToken.ts b/server/routers/olm/getOlmToken.ts index 3852b00e..b6dc8148 100644 --- a/server/routers/olm/getOlmToken.ts +++ b/server/routers/olm/getOlmToken.ts @@ -197,6 +197,7 @@ export async function getOlmToken( const exitNodesHpData = allExitNodes.map((exitNode: ExitNode) => { return { publicKey: exitNode.publicKey, + relayPort: config.getRawConfig().gerbil.clients_start_port, endpoint: exitNode.endpoint }; }); diff --git a/server/routers/olm/handleOlmRelayMessage.ts b/server/routers/olm/handleOlmRelayMessage.ts index 595b35ba..88886cd1 100644 --- a/server/routers/olm/handleOlmRelayMessage.ts +++ b/server/routers/olm/handleOlmRelayMessage.ts @@ -4,6 +4,7 @@ import { clients, clientSitesAssociationsCache, Olm } from "@server/db"; import { and, eq } from "drizzle-orm"; import { updatePeer as newtUpdatePeer } from "../newt/peers"; import logger from "@server/logger"; +import config from "@server/lib/config"; export const handleOlmRelayMessage: MessageHandler = async (context) => { const { message, client: c, sendToClient } = context; @@ -88,7 +89,8 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => { type: "olm/wg/peer/relay", data: { siteId: siteId, - relayEndpoint: exitNode.endpoint + relayEndpoint: exitNode.endpoint, + relayPort: config.getRawConfig().gerbil.clients_start_port } }, broadcast: false, diff --git a/server/routers/olm/peers.ts b/server/routers/olm/peers.ts index 4aa8edd7..e164b257 100644 --- a/server/routers/olm/peers.ts +++ b/server/routers/olm/peers.ts @@ -1,5 +1,6 @@ import { sendToClient } from "#dynamic/routers/ws"; import { db, olms } from "@server/db"; +import config from "@server/lib/config"; import logger from "@server/logger"; import { eq } from "drizzle-orm"; import { Alias } from "yaml"; @@ -156,6 +157,7 @@ export async function initPeerAddHandshake( siteId: peer.siteId, exitNode: { publicKey: peer.exitNode.publicKey, + relayPort: config.getRawConfig().gerbil.clients_start_port, endpoint: peer.exitNode.endpoint } } From e02fa7c14889cd3ba5bcc6b19214384a9fcf65cb Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 17 Dec 2025 00:52:12 +0100 Subject: [PATCH 096/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20pass=20the=20defau?= =?UTF-8?q?lt=20domainId=20instead=20of=20the=20base=20domain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/proxy/[niceId]/general/page.tsx | 22 ++----------------- src/components/DomainPicker.tsx | 17 ++++++-------- 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx index 7d4db07c..ed1080f3 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx @@ -91,24 +91,6 @@ export default function GeneralForm() { `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}` ); - const [defaultSubdomain, defaultBaseDomain] = useMemo(() => { - const resourceUrl = new URL(resourceFullDomain); - const domain = resourceUrl.hostname; - - const allDomainParts = domain.split("."); - let sub = undefined; - let base = domain; - - if (allDomainParts.length >= 3) { - // 3 parts: [subdomain, domain, tld] - const [first, ...rest] = allDomainParts; - sub = first; - base = rest.join("."); - } - - return [sub, base]; - }, [resourceFullDomain]); - const [selectedDomain, setSelectedDomain] = useState<{ domainId: string; subdomain?: string; @@ -507,8 +489,8 @@ export default function GeneralForm() { { const selected = { domainId: res.domainId, diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 625b566e..5e883add 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -74,8 +74,9 @@ interface DomainPickerProps { }) => void; cols?: number; hideFreeDomain?: boolean; - defaultSubdomain?: string; - defaultBaseDomain?: string; + defaultFullDomain?: string | null; + defaultSubdomain?: string | null; + defaultDomainId?: string | null; } export default function DomainPicker({ @@ -84,7 +85,8 @@ export default function DomainPicker({ cols = 2, hideFreeDomain = false, defaultSubdomain, - defaultBaseDomain + defaultFullDomain, + defaultDomainId }: DomainPickerProps) { const { env } = useEnvContext(); const api = createApiClient({ env }); @@ -139,7 +141,7 @@ export default function DomainPicker({ // Select the first organization domain or the one provided from props const firstOrgDomain = organizationDomains.find( - (domain) => domain.baseDomain === defaultBaseDomain + (domain) => domain.domainId === defaultDomainId ) ?? organizationDomains[0]; const domainOption: DomainOption = { id: `org-${firstOrgDomain.domainId}`, @@ -175,12 +177,7 @@ export default function DomainPicker({ setSelectedBaseDomain(freeDomainOption); } } - }, [ - hideFreeDomain, - loadingDomains, - organizationDomains, - defaultBaseDomain - ]); + }, [hideFreeDomain, loadingDomains, organizationDomains, defaultDomainId]); const checkAvailability = useCallback( async (input: string) => { From c98d61a8fb937c24ec5bf1a6abe3d49d83dd5a23 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 17 Dec 2025 02:36:29 +0100 Subject: [PATCH 097/153] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20pass=20default=20v?= =?UTF-8?q?alue=20to=20domain=20picker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/proxy/[niceId]/general/page.tsx | 13 ++++++++++--- src/components/DomainPicker.tsx | 17 ++++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx index 9b2c120e..8d3ccab5 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx @@ -226,7 +226,8 @@ export default function GeneralForm() { niceId: data.niceId, subdomain: data.subdomain, fullDomain: updated.fullDomain, - proxyPort: data.proxyPort + proxyPort: data.proxyPort, + domainId: data.domainId // ...(!resource.http && { // enableProxy: data.enableProxy // }) @@ -489,8 +490,14 @@ export default function GeneralForm() { { const selected = { domainId: res.domainId, diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 5e883add..36e76b2b 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -143,6 +143,7 @@ export default function DomainPicker({ organizationDomains.find( (domain) => domain.domainId === defaultDomainId ) ?? organizationDomains[0]; + const domainOption: DomainOption = { id: `org-${firstOrgDomain.domainId}`, domain: firstOrgDomain.baseDomain, @@ -156,7 +157,10 @@ export default function DomainPicker({ onDomainChange?.({ domainId: firstOrgDomain.domainId, type: "organization", - subdomain: undefined, + subdomain: + firstOrgDomain.type !== "cname" + ? defaultSubdomain || undefined + : undefined, fullDomain: firstOrgDomain.baseDomain, baseDomain: firstOrgDomain.baseDomain }); @@ -177,7 +181,13 @@ export default function DomainPicker({ setSelectedBaseDomain(freeDomainOption); } } - }, [hideFreeDomain, loadingDomains, organizationDomains, defaultDomainId]); + }, [ + loadingDomains, + organizationDomains, + defaultSubdomain, + hideFreeDomain, + defaultDomainId + ]); const checkAvailability = useCallback( async (input: string) => { @@ -354,7 +364,8 @@ export default function DomainPicker({ domainNamespaceId: option.domainNamespaceId, type: option.type === "provided-search" ? "provided" : "organization", - subdomain: sub || undefined, + subdomain: + option.domainType !== "cname" ? sub || undefined : undefined, fullDomain, baseDomain: option.domain }); From 9de39dbe4204dce377ab0d4a493b6ffff1337cfc Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 16 Dec 2025 21:33:13 -0500 Subject: [PATCH 098/153] Support wildcard resources --- server/routers/siteResource/createSiteResource.ts | 4 ++-- server/routers/siteResource/updateSiteResource.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index f2e343cd..d2bb1665 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -39,8 +39,8 @@ const createSiteResourceSchema = z alias: z .string() .regex( - /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, - "Alias must be a fully qualified domain name (e.g., example.com)" + /^(?:[a-zA-Z0-9*?](?:[a-zA-Z0-9*?-]{0,61}[a-zA-Z0-9*?])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, + "Alias must be a fully qualified domain name with optional wildcards (e.g., example.com, *.example.com, host-0?.example.internal)" ) .optional(), userIds: z.array(z.string()), diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 376d9c0a..17a4033e 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -50,8 +50,8 @@ const updateSiteResourceSchema = z alias: z .string() .regex( - /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, - "Alias must be a fully qualified domain name (e.g., example.internal)" + /^(?:[a-zA-Z0-9*?](?:[a-zA-Z0-9*?-]{0,61}[a-zA-Z0-9*?])?\.)+[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/, + "Alias must be a fully qualified domain name with optional wildcards (e.g., example.internal, *.example.internal, host-0?.example.internal)" ) .nullish(), userIds: z.array(z.string()), From 43fb06084faa021b526339e32e851b84db1e2cd5 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 16 Dec 2025 21:52:01 -0500 Subject: [PATCH 099/153] Alias should not get double regex --- server/routers/siteResource/createSiteResource.ts | 2 +- server/routers/siteResource/updateSiteResource.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index d2bb1665..c103b09e 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -68,7 +68,7 @@ const createSiteResourceSchema = z const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; const isValidDomain = domainRegex.test(data.destination); - const isValidAlias = data.alias && domainRegex.test(data.alias); + const isValidAlias = data.alias !== undefined && data.alias !== null && data.alias.trim() !== ""; return isValidDomain && isValidAlias; // require the alias to be set in the case of domain } diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 17a4033e..c3360e6f 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -78,7 +78,7 @@ const updateSiteResourceSchema = z const domainRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/; const isValidDomain = domainRegex.test(data.destination); - const isValidAlias = data.alias && domainRegex.test(data.alias); + const isValidAlias = data.alias !== undefined && data.alias !== null && data.alias.trim() !== ""; return isValidDomain && isValidAlias; // require the alias to be set in the case of domain } From b133593ea2e87b07e7ed7fad333391705c1cad58 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Wed, 17 Dec 2025 04:57:16 +0100 Subject: [PATCH 100/153] =?UTF-8?q?=F0=9F=9A=B8=20now=20the=20domain=20pic?= =?UTF-8?q?ker=20is=20deterministic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resources/proxy/[niceId]/general/page.tsx | 14 +- src/components/DomainPicker.tsx | 171 ++++++++++-------- 2 files changed, 109 insertions(+), 76 deletions(-) diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx index 8d3ccab5..e846ec6c 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/general/page.tsx @@ -91,8 +91,14 @@ export default function GeneralForm() { `${resource.ssl ? "https" : "http"}://${toUnicode(resource.fullDomain || "")}` ); + const resourceFullDomainName = useMemo(() => { + const url = new URL(resourceFullDomain); + return url.hostname; + }, [resourceFullDomain]); + const [selectedDomain, setSelectedDomain] = useState<{ domainId: string; + domainNamespaceId?: string; subdomain?: string; fullDomain: string; baseDomain: string; @@ -491,19 +497,21 @@ export default function GeneralForm() { orgId={orgId as string} cols={1} defaultSubdomain={ - selectedDomain?.subdomain ?? + form.getValues("subdomain") ?? resource.subdomain } defaultDomainId={ - selectedDomain?.domainId ?? + form.getValues("domainId") ?? resource.domainId } + defaultFullDomain={resourceFullDomainName} onDomainChange={(res) => { const selected = { domainId: res.domainId, subdomain: res.subdomain, fullDomain: res.fullDomain, - baseDomain: res.baseDomain + baseDomain: res.baseDomain, + domainNamespaceId: res.domainNamespaceId }; setSelectedDomain(selected); }} diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 36e76b2b..ba29c029 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -103,6 +103,7 @@ export default function DomainPicker({ const [subdomainInput, setSubdomainInput] = useState( defaultSubdomain ?? "" ); + const [selectedBaseDomain, setSelectedBaseDomain] = useState(null); const [availableOptions, setAvailableOptions] = useState( @@ -129,7 +130,7 @@ export default function DomainPicker({ const [open, setOpen] = useState(false); // Provided domain search states - const [userInput, setUserInput] = useState(""); + const [userInput, setUserInput] = useState(defaultSubdomain ?? ""); const [isChecking, setIsChecking] = useState(false); const [providedDomainsShown, setProvidedDomainsShown] = useState(3); const [selectedProvidedDomain, setSelectedProvidedDomain] = @@ -137,49 +138,60 @@ export default function DomainPicker({ useEffect(() => { if (!loadingDomains) { + let domainOptionToSelect: DomainOption | null = null; if (organizationDomains.length > 0) { // Select the first organization domain or the one provided from props - const firstOrgDomain = - organizationDomains.find( - (domain) => domain.domainId === defaultDomainId - ) ?? organizationDomains[0]; + let firstOrExistingDomain = organizationDomains.find( + (domain) => domain.domainId === defaultDomainId + ); + // if no default Domain + if (!defaultDomainId) { + firstOrExistingDomain = organizationDomains[0]; + } - const domainOption: DomainOption = { - id: `org-${firstOrgDomain.domainId}`, - domain: firstOrgDomain.baseDomain, - type: "organization", - verified: firstOrgDomain.verified, - domainType: firstOrgDomain.type, - domainId: firstOrgDomain.domainId - }; - setSelectedBaseDomain(domainOption); + if (firstOrExistingDomain) { + domainOptionToSelect = { + id: `org-${firstOrExistingDomain.domainId}`, + domain: firstOrExistingDomain.baseDomain, + type: "organization", + verified: firstOrExistingDomain.verified, + domainType: firstOrExistingDomain.type, + domainId: firstOrExistingDomain.domainId + }; - onDomainChange?.({ - domainId: firstOrgDomain.domainId, - type: "organization", - subdomain: - firstOrgDomain.type !== "cname" - ? defaultSubdomain || undefined - : undefined, - fullDomain: firstOrgDomain.baseDomain, - baseDomain: firstOrgDomain.baseDomain - }); - } else if ( - (build === "saas" || build === "enterprise") && - !hideFreeDomain + onDomainChange?.({ + domainId: firstOrExistingDomain.domainId, + type: "organization", + subdomain: + firstOrExistingDomain.type !== "cname" + ? defaultSubdomain || undefined + : undefined, + fullDomain: firstOrExistingDomain.baseDomain, + baseDomain: firstOrExistingDomain.baseDomain + }); + } + } + + if ( + !domainOptionToSelect && + build !== "oss" && + !hideFreeDomain && + defaultDomainId !== undefined ) { // If no organization domains, select the provided domain option const domainOptionText = build === "enterprise" ? t("domainPickerProvidedDomain") : t("domainPickerFreeProvidedDomain"); - const freeDomainOption: DomainOption = { + // free domain option + domainOptionToSelect = { id: "provided-search", domain: domainOptionText, type: "provided-search" }; - setSelectedBaseDomain(freeDomainOption); } + + setSelectedBaseDomain(domainOptionToSelect); } }, [ loadingDomains, @@ -349,6 +361,9 @@ export default function DomainPicker({ setSelectedProvidedDomain(null); } + console.log({ + setSelectedBaseDomain: option + }); setSelectedBaseDomain(option); setOpen(false); @@ -414,6 +429,15 @@ export default function DomainPicker({ 0, providedDomainsShown ); + console.log({ + displayedProvidedOptions + }); + + const selectedDomainNamespaceId = + selectedProvidedDomain?.domainNamespaceId ?? + displayedProvidedOptions.find( + (opt) => opt.fullDomain === defaultFullDomain + )?.domainNamespaceId; const hasMoreProvided = sortedAvailableOptions.length > providedDomainsShown; @@ -699,10 +723,8 @@ export default function DomainPicker({ {!isChecking && sortedAvailableOptions.length > 0 && (
{ const option = displayedProvidedOptions.find( @@ -715,47 +737,50 @@ export default function DomainPicker({ }} className={`grid gap-2 grid-cols-1 sm:grid-cols-${cols}`} > - {displayedProvidedOptions.map((option) => ( -
- - - - {t("siteCredentialsSave")} - - - {t( - "siteCredentialsSaveDescription" - )} - - )} diff --git a/src/app/globals.css b/src/app/globals.css index 10a9764e..bd5860a6 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -40,7 +40,7 @@ } .dark { - --background: oklch(0.17 0.006 285.885); + --background: oklch(0.19 0.006 285.885); --foreground: oklch(0.985 0 0); --card: oklch(0.21 0.006 285.885); --card-foreground: oklch(0.985 0 0); @@ -56,7 +56,7 @@ --accent-foreground: oklch(0.985 0 0); --destructive: oklch(0.5382 0.1949 22.216); --border: oklch(1 0 0 / 13%); - --input: oklch(1 0 0 / 15%); + --input: oklch(1 0 0 / 18%); --ring: oklch(0.646 0.222 41.116); --chart-1: oklch(0.488 0.243 264.376); --chart-2: oklch(0.696 0.17 162.48); diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 80e13ff2..54576c0c 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -82,6 +82,11 @@ export const orgNavSections = (): SidebarNavSection[] => [ } ] }, + { + title: "sidebarDomains", + href: "/{orgId}/settings/domains", + icon: + }, ...(build == "saas" ? [ { @@ -91,12 +96,7 @@ export const orgNavSections = (): SidebarNavSection[] => [ showEE: true } ] - : []), - { - title: "sidebarDomains", - href: "/{orgId}/settings/domains", - icon: - } + : []) ] }, { diff --git a/src/components/DNSRecordsDataTable.tsx b/src/components/DNSRecordsDataTable.tsx index 786b2d71..0a9eaa7d 100644 --- a/src/components/DNSRecordsDataTable.tsx +++ b/src/components/DNSRecordsDataTable.tsx @@ -114,6 +114,7 @@ export function DNSRecordsDataTable({ href="https://docs.pangolin.net/manage/domains" target="_blank" rel="noopener noreferrer" + className="hidden sm:block" > + ); + } + }, { accessorKey: "resourceName", enableHiding: false, @@ -135,23 +152,6 @@ export default function ShareLinksTable({ ); } }, - { - accessorKey: "title", - friendlyName: t("title"), - header: ({ column }) => { - return ( - - ); - } - }, // { // accessorKey: "domain", // header: "Link", From 56b0185c8f37dea0fab2961cf58a072933efa0e8 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 18 Dec 2025 10:58:16 -0500 Subject: [PATCH 131/153] visual adjustments --- messages/en-US.json | 13 +++++----- src/app/[orgId]/settings/general/page.tsx | 20 +++++++--------- .../proxy/[niceId]/authentication/page.tsx | 14 +++++------ .../resources/proxy/[niceId]/general/page.tsx | 2 +- .../resources/proxy/[niceId]/proxy/page.tsx | 4 ++-- .../resources/proxy/[niceId]/rules/page.tsx | 22 ++++++++--------- .../settings/sites/[niceId]/general/page.tsx | 24 +++++++++---------- src/components/ui/checkbox.tsx | 8 +++---- 8 files changed, 53 insertions(+), 54 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 1d705dae..0cfd8f6f 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -688,7 +688,7 @@ "resourceRoleDescription": "Admins can always access this resource.", "resourceUsersRoles": "Access Controls", "resourceUsersRolesDescription": "Configure which users and roles can visit this resource", - "resourceUsersRolesSubmit": "Save Users & Roles", + "resourceUsersRolesSubmit": "Save Access Controls", "resourceWhitelistSave": "Saved successfully", "resourceWhitelistSaveDescription": "Whitelist settings have been saved", "ssoUse": "Use Platform SSO", @@ -1311,7 +1311,7 @@ "documentation": "Documentation", "saveAllSettings": "Save All Settings", "saveResourceTargets": "Save Targets", - "saveResourceHttp": "Save Additional fields", + "saveResourceHttp": "Save Proxy Settings", "saveProxyProtocol": "Save Proxy protocol settings", "settingsUpdated": "Settings updated", "settingsUpdatedDescription": "Settings updated successfully", @@ -1662,7 +1662,7 @@ "siteAddressDescription": "The internal address of the site. Must fall within the organization's subnet.", "siteNameDescription": "The display name of the site that can be changed later.", "autoLoginExternalIdp": "Auto Login with External IDP", - "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external identity provider for authentication.", "selectIdp": "Select IDP", "selectIdpPlaceholder": "Choose an IDP...", "selectIdpRequired": "Please select an IDP when auto login is enabled.", @@ -1877,8 +1877,8 @@ "enableTwoFactorAuthentication": "Enable two-factor authentication", "completeSecuritySteps": "Complete Security Steps", "securitySettings": "Security Settings", - "dangerSection": "Danger section", - "dangerSectionDescription": "Delete organization alongside all its sites, clients, resources, etc...", + "dangerSection": "Danger Zone", + "dangerSectionDescription": "Permanently delete all data associated with this organization", "securitySettingsDescription": "Configure security policies for the organization", "requireTwoFactorForAllUsers": "Require Two-Factor Authentication for All Users", "requireTwoFactorDescription": "When enabled, all internal users in this organization must have two-factor authentication enabled to access the organization.", @@ -2317,5 +2317,6 @@ "resourceLoginPageTitle": "Resource Login Page", "resourceLoginPageDescription": "Customize the login page for individual resources", "enterConfirmation": "Enter confirmation", - "blueprintViewDetails": "Details" + "blueprintViewDetails": "Details", + "defaultIdentityProvider": "Default Identity Provider" } diff --git a/src/app/[orgId]/settings/general/page.tsx b/src/app/[orgId]/settings/general/page.tsx index fa28dd05..7816924f 100644 --- a/src/app/[orgId]/settings/general/page.tsx +++ b/src/app/[orgId]/settings/general/page.tsx @@ -49,7 +49,8 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionForm + SettingsSectionForm, + SettingsSectionFooter } from "@app/components/Settings"; import { useUserContext } from "@app/hooks/useUserContext"; import { useTranslations } from "next-intl"; @@ -122,16 +123,12 @@ export default function GeneralPage() { const { org } = useOrgContext(); return ( -
- + - + - {build !== "oss" && ( - - )} - {build !== "saas" && } -
+ {build !== "oss" && } + {build !== "saas" && }
); } @@ -222,7 +219,7 @@ function DeleteForm({ org }: SectionFormProps) { {t("dangerSectionDescription")} -
+ -
+ ); @@ -758,6 +755,7 @@ function SecuritySettingsSectionForm({ org }: SectionFormProps) { action={formAction} ref={formRef} id="security-settings-section-form" + className="space-y-4" > 0 && ( -
+ <>
+ + + )} -

-
- ( - - - {t( - "createInternalResourceDialogName" - )} - - - - - - - )} - /> + /> - ( - - - {t( - "site" - )} - - - - - - - - - - - - - {t( - "noSitesFound" - )} - - - {availableSites.map( - ( - site - ) => ( - { - field.onChange( - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - - )} - /> - - ( - - - {t( - "createInternalResourceDialogMode" - )} - - - - - )} - /> - {/* - {mode === "port" && ( - <> -
+ /> + + + {t( + "noSitesFound" + )} + + + {availableSites.map( + (site) => ( + { + field.onChange( + site.siteId + ); + }} + > + + { + site.name + } + + ) + )} + + + + + + + + )} + /> +
+ + {/* Tabs for Network Settings and Access Control */} + + {/* Network Settings Tab */} +
+
+
+ +
+ {t( + "editInternalResourceDialogDestinationDescription" + )} +
+
+ +
+ {/* Mode - Smaller select */} +
( - {t("createInternalResourceDialogProtocol")} + {t( + "createInternalResourceDialogMode" + )} @@ -708,22 +708,29 @@ export default function CreateInternalResourceDialog({ )} /> +
+ {/* Destination - Larger input */} +
( - {t("createInternalResourceDialogSitePort")} + + {t( + "createInternalResourceDialogDestination" + )} + - field.onChange( - e.target.value === "" ? undefined : parseInt(e.target.value) - ) - } + {...field} /> @@ -731,418 +738,396 @@ export default function CreateInternalResourceDialog({ )} />
- - )} */} -
-
- {/* Target Configuration Form */} -
-

- {t( - "createInternalResourceDialogTargetConfiguration" - )} -

-
- ( - - - {t( - "createInternalResourceDialogDestination" - )} - - - - - - {mode === "host" && - t( - "createInternalResourceDialogDestinationHostDescription" + {/* Alias - Equally sized input (if allowed) */} + {mode !== "cidr" && ( +
+ ( + + + {t( + "createInternalResourceDialogAlias" + )} + + + + + + )} - {mode === "cidr" && - t( - "createInternalResourceDialogDestinationCidrDescription" - )} - {/* {mode === "port" && t("createInternalResourceDialogDestinationIPDescription")} */} - - - - )} - /> - - {/* {mode === "port" && ( - ( - - - {t("targetPort")} - - - - field.onChange( - e.target.value === "" ? undefined : parseInt(e.target.value) - ) - } - /> - - - {t("createInternalResourceDialogDestinationPortDescription")} - - - + /> +
)} - /> - )} */} -
-
+
+
- {/* Alias */} - {mode !== "cidr" && ( -
- ( - - + {/* Ports and Restrictions */} +
+ {/* TCP Ports */} +
+ +
+ {t( + "editInternalResourceDialogPortRestrictionsDescription" + )} +
+
+
+
+ {t( - "createInternalResourceDialogAlias" + "editInternalResourceDialogTcp" )} - - - - +
+
+ ( + +
+ {/**/} + + {tcpPortMode === + "custom" ? ( + + + setTcpCustomPorts( + e + .target + .value + ) + } + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+ + {/* UDP Ports */} +
+
+ {t( - "createInternalResourceDialogAliasDescription" + "editInternalResourceDialogUdp" )} - - - - )} - /> -
- )} - - {/* Port Restrictions Section */} -
-

- {t("portRestrictions")} -

-
- {/* TCP Ports */} - ( - -
- - TCP - - {/**/} - - {tcpPortMode === "custom" ? ( - - - setTcpCustomPorts(e.target.value) - } - className="flex-1" - /> - - ) : ( - - )} -
- -
- )} - /> - - {/* UDP Ports */} - ( - -
- - UDP - - {/**/} - - {udpPortMode === "custom" ? ( - - - setUdpCustomPorts(e.target.value) - } - className="flex-1" - /> - - ) : ( - - )} -
- -
- )} - /> - - {/* ICMP Toggle */} - ( - -
- - ICMP - - - field.onChange(!checked)} - /> - - - {field.value ? t("blocked") : t("allowed")} - -
- -
- )} - /> -
-
- - {/* Access Control Section */} -
-

- {t("resourceUsersRoles")} -

-
- ( - - - {t("roles")} - - { - form.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allRoles - } - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - +
+
+ ( + +
+ {/**/} + + {udpPortMode === + "custom" ? ( + + + setUdpCustomPorts( + e + .target + .value + ) + } + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+ + {/* ICMP Toggle */} +
+
+ {t( - "resourceRoleDescription" + "editInternalResourceDialogIcmp" )} - - - )} - /> - ( - - - {t("users")} - - { - form.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allUsers - } - allowDuplicates={false} - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - )} - /> - {hasMachineClients && ( +
+
+ ( + +
+ + + field.onChange( + !checked + ) + } + /> + + + {field.value + ? t( + "blocked" + ) + : t( + "allowed" + )} + +
+ +
+ )} + /> +
+
+
+
+ + {/* Access Control Tab */} +
+
+ +
+ {t( + "editInternalResourceDialogAccessControlDescription" + )} +
+
+
+ {/* Roles */} ( - {t("machineClients")} + {t("roles")} { form.setValue( - "clients", - newClients as [ + "roles", + newRoles as [ Tag, ...Tag[] ] @@ -1152,7 +1137,70 @@ export default function CreateInternalResourceDialog({ true } autocompleteOptions={ - allClients + allRoles + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + {t( + "resourceRoleDescription" + )} + + + )} + /> + + {/* Users */} + ( + + + {t("users")} + + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allUsers } allowDuplicates={ false @@ -1167,9 +1215,76 @@ export default function CreateInternalResourceDialog({ )} /> - )} + + {/* Clients (Machines) */} + {hasMachineClients && ( + ( + + + {t( + "machineClients" + )} + + + { + form.setValue( + "clients", + newClients as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allClients + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={ + true + } + /> + + + + )} + /> + )} +
-
+ diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx index 5d5745c7..88d98aa5 100644 --- a/src/components/EditInternalResourceDialog.tsx +++ b/src/components/EditInternalResourceDialog.tsx @@ -56,7 +56,14 @@ import { } from "@app/components/ui/popover"; import { cn } from "@app/lib/cn"; import { ListSitesResponse } from "@server/routers/site"; -import { Check, ChevronsUpDown } from "lucide-react"; +import { Check, ChevronsUpDown, ChevronDown } from "lucide-react"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger +} from "@app/components/ui/collapsible"; +import { HorizontalTabs, TabItem } from "@app/components/HorizontalTabs"; +import { Separator } from "@app/components/ui/separator"; // import { InfoPopup } from "@app/components/ui/info-popup"; // Helper to validate port range string format @@ -85,7 +92,12 @@ const isValidPortRangeString = (val: string | undefined | null): boolean => { return false; } - if (startPort < 1 || startPort > 65535 || endPort < 1 || endPort > 65535) { + if ( + startPort < 1 || + startPort > 65535 || + endPort < 1 || + endPort > 65535 + ) { return false; } @@ -107,17 +119,18 @@ const isValidPortRangeString = (val: string | undefined | null): boolean => { }; // Port range string schema for client-side validation -const portRangeStringSchema = z - .string() - .optional() - .nullable() - .refine( - (val) => isValidPortRangeString(val), - { - message: - 'Port range must be "*" for all ports, or a comma-separated list of ports and ranges (e.g., "80,443,8000-9000"). Ports must be between 1 and 65535.' - } - ); +// Note: This schema is defined outside the component, so we'll use a function to get the message +const getPortRangeValidationMessage = (t: (key: string) => string) => + t("editInternalResourceDialogPortRangeValidationError"); + +const createPortRangeStringSchema = (t: (key: string) => string) => + z + .string() + .optional() + .nullable() + .refine((val) => isValidPortRangeString(val), { + message: getPortRangeValidationMessage(t) + }); // Helper to determine the port mode from a port range string type PortMode = "all" | "blocked" | "custom"; @@ -128,7 +141,10 @@ const getPortModeFromString = (val: string | undefined | null): PortMode => { }; // Helper to get the port string for API from mode and custom value -const getPortStringFromMode = (mode: PortMode, customValue: string): string | undefined => { +const getPortStringFromMode = ( + mode: PortMode, + customValue: string +): string | undefined => { if (mode === "all") return "*"; if (mode === "blocked") return ""; return customValue; @@ -188,8 +204,8 @@ export default function EditInternalResourceDialog({ destination: z.string().min(1), // destinationPort: z.int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(), alias: z.string().nullish(), - tcpPortRangeString: portRangeStringSchema, - udpPortRangeString: portRangeStringSchema, + tcpPortRangeString: createPortRangeStringSchema(t), + udpPortRangeString: createPortRangeStringSchema(t), disableIcmp: z.boolean().optional(), roles: z .array( @@ -352,6 +368,9 @@ export default function EditInternalResourceDialog({ number | null >(null); + // Collapsible state for ports and restrictions + const [isPortsExpanded, setIsPortsExpanded] = useState(false); + // Port restriction UI state const [tcpPortMode, setTcpPortMode] = useState( getPortModeFromString(resource.tcpPortRangeString) @@ -446,30 +465,27 @@ export default function EditInternalResourceDialog({ } // Update the site resource - await api.post( - `/site-resource/${resource.id}`, - { - name: data.name, - siteId: data.siteId, - mode: data.mode, - // protocol: data.mode === "port" ? data.protocol : null, - // proxyPort: data.mode === "port" ? data.proxyPort : null, - // destinationPort: data.mode === "port" ? data.destinationPort : null, - destination: data.destination, - alias: - data.alias && - typeof data.alias === "string" && - data.alias.trim() - ? data.alias - : null, - tcpPortRangeString: data.tcpPortRangeString, - udpPortRangeString: data.udpPortRangeString, - disableIcmp: data.disableIcmp ?? false, - roleIds: (data.roles || []).map((r) => parseInt(r.id)), - userIds: (data.users || []).map((u) => u.id), - clientIds: (data.clients || []).map((c) => parseInt(c.id)) - } - ); + await api.post(`/site-resource/${resource.id}`, { + name: data.name, + siteId: data.siteId, + mode: data.mode, + // protocol: data.mode === "port" ? data.protocol : null, + // proxyPort: data.mode === "port" ? data.proxyPort : null, + // destinationPort: data.mode === "port" ? data.destinationPort : null, + destination: data.destination, + alias: + data.alias && + typeof data.alias === "string" && + data.alias.trim() + ? data.alias + : null, + tcpPortRangeString: data.tcpPortRangeString, + udpPortRangeString: data.udpPortRangeString, + disableIcmp: data.disableIcmp ?? false, + roleIds: (data.roles || []).map((r) => parseInt(r.id)), + userIds: (data.users || []).map((u) => u.id), + clientIds: (data.clients || []).map((c) => parseInt(c.id)) + }); // Update roles, users, and clients // await Promise.all([ @@ -502,8 +518,8 @@ export default function EditInternalResourceDialog({ variant: "default" }); - onSuccess?.(); setOpen(false); + onSuccess?.(); } catch (error) { console.error("Error updating internal resource:", error); toast({ @@ -543,18 +559,26 @@ export default function EditInternalResourceDialog({ clients: [] }); // Reset port mode state - setTcpPortMode(getPortModeFromString(resource.tcpPortRangeString)); - setUdpPortMode(getPortModeFromString(resource.udpPortRangeString)); + setTcpPortMode( + getPortModeFromString(resource.tcpPortRangeString) + ); + setUdpPortMode( + getPortModeFromString(resource.udpPortRangeString) + ); setTcpCustomPorts( - resource.tcpPortRangeString && resource.tcpPortRangeString !== "*" + resource.tcpPortRangeString && + resource.tcpPortRangeString !== "*" ? resource.tcpPortRangeString : "" ); setUdpCustomPorts( - resource.udpPortRangeString && resource.udpPortRangeString !== "*" + resource.udpPortRangeString && + resource.udpPortRangeString !== "*" ? resource.udpPortRangeString : "" ); + // Reset visibility states + setIsPortsExpanded(false); previousResourceId.current = resource.id; } @@ -602,25 +626,33 @@ export default function EditInternalResourceDialog({ clients: [] }); // Reset port mode state - setTcpPortMode(getPortModeFromString(resource.tcpPortRangeString)); - setUdpPortMode(getPortModeFromString(resource.udpPortRangeString)); + setTcpPortMode( + getPortModeFromString(resource.tcpPortRangeString) + ); + setUdpPortMode( + getPortModeFromString(resource.udpPortRangeString) + ); setTcpCustomPorts( - resource.tcpPortRangeString && resource.tcpPortRangeString !== "*" + resource.tcpPortRangeString && + resource.tcpPortRangeString !== "*" ? resource.tcpPortRangeString : "" ); setUdpCustomPorts( - resource.udpPortRangeString && resource.udpPortRangeString !== "*" + resource.udpPortRangeString && + resource.udpPortRangeString !== "*" ? resource.udpPortRangeString : "" ); + // Reset visibility states + setIsPortsExpanded(false); // Reset previous resource ID to ensure clean state on next open previousResourceId.current = null; } setOpen(open); }} > - + {t("editInternalResourceDialogEditClientResource")} @@ -639,627 +671,628 @@ export default function EditInternalResourceDialog({ className="space-y-6" id="edit-internal-resource-form" > - {/* Resource Properties Form */} -
-

- {t( - "editInternalResourceDialogResourceProperties" + {/* Name and Site - Side by Side */} +
+ ( + + + {t( + "editInternalResourceDialogName" + )} + + + + + + )} -

-
- ( - - - {t( - "editInternalResourceDialogName" - )} - - - - - - - )} - /> + /> - ( - - - {t( - "site" - )} - - - - - - - - - - - - - {t( - "noSitesFound" - )} - - - {availableSites.map( - ( - site - ) => ( - { - field.onChange( - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - - )} - /> - - ( - - - {t( - "editInternalResourceDialogMode" - )} - - - - - )} - /> - - {/* {mode === "port" && ( -
- ( - - {t("editInternalResourceDialogProtocol")} - - - - )} - /> - - ( - - {t("editInternalResourceDialogSitePort")} - - field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)} - /> - - - - )} - /> -
- )} */} -
-
- - {/* Target Configuration Form */} -
-

- {t( - "editInternalResourceDialogTargetConfiguration" + {field.value + ? availableSites.find( + (site) => + site.siteId === + field.value + )?.name + : t( + "selectSite" + )} + + + + + + + + + + {t( + "noSitesFound" + )} + + + {availableSites.map( + (site) => ( + { + field.onChange( + site.siteId + ); + }} + > + + { + site.name + } + + ) + )} + + + + + + + )} -

-
- ( - - - {t( - "editInternalResourceDialogDestination" - )} - - - - - - {mode === "host" && - t( - "editInternalResourceDialogDestinationHostDescription" - )} - {mode === "cidr" && - t( - "editInternalResourceDialogDestinationCidrDescription" - )} - {/* {mode === "port" && t("editInternalResourceDialogDestinationIPDescription")} */} - - - - )} - /> - - {/* {mode === "port" && ( - ( - - {t("targetPort")} - - field.onChange(e.target.value === "" ? undefined : parseInt(e.target.value) || 0)} - /> - - - - )} - /> - )} */} -
+ />
- {/* Alias */} - {mode !== "cidr" && ( -
- ( - - - {t( - "editInternalResourceDialogAlias" + {/* Tabs for Network Settings and Access Control */} + + {/* Network Settings Tab */} +
+
+
+ +
+ {t( + "editInternalResourceDialogDestinationDescription" + )} +
+
+ +
+ {/* Mode - Smaller select */} +
+ ( + + + {t( + "editInternalResourceDialogMode" + )} + + + + )} - - - +
+ + {/* Destination - Larger input */} +
+ ( + + + {t( + "editInternalResourceDialogDestination" + )} + + + + + + + )} + /> +
+ + {/* Alias - Equally sized input (if allowed) */} + {mode !== "cidr" && ( +
+ ( + + + {t( + "editInternalResourceDialogAlias" + )} + + + + + + + )} /> - - - {t( - "editInternalResourceDialogAliasDescription" - )} - - - - )} - /> -
- )} - - {/* Port Restrictions Section */} -
-

- {t("portRestrictions")} -

-
- {/* TCP Ports */} - ( - -
- - TCP - - {/**/} - - {tcpPortMode === "custom" ? ( - - - setTcpCustomPorts(e.target.value) - } - className="flex-1" - /> - - ) : ( - - )}
- -
- )} - /> - - {/* UDP Ports */} - ( - -
- - UDP - - {/**/} - - {udpPortMode === "custom" ? ( - - - setUdpCustomPorts(e.target.value) - } - className="flex-1" - /> - - ) : ( - - )} -
- -
- )} - /> - - {/* ICMP Toggle */} - ( - -
- - ICMP - - - field.onChange(!checked)} - /> - - - {field.value ? t("blocked") : t("allowed")} - -
- -
- )} - /> -
-
- - {/* Access Control Section */} -
-

- {t("resourceUsersRoles")} -

- {loadingRolesUsers ? ( -
- {t("loading")} + )} +
- ) : ( + + {/* Ports and Restrictions */}
- ( - - - {t("roles")} - - - { - form.setValue( - "roles", - newRoles as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allRoles - } - allowDuplicates={ - false - } - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - - {t( - "resourceRoleDescription" - )} - - + {/* TCP Ports */} +
+ +
+ {t( + "editInternalResourceDialogPortRestrictionsDescription" + )} +
+
+
- ( - - - {t("users")} - - - { - form.setValue( - "users", - newUsers as [ - Tag, - ...Tag[] - ] - ); - }} - enableAutocomplete={ - true - } - autocompleteOptions={ - allUsers - } - allowDuplicates={ - false - } - restrictTagsToAutocompleteOptions={ - true - } - sortTags={true} - /> - - - + > +
+ + {t( + "editInternalResourceDialogTcp" + )} + +
+
+ ( + +
+ {/**/} + + {tcpPortMode === + "custom" ? ( + + + setTcpCustomPorts( + e + .target + .value + ) + } + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+ + {/* UDP Ports */} +
- {hasMachineClients && ( + > +
+ + {t( + "editInternalResourceDialogUdp" + )} + +
+
+ ( + +
+ {/**/} + + {udpPortMode === + "custom" ? ( + + + setUdpCustomPorts( + e + .target + .value + ) + } + /> + + ) : ( + + )} +
+ +
+ )} + /> +
+
+ + {/* ICMP Toggle */} +
+
+ + {t( + "editInternalResourceDialogIcmp" + )} + +
+
+ ( + +
+ + + field.onChange( + !checked + ) + } + /> + + + {field.value + ? t( + "blocked" + ) + : t( + "allowed" + )} + +
+ +
+ )} + /> +
+
+
+
+ + {/* Access Control Tab */} +
+
+ +
+ {t( + "editInternalResourceDialogAccessControlDescription" + )} +
+
+ {loadingRolesUsers ? ( +
+ {t("loading")} +
+ ) : ( +
+ {/* Roles */} ( - {t( - "machineClients" - )} + {t("roles")} { form.setValue( - "clients", - newClients as [ + "roles", + newRoles as [ Tag, ...Tag[] ] @@ -1269,7 +1302,7 @@ export default function EditInternalResourceDialog({ true } autocompleteOptions={ - machineClients + allRoles } allowDuplicates={ false @@ -1284,10 +1317,135 @@ export default function EditInternalResourceDialog({ )} /> - )} -
- )} -
+ + {/* Users */} + ( + + + {t("users")} + + + { + form.setValue( + "users", + newUsers as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + allUsers + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={true} + /> + + + + )} + /> + + {/* Clients (Machines) */} + {hasMachineClients && ( + ( + + + {t( + "machineClients" + )} + + + { + form.setValue( + "clients", + newClients as [ + Tag, + ...Tag[] + ] + ); + }} + enableAutocomplete={ + true + } + autocompleteOptions={ + machineClients + } + allowDuplicates={ + false + } + restrictTagsToAutocompleteOptions={ + true + } + sortTags={ + true + } + /> + + + + )} + /> + )} +
+ )} +
+
diff --git a/src/components/HorizontalTabs.tsx b/src/components/HorizontalTabs.tsx index 72093e0d..717a3c12 100644 --- a/src/components/HorizontalTabs.tsx +++ b/src/components/HorizontalTabs.tsx @@ -1,6 +1,6 @@ "use client"; -import React from "react"; +import React, { useState } from "react"; import Link from "next/link"; import { useParams, usePathname } from "next/navigation"; import { cn } from "@app/lib/cn"; @@ -20,17 +20,22 @@ interface HorizontalTabsProps { children: React.ReactNode; items: TabItem[]; disabled?: boolean; + clientSide?: boolean; + defaultTab?: number; } export function HorizontalTabs({ children, items, - disabled = false + disabled = false, + clientSide = false, + defaultTab = 0 }: HorizontalTabsProps) { const pathname = usePathname(); const params = useParams(); const { licenseStatus, isUnlocked } = useLicenseStatusContext(); const t = useTranslations(); + const [activeClientTab, setActiveClientTab] = useState(defaultTab); function hydrateHref(href: string) { return href @@ -43,6 +48,73 @@ export function HorizontalTabs({ .replace("{remoteExitNodeId}", params.remoteExitNodeId as string); } + // Client-side mode: render tabs as buttons with state management + if (clientSide) { + const childrenArray = React.Children.toArray(children); + const activeChild = childrenArray[activeClientTab] || null; + + return ( +
+
+
+
+ {items.map((item, index) => { + const isActive = activeClientTab === index; + const isProfessional = + item.showProfessional && !isUnlocked(); + const isDisabled = + disabled || + (isProfessional && !isUnlocked()); + + return ( + + ); + })} +
+
+
+
{activeChild}
+
+ ); + } + + // Server-side mode: original behavior with routing return (
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index c3037250..f530ace1 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -15,7 +15,7 @@ const buttonVariants = cva( destructive: "bg-destructive text-white dark:text-destructive-foreground hover:bg-destructive/90 ", outline: - "border border-input bg-card hover:bg-accent hover:text-accent-foreground ", + "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground ", outlinePrimary: "border border-primary bg-card hover:bg-primary/10 text-primary ", secondary: diff --git a/src/lib/queries.ts b/src/lib/queries.ts index 0dc44147..e90f6eea 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -228,7 +228,7 @@ export const resourceQueries = { queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< AxiosResponse - >(`/resource/${resourceId}/users`, { signal }); + >(`/site-resource/${resourceId}/users`, { signal }); return res.data.data.users; } }), @@ -238,7 +238,7 @@ export const resourceQueries = { queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< AxiosResponse - >(`/resource/${resourceId}/roles`, { signal }); + >(`/site-resource/${resourceId}/roles`, { signal }); return res.data.data.roles; } @@ -249,7 +249,7 @@ export const resourceQueries = { queryFn: async ({ signal, meta }) => { const res = await meta!.api.get< AxiosResponse - >(`/resource/${resourceId}/clients`, { signal }); + >(`/site-resource/${resourceId}/clients`, { signal }); return res.data.data.clients; } From 3e01bfef7d7e489dd4903f3d6692504f3ec4233e Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 18 Dec 2025 17:07:48 -0500 Subject: [PATCH 135/153] Move primaryDb into driver --- server/db/pg/driver.ts | 1 + server/db/sqlite/driver.ts | 2 +- server/routers/auditLogs/queryRequestAnalytics.ts | 7 +------ server/routers/auditLogs/queryRequestAuditLog.ts | 7 +------ 4 files changed, 4 insertions(+), 13 deletions(-) diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts index 2ee34da6..5b357d06 100644 --- a/server/db/pg/driver.ts +++ b/server/db/pg/driver.ts @@ -81,6 +81,7 @@ function createDb() { export const db = createDb(); export default db; +export const primaryDb = db.$primary; export type Transaction = Parameters< Parameters<(typeof db)["transaction"]>[0] >[0]; diff --git a/server/db/sqlite/driver.ts b/server/db/sqlite/driver.ts index 5a4aa542..9cbc8d7b 100644 --- a/server/db/sqlite/driver.ts +++ b/server/db/sqlite/driver.ts @@ -20,7 +20,7 @@ function createDb() { export const db = createDb(); export default db; -export const driver: "pg" | "sqlite" = "sqlite"; +export const primaryDb = db; export type Transaction = Parameters< Parameters<(typeof db)["transaction"]>[0] >[0]; diff --git a/server/routers/auditLogs/queryRequestAnalytics.ts b/server/routers/auditLogs/queryRequestAnalytics.ts index f4b4444c..cd1218ce 100644 --- a/server/routers/auditLogs/queryRequestAnalytics.ts +++ b/server/routers/auditLogs/queryRequestAnalytics.ts @@ -1,4 +1,4 @@ -import { db, requestAuditLog, driver } from "@server/db"; +import { db, requestAuditLog, driver, primaryDb } from "@server/db"; import { registry } from "@server/openApi"; import { NextFunction } from "express"; import { Request, Response } from "express"; @@ -12,11 +12,6 @@ import response from "@server/lib/response"; import logger from "@server/logger"; import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo"; -let primaryDb = db; -if (driver == "pg") { - primaryDb = db.$primary as typeof db; // select the primary instance in a replicated setup -} - const queryAccessAuditLogsQuery = z.object({ // iso string just validate its a parseable date timeStart: z diff --git a/server/routers/auditLogs/queryRequestAuditLog.ts b/server/routers/auditLogs/queryRequestAuditLog.ts index 73f9fc43..602b4475 100644 --- a/server/routers/auditLogs/queryRequestAuditLog.ts +++ b/server/routers/auditLogs/queryRequestAuditLog.ts @@ -1,4 +1,4 @@ -import { db, driver, requestAuditLog, resources } from "@server/db"; +import { db, primaryDb, requestAuditLog, resources } from "@server/db"; import { registry } from "@server/openApi"; import { NextFunction } from "express"; import { Request, Response } from "express"; @@ -13,11 +13,6 @@ import response from "@server/lib/response"; import logger from "@server/logger"; import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo"; -let primaryDb = db; -if (driver == "pg") { - primaryDb = db.$primary as typeof db; // select the primary instance in a replicated setup -} - export const queryAccessAuditLogsQuery = z.object({ // iso string just validate its a parseable date timeStart: z From 6e7ba1dc52309d72c8f4a6b7d4ff5708256b7633 Mon Sep 17 00:00:00 2001 From: Owen Date: Thu, 18 Dec 2025 17:08:38 -0500 Subject: [PATCH 136/153] Prevent overlapping resources with org subnets --- .../siteResource/createSiteResource.ts | 39 +++++++++++++++++-- .../siteResource/updateSiteResource.ts | 38 +++++++++++++++++- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index c50a800b..d2196e87 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -2,6 +2,7 @@ import { clientSiteResources, db, newts, + orgs, roles, roleSiteResources, SiteResource, @@ -10,7 +11,7 @@ import { userSiteResources } from "@server/db"; import { getUniqueSiteResourceName } from "@server/db/names"; -import { getNextAvailableAliasAddress, portRangeStringSchema } from "@server/lib/ip"; +import { getNextAvailableAliasAddress, isIpInCidr, portRangeStringSchema } from "@server/lib/ip"; import { rebuildClientAssociationsFromSiteResource } from "@server/lib/rebuildClientAssociations"; import response from "@server/lib/response"; import logger from "@server/logger"; @@ -84,8 +85,7 @@ const createSiteResourceSchema = z if (data.mode === "cidr") { // Check if it's a valid CIDR (v4 or v6) const isValidCIDR = z - // .union([z.cidrv4(), z.cidrv6()]) - .union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere + .union([z.cidrv4(), z.cidrv6()]) .safeParse(data.destination).success; return isValidCIDR; } @@ -175,6 +175,39 @@ export async function createSiteResource( return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); } + const [org] = await db + .select() + .from(orgs) + .where(eq(orgs.orgId, orgId)) + .limit(1); + + if (!org) { + return next(createHttpError(HttpCode.NOT_FOUND, "Organization not found")); + } + + if (!org.subnet || !org.utilitySubnet) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Organization with ID ${orgId} has no subnet or utilitySubnet defined defined` + ) + ); + } + + // Only check if destination is an IP address + const isIp = z.union([z.ipv4(), z.ipv6()]).safeParse(destination).success; + if ( + isIp && + (isIpInCidr(destination, org.subnet) || isIpInCidr(destination, org.utilitySubnet)) + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "IP can not be in the CIDR range of the organization's subnet or utility subnet" + ) + ); + } + // // check if resource with same protocol and proxy port already exists (only for port mode) // if (mode === "port" && protocol && proxyPort) { // const [existingResource] = await db diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 10708443..c0383616 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -5,6 +5,7 @@ import { clientSiteResourcesAssociationsCache, db, newts, + orgs, roles, roleSiteResources, sites, @@ -24,6 +25,7 @@ import { generateAliasConfig, generateRemoteSubnets, generateSubnetProxyTargets, + isIpInCidr, portRangeStringSchema } from "@server/lib/ip"; import { @@ -96,8 +98,7 @@ const updateSiteResourceSchema = z if (data.mode === "cidr" && data.destination) { // Check if it's a valid CIDR (v4 or v6) const isValidCIDR = z - // .union([z.cidrv4(), z.cidrv6()]) - .union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere + .union([z.cidrv4(), z.cidrv6()]) .safeParse(data.destination).success; return isValidCIDR; } @@ -196,6 +197,39 @@ export async function updateSiteResource( ); } + const [org] = await db + .select() + .from(orgs) + .where(eq(orgs.orgId, existingSiteResource.orgId)) + .limit(1); + + if (!org) { + return next(createHttpError(HttpCode.NOT_FOUND, "Organization not found")); + } + + if (!org.subnet || !org.utilitySubnet) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Organization with ID ${existingSiteResource.orgId} has no subnet or utilitySubnet defined defined` + ) + ); + } + + // Only check if destination is an IP address + const isIp = z.union([z.ipv4(), z.ipv6()]).safeParse(destination).success; + if ( + isIp && + (isIpInCidr(destination!, org.subnet) || isIpInCidr(destination!, org.utilitySubnet)) + ) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "IP can not be in the CIDR range of the organization's subnet or utility subnet" + ) + ); + } + let existingSite = site; let siteChanged = false; if (existingSiteResource.siteId !== siteId) { From fc924f707c6b9251fed3c51778511b695b038c79 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 18 Dec 2025 17:47:54 -0500 Subject: [PATCH 137/153] add banners --- messages/en-US.json | 14 +++ .../[orgId]/settings/clients/machine/page.tsx | 3 + .../settings/resources/client/page.tsx | 3 + .../[orgId]/settings/resources/proxy/page.tsx | 3 + src/app/[orgId]/settings/sites/page.tsx | 3 + src/components/ClientDownloadBanner.tsx | 69 +++++++++++++ src/components/DismissableBanner.tsx | 98 +++++++++++++++++++ src/components/MachineClientsBanner.tsx | 60 ++++++++++++ src/components/PrivateResourcesBanner.tsx | 54 ++++++++++ src/components/ProxyResourcesBanner.tsx | 23 +++++ src/components/SitesBanner.tsx | 40 ++++++++ src/components/UserDevicesTable.tsx | 3 + 12 files changed, 373 insertions(+) create mode 100644 src/components/ClientDownloadBanner.tsx create mode 100644 src/components/DismissableBanner.tsx create mode 100644 src/components/MachineClientsBanner.tsx create mode 100644 src/components/PrivateResourcesBanner.tsx create mode 100644 src/components/ProxyResourcesBanner.tsx create mode 100644 src/components/SitesBanner.tsx diff --git a/messages/en-US.json b/messages/en-US.json index b71eb202..e0728c94 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -51,6 +51,9 @@ "siteQuestionRemove": "Are you sure you want to remove the site from the organization?", "siteManageSites": "Manage Sites", "siteDescription": "Create and manage sites to enable connectivity to private networks", + "sitesBannerTitle": "Connect Any Network", + "sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.", + "sitesBannerButtonText": "Install Site", "siteCreate": "Create Site", "siteCreateDescription2": "Follow the steps below to create and connect a new site", "siteCreateDescription": "Create a new site to start connecting resources", @@ -147,8 +150,12 @@ "shareErrorSelectResource": "Please select a resource", "proxyResourceTitle": "Manage Public Resources", "proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser", + "proxyResourcesBannerTitle": "Web-based Public Access", + "proxyResourcesBannerDescription": "Public resources are HTTPS or TCP/UDP proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.", "clientResourceTitle": "Manage Private Resources", "clientResourceDescription": "Create and manage resources that are only accessible through a connected client", + "privateResourcesBannerTitle": "Zero-Trust Private Access", + "privateResourcesBannerDescription": "Private resources use zero-trust security, ensuring users and machines can only access resources you explicitly grant. Connect user devices or machine clients to access these resources over a secure virtual private network.", "resourcesSearch": "Search resources...", "resourceAdd": "Add Resource", "resourceErrorDelte": "Error deleting resource", @@ -1944,8 +1951,15 @@ "beta": "Beta", "manageUserDevices": "User Devices", "manageUserDevicesDescription": "View and manage devices that users use to privately connect to resources", + "downloadClientBannerTitle": "Download Pangolin Client", + "downloadClientBannerDescription": "Download the Pangolin client for your system to connect to the Pangolin network and access resources privately", "manageMachineClients": "Manage Machine Clients", "manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources", + "machineClientsBannerTitle": "Servers & Automated Systems", + "machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can run with Pangolin CLI, Olm CLI, or Olm as a container.", + "machineClientsBannerPangolinCLI": "Pangolin CLI", + "machineClientsBannerOlmCLI": "Olm CLI", + "machineClientsBannerOlmContainer": "Olm Container", "clientsTableUserClients": "User", "clientsTableMachineClients": "Machine", "licenseTableValidUntil": "Valid Until", diff --git a/src/app/[orgId]/settings/clients/machine/page.tsx b/src/app/[orgId]/settings/clients/machine/page.tsx index e1a904ad..f2618bc2 100644 --- a/src/app/[orgId]/settings/clients/machine/page.tsx +++ b/src/app/[orgId]/settings/clients/machine/page.tsx @@ -1,6 +1,7 @@ import type { ClientRow } from "@app/components/MachineClientsTable"; import MachineClientsTable from "@app/components/MachineClientsTable"; import SettingsSectionTitle from "@app/components/SettingsSectionTitle"; +import MachineClientsBanner from "@app/components/MachineClientsBanner"; import { internal } from "@app/lib/api"; import { authCookieHeader } from "@app/lib/api/cookies"; import { ListClientsResponse } from "@server/routers/client"; @@ -71,6 +72,8 @@ export default async function ClientsPage(props: ClientsPageProps) { description={t("manageMachineClientsDescription")} /> + + + + + + + + ); diff --git a/src/components/ClientDownloadBanner.tsx b/src/components/ClientDownloadBanner.tsx new file mode 100644 index 00000000..dcd572fd --- /dev/null +++ b/src/components/ClientDownloadBanner.tsx @@ -0,0 +1,69 @@ +"use client"; + +import React from "react"; +import { Button } from "@app/components/ui/button"; +import { Download } from "lucide-react"; +import { FaApple, FaWindows, FaLinux } from "react-icons/fa"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import DismissableBanner from "./DismissableBanner"; + +export const ClientDownloadBanner = () => { + const t = useTranslations(); + + return ( + } + description={t("downloadClientBannerDescription")} + > + + + + + + + + + + + ); +}; + +export default ClientDownloadBanner; + diff --git a/src/components/DismissableBanner.tsx b/src/components/DismissableBanner.tsx new file mode 100644 index 00000000..6f49e036 --- /dev/null +++ b/src/components/DismissableBanner.tsx @@ -0,0 +1,98 @@ +"use client"; + +import React, { useState, useEffect, type ReactNode } from "react"; +import { Card, CardContent } from "@app/components/ui/card"; +import { X } from "lucide-react"; +import { useTranslations } from "next-intl"; + +type DismissableBannerProps = { + storageKey: string; + version: number; + title: string; + titleIcon: ReactNode; + description: string; + children?: ReactNode; +}; + +export const DismissableBanner = ({ + storageKey, + version, + title, + titleIcon, + description, + children +}: DismissableBannerProps) => { + const [isDismissed, setIsDismissed] = useState(true); + const t = useTranslations(); + + useEffect(() => { + const dismissedData = localStorage.getItem(storageKey); + if (dismissedData) { + try { + const parsed = JSON.parse(dismissedData); + // If version matches, use the dismissed state + if (parsed.version === version) { + setIsDismissed(parsed.dismissed); + } else { + // Version changed, show the banner again + setIsDismissed(false); + } + } catch { + // If parsing fails, check for old format (just "true" string) + if (dismissedData === "true") { + // Old format, show banner again for new version + setIsDismissed(false); + } else { + setIsDismissed(true); + } + } + } else { + setIsDismissed(false); + } + }, [storageKey, version]); + + const handleDismiss = () => { + setIsDismissed(true); + localStorage.setItem( + storageKey, + JSON.stringify({ dismissed: true, version }) + ); + }; + + if (isDismissed) { + return null; + } + + return ( + + + +
+
+

+ {titleIcon} + {title} +

+

+ {description} +

+
+ {children && ( +
+ {children} +
+ )} +
+
+
+ ); +}; + +export default DismissableBanner; + diff --git a/src/components/MachineClientsBanner.tsx b/src/components/MachineClientsBanner.tsx new file mode 100644 index 00000000..f69fa061 --- /dev/null +++ b/src/components/MachineClientsBanner.tsx @@ -0,0 +1,60 @@ +"use client"; + +import React from "react"; +import { Button } from "@app/components/ui/button"; +import { Server, Terminal, Container } from "lucide-react"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import DismissableBanner from "./DismissableBanner"; + +type MachineClientsBannerProps = { + orgId: string; +}; + +export const MachineClientsBanner = ({ + orgId +}: MachineClientsBannerProps) => { + const t = useTranslations(); + + return ( + } + description={t("machineClientsBannerDescription")} + > + + + + + + + + ); +}; + +export default MachineClientsBanner; + diff --git a/src/components/PrivateResourcesBanner.tsx b/src/components/PrivateResourcesBanner.tsx new file mode 100644 index 00000000..8320178d --- /dev/null +++ b/src/components/PrivateResourcesBanner.tsx @@ -0,0 +1,54 @@ +"use client"; + +import React from "react"; +import { Button } from "@app/components/ui/button"; +import { Shield, ArrowRight, Laptop, Server } from "lucide-react"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import DismissableBanner from "./DismissableBanner"; + +type PrivateResourcesBannerProps = { + orgId: string; +}; + +export const PrivateResourcesBanner = ({ + orgId +}: PrivateResourcesBannerProps) => { + const t = useTranslations(); + + return ( + } + description={t("privateResourcesBannerDescription")} + > + + + + + + + + ); +}; + +export default PrivateResourcesBanner; + diff --git a/src/components/ProxyResourcesBanner.tsx b/src/components/ProxyResourcesBanner.tsx new file mode 100644 index 00000000..40616758 --- /dev/null +++ b/src/components/ProxyResourcesBanner.tsx @@ -0,0 +1,23 @@ +"use client"; + +import React from "react"; +import { Globe } from "lucide-react"; +import { useTranslations } from "next-intl"; +import DismissableBanner from "./DismissableBanner"; + +export const ProxyResourcesBanner = () => { + const t = useTranslations(); + + return ( + } + description={t("proxyResourcesBannerDescription")} + /> + ); +}; + +export default ProxyResourcesBanner; + diff --git a/src/components/SitesBanner.tsx b/src/components/SitesBanner.tsx new file mode 100644 index 00000000..8ba7a232 --- /dev/null +++ b/src/components/SitesBanner.tsx @@ -0,0 +1,40 @@ +"use client"; + +import React from "react"; +import { Button } from "@app/components/ui/button"; +import { Plug, ArrowRight } from "lucide-react"; +import { useTranslations } from "next-intl"; +import Link from "next/link"; +import DismissableBanner from "./DismissableBanner"; + +export const SitesBanner = () => { + const t = useTranslations(); + + return ( + } + description={t("sitesBannerDescription")} + > + + + + + ); +}; + +export default SitesBanner; + diff --git a/src/components/UserDevicesTable.tsx b/src/components/UserDevicesTable.tsx index 71321bf8..e413207a 100644 --- a/src/components/UserDevicesTable.tsx +++ b/src/components/UserDevicesTable.tsx @@ -25,6 +25,7 @@ import { useRouter } from "next/navigation"; import { useMemo, useState, useTransition } from "react"; import { Badge } from "./ui/badge"; import { InfoPopup } from "./ui/info-popup"; +import ClientDownloadBanner from "./ClientDownloadBanner"; export type ClientRow = { id: number; @@ -413,6 +414,8 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) { /> )} + + Date: Thu, 18 Dec 2025 17:54:29 -0500 Subject: [PATCH 138/153] sidebar enhancements --- src/components/SidebarNav.tsx | 99 +++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 389f3978..84ce34eb 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -24,7 +24,7 @@ import { PopoverContent, PopoverTrigger } from "@app/components/ui/popover"; -import { ChevronDown } from "lucide-react"; +import { ChevronRight } from "lucide-react"; import { build } from "@server/build"; export type SidebarNavItem = { @@ -51,6 +51,7 @@ export interface SidebarNavProps extends React.HTMLAttributes { type CollapsibleNavItemProps = { item: SidebarNavItem; level: number; + isActive: boolean; isChildActive: boolean; isDisabled: boolean; isCollapsed: boolean; @@ -63,6 +64,7 @@ type CollapsibleNavItemProps = { function CollapsibleNavItem({ item, level, + isActive, isChildActive, isDisabled, isCollapsed, @@ -112,30 +114,30 @@ function CollapsibleNavItem({ + {version && ( + +
+ +

+ {t("pangolinUpdateAvailable")} +

+ +
+
+
+ + )} ); } From fea4d43920a408d61527c456ea6332f14d6095b0 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 19 Dec 2025 14:44:48 -0500 Subject: [PATCH 142/153] Make utility subnet configurable --- messages/en-US.json | 2 ++ server/lib/ip.ts | 23 ++++++++++++ server/lib/readConfigFile.ts | 4 +-- server/routers/org/createOrg.ts | 20 ++++++++--- server/routers/org/pickOrgDefaults.ts | 6 +++- src/app/setup/page.tsx | 51 ++++++++++++++++++++------- 6 files changed, 86 insertions(+), 20 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index e0728c94..c6028ec2 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -2300,6 +2300,8 @@ "setupFailedToFetchSubnet": "Failed to fetch default subnet", "setupSubnetAdvanced": "Subnet (Advanced)", "setupSubnetDescription": "The subnet for this organization's internal network.", + "setupUtilitySubnet": "Utility Subnet (Advanced)", + "setupUtilitySubnetDescription": "The subnet for this organization's alias addresses and DNS server.", "siteRegenerateAndDisconnect": "Regenerate and Disconnect", "siteRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this site?", "siteRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the site. The site will need to be restarted with the new credentials.", diff --git a/server/lib/ip.ts b/server/lib/ip.ts index 21c148ac..87a0c3c6 100644 --- a/server/lib/ip.ts +++ b/server/lib/ip.ts @@ -301,6 +301,29 @@ export function isIpInCidr(ip: string, cidr: string): boolean { return ipBigInt >= range.start && ipBigInt <= range.end; } +/** + * Checks if two CIDR ranges overlap + * @param cidr1 First CIDR string + * @param cidr2 Second CIDR string + * @returns boolean indicating if the two CIDRs overlap + */ +export function doCidrsOverlap(cidr1: string, cidr2: string): boolean { + const version1 = detectIpVersion(cidr1.split("/")[0]); + const version2 = detectIpVersion(cidr2.split("/")[0]); + if (version1 !== version2) { + // Different IP versions cannot overlap + return false; + } + const range1 = cidrToRange(cidr1); + const range2 = cidrToRange(cidr2); + + // Overlap if the ranges intersect + return ( + range1.start <= range2.end && + range2.start <= range1.end + ); +} + export async function getNextAvailableClientSubnet( orgId: string, transaction: Transaction | typeof db = db diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index fe610663..365bcb13 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -255,11 +255,11 @@ export const configSchema = z orgs: z .object({ block_size: z.number().positive().gt(0).optional().default(24), - subnet_group: z.string().optional().default("100.90.128.0/24"), + subnet_group: z.string().optional().default("100.90.128.0/20"), utility_subnet_group: z .string() .optional() - .default("100.96.128.0/24") //just hardcode this for now as well + .default("100.96.128.0/20") //just hardcode this for now as well }) .optional() .default({ diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index f1d06566..e93af889 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -27,6 +27,7 @@ import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; import { build } from "@server/build"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; +import { doCidrsOverlap } from "@server/lib/ip"; const createOrgSchema = z.strictObject({ orgId: z.string(), @@ -36,6 +37,11 @@ const createOrgSchema = z.strictObject({ .union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere .refine((val) => isValidCIDR(val), { message: "Invalid subnet CIDR" + }), + utilitySubnet: z + .union([z.cidrv4()]) // for now lets just do ipv4 until we verify ipv6 works everywhere + .refine((val) => isValidCIDR(val), { + message: "Invalid utility subnet CIDR" }) }); @@ -84,7 +90,7 @@ export async function createOrg( ); } - const { orgId, name, subnet } = parsedBody.data; + const { orgId, name, subnet, utilitySubnet } = parsedBody.data; // TODO: for now we are making all of the orgs the same subnet // make sure the subnet is unique @@ -119,6 +125,15 @@ export async function createOrg( ); } + if (doCidrsOverlap(subnet, utilitySubnet)) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Subnet ${subnet} overlaps with utility subnet ${utilitySubnet}` + ) + ); + } + let error = ""; let org: Org | null = null; @@ -128,9 +143,6 @@ export async function createOrg( .from(domains) .where(eq(domains.configManaged, true)); - const utilitySubnet = - config.getRawConfig().orgs.utility_subnet_group; - const newOrg = await trx .insert(orgs) .values({ diff --git a/server/routers/org/pickOrgDefaults.ts b/server/routers/org/pickOrgDefaults.ts index 771b0d99..cce46a01 100644 --- a/server/routers/org/pickOrgDefaults.ts +++ b/server/routers/org/pickOrgDefaults.ts @@ -8,6 +8,7 @@ import config from "@server/lib/config"; export type PickOrgDefaultsResponse = { subnet: string; + utilitySubnet: string; }; export async function pickOrgDefaults( @@ -20,10 +21,13 @@ export async function pickOrgDefaults( // const subnet = await getNextAvailableOrgSubnet(); // Just hard code the subnet for now for everyone const subnet = config.getRawConfig().orgs.subnet_group; + const utilitySubnet = + config.getRawConfig().orgs.utility_subnet_group; return response(res, { data: { - subnet: subnet + subnet: subnet, + utilitySubnet: utilitySubnet }, success: true, error: false, diff --git a/src/app/setup/page.tsx b/src/app/setup/page.tsx index 36853e5c..10a8b14e 100644 --- a/src/app/setup/page.tsx +++ b/src/app/setup/page.tsx @@ -41,13 +41,14 @@ export default function StepperForm() { const [loading, setLoading] = useState(false); const [isChecked, setIsChecked] = useState(false); - const [error, setError] = useState(null); + // Removed error state, now using toast for API errors const [orgCreated, setOrgCreated] = useState(false); const orgSchema = z.object({ orgName: z.string().min(1, { message: t("orgNameRequired") }), orgId: z.string().min(1, { message: t("orgIdRequired") }), - subnet: z.string().min(1, { message: t("subnetRequired") }) + subnet: z.string().min(1, { message: t("subnetRequired") }), + utilitySubnet: z.string().min(1, { message: t("subnetRequired") }) }); const orgForm = useForm({ @@ -55,7 +56,8 @@ export default function StepperForm() { defaultValues: { orgName: "", orgId: "", - subnet: "" + subnet: "", + utilitySubnet: "" } }); @@ -72,6 +74,7 @@ export default function StepperForm() { const res = await api.get(`/pick-org-defaults`); if (res && res.data && res.data.data) { orgForm.setValue("subnet", res.data.data.subnet); + orgForm.setValue("utilitySubnet", res.data.data.utilitySubnet); } } catch (e) { console.error("Failed to fetch default subnet:", e); @@ -129,7 +132,8 @@ export default function StepperForm() { const res = await api.put(`/org`, { orgId: values.orgId, name: values.orgName, - subnet: values.subnet + subnet: values.subnet, + utilitySubnet: values.utilitySubnet }); if (res && res.status === 201) { @@ -138,7 +142,11 @@ export default function StepperForm() { } } catch (e) { console.error(e); - setError(formatAxiosError(e, t("orgErrorCreate"))); + toast({ + title: t("error"), + description: formatAxiosError(e, t("orgErrorCreate")), + variant: "destructive" + }); } setLoading(false); @@ -320,6 +328,30 @@ export default function StepperForm() { )} /> + ( + + + {t("setupUtilitySubnet")} + + + + + + + {t( + "setupUtilitySubnetDescription" + )} + + + )} + /> + {orgIdTaken && !orgCreated ? ( @@ -328,20 +360,13 @@ export default function StepperForm() { ) : null} - {error && ( - - - {error} - - - )} + {/* Error Alert removed, errors now shown as toast */}
- -
- )} - - -
+ ); } diff --git a/src/components/DashboardLoginForm.tsx b/src/components/DashboardLoginForm.tsx index e64a4c70..ccb5c497 100644 --- a/src/components/DashboardLoginForm.tsx +++ b/src/components/DashboardLoginForm.tsx @@ -17,7 +17,6 @@ import { cleanRedirect } from "@app/lib/cleanRedirect"; import BrandingLogo from "@app/components/BrandingLogo"; import { useTranslations } from "next-intl"; import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; -import { build } from "@server/build"; type DashboardLoginFormProps = { redirect?: string; @@ -49,14 +48,9 @@ export default function DashboardLoginForm({ ? env.branding.logo?.authPage?.height || 58 : 58; - const gradientClasses = - build === "saas" - ? "border-b border-primary/30 bg-gradient-to-br dark:from-primary/20 from-primary/20 via-background to-background overflow-hidden rounded-t-lg" - : "border-b"; - return ( - +
diff --git a/src/components/OrgInfoCard.tsx b/src/components/OrgInfoCard.tsx new file mode 100644 index 00000000..cac8eb3f --- /dev/null +++ b/src/components/OrgInfoCard.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { Alert, AlertDescription } from "@/components/ui/alert"; +import { useOrgContext } from "@app/hooks/useOrgContext"; +import { + InfoSection, + InfoSectionContent, + InfoSections, + InfoSectionTitle +} from "@app/components/InfoSection"; +import { useTranslations } from "next-intl"; + +type OrgInfoCardProps = {}; + +export default function OrgInfoCard({}: OrgInfoCardProps) { + const { org } = useOrgContext(); + const t = useTranslations(); + + return ( + + + + + {t("name")} + {org.org.name} + + + {t("orgId")} + {org.org.orgId} + + + {t("subnet")} + + {org.org.subnet || t("none")} + + + + + + ); +} + diff --git a/src/components/OrgLoginPage.tsx b/src/components/OrgLoginPage.tsx new file mode 100644 index 00000000..78b831bc --- /dev/null +++ b/src/components/OrgLoginPage.tsx @@ -0,0 +1,122 @@ +import { LoginFormIDP } from "@app/components/LoginForm"; +import { + LoadLoginPageBrandingResponse, + LoadLoginPageResponse +} from "@server/routers/loginPage/types"; +import IdpLoginButtons from "@app/components/private/IdpLoginButtons"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle +} from "@app/components/ui/card"; +import { Button } from "@app/components/ui/button"; +import Link from "next/link"; +import { replacePlaceholder } from "@app/lib/replacePlaceholder"; +import { getTranslations } from "next-intl/server"; +import { pullEnv } from "@app/lib/pullEnv"; + +type OrgLoginPageProps = { + loginPage: LoadLoginPageResponse | undefined; + loginIdps: LoginFormIDP[]; + branding: LoadLoginPageBrandingResponse | null; + searchParams: { + redirect?: string; + forceLogin?: string; + }; +}; + +function buildQueryString(searchParams: { + redirect?: string; + forceLogin?: string; +}): string { + const params = new URLSearchParams(); + if (searchParams.redirect) { + params.set("redirect", searchParams.redirect); + } + if (searchParams.forceLogin) { + params.set("forceLogin", searchParams.forceLogin); + } + const queryString = params.toString(); + return queryString ? `?${queryString}` : ""; +} + +export default async function OrgLoginPage({ + loginPage, + loginIdps, + branding, + searchParams +}: OrgLoginPageProps) { + const env = pullEnv(); + const t = await getTranslations(); + return ( +
+
+ + {t("poweredBy")}{" "} + + {env.branding.appName || "Pangolin"} + + +
+ + + {branding?.logoUrl && ( +
+ +
+ )} + + {branding?.orgTitle + ? replacePlaceholder(branding.orgTitle, { + orgName: branding.orgName + }) + : t("orgAuthSignInTitle")} + + + {branding?.orgSubtitle + ? replacePlaceholder(branding.orgSubtitle, { + orgName: branding.orgName + }) + : loginIdps.length > 0 + ? t("orgAuthChooseIdpDescription") + : ""} + +
+ + {loginIdps.length > 0 ? ( + + ) : ( +
+

+ {t("orgAuthNoIdpConfigured")} +

+ + + +
+ )} +
+
+
+ ); +} + diff --git a/src/components/OrgSelectionForm.tsx b/src/components/OrgSelectionForm.tsx new file mode 100644 index 00000000..51d84d36 --- /dev/null +++ b/src/components/OrgSelectionForm.tsx @@ -0,0 +1,155 @@ +"use client"; + +import { Button } from "@app/components/ui/button"; +import { Input } from "@app/components/ui/input"; +import { Label } from "@app/components/ui/label"; +import { Card, CardContent, CardHeader } from "@app/components/ui/card"; +import Link from "next/link"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { useState, FormEvent, useEffect } from "react"; +import BrandingLogo from "@app/components/BrandingLogo"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { useLocalStorage } from "@app/hooks/useLocalStorage"; +import { CheckboxWithLabel } from "@app/components/ui/checkbox"; + +export function OrgSelectionForm() { + const router = useRouter(); + const searchParams = useSearchParams(); + const t = useTranslations(); + const { env } = useEnvContext(); + const { isUnlocked } = useLicenseStatusContext(); + const [storedOrgId, setStoredOrgId] = useLocalStorage( + "org-selection:org-id", + null + ); + const [rememberOrgId, setRememberOrgId] = useLocalStorage( + "org-selection:remember", + false + ); + const [orgId, setOrgId] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + // Prefill org ID from storage if remember is enabled + useEffect(() => { + if (rememberOrgId && storedOrgId) { + setOrgId(storedOrgId); + } + }, []); + + const logoWidth = isUnlocked() + ? env.branding.logo?.authPage?.width || 175 + : 175; + const logoHeight = isUnlocked() + ? env.branding.logo?.authPage?.height || 58 + : 58; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + if (!orgId.trim()) return; + + setIsSubmitting(true); + const trimmedOrgId = orgId.trim(); + + // Save org ID to storage if remember is checked + if (rememberOrgId) { + setStoredOrgId(trimmedOrgId); + } else { + setStoredOrgId(null); + } + + const queryString = buildQueryString(searchParams); + const url = `/auth/org/${trimmedOrgId}${queryString}`; + console.log(url); + router.push(url); + }; + + return ( + <> + + +
+ +
+
+

+ {t("orgAuthSelectOrgDescription")} +

+
+
+ +
+
+ + setOrgId(e.target.value)} + required + disabled={isSubmitting} + /> +

+ {t("orgAuthWhatsThis")}{" "} + + {t("learnMore")} + +

+
+ +
+ { + setRememberOrgId(checked === true); + if (!checked) { + setStoredOrgId(null); + } + }} + /> +
+ + +
+
+
+ +

+ + {t("loginBack")} + +

+ + ); +} + +function buildQueryString(searchParams: URLSearchParams): string { + const params = new URLSearchParams(); + if (searchParams.get("redirect")) { + params.set("redirect", searchParams.get("redirect")!); + } + if (searchParams.get("forceLogin")) { + params.set("forceLogin", searchParams.get("forceLogin")!); + } + const queryString = params.toString(); + return queryString ? `?${queryString}` : ""; +} diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index ca051253..194d8b46 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -28,7 +28,7 @@ export function SettingsSectionForm({ className?: string; }) { return ( -
{children}
+
{children}
); } diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 34099558..67837ec6 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -117,7 +117,7 @@ function CollapsibleNavItem({ "flex items-center w-full rounded-md transition-colors", level === 0 ? "px-3 py-2" : "px-3 py-1.5", isActive - ? "bg-secondary text-primary font-medium" + ? "bg-secondary font-medium" : "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground", isDisabled && "cursor-not-allowed opacity-60" )} @@ -258,7 +258,7 @@ export function SidebarNav({ "flex items-center rounded-md transition-colors", isCollapsed ? "px-2 py-2 justify-center" : level === 0 ? "px-3 py-2" : "px-3 py-1.5", isActive - ? "bg-secondary text-primary font-medium" + ? "bg-secondary font-medium" : "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground", isDisabled && "cursor-not-allowed opacity-60" )} @@ -347,7 +347,7 @@ export function SidebarNav({ className={cn( "flex items-center rounded-md transition-colors px-2 py-2 justify-center w-full", isActive || isChildActive - ? "bg-secondary text-primary font-medium" + ? "bg-secondary font-medium" : "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground", isDisabled && "cursor-not-allowed opacity-60" @@ -402,7 +402,7 @@ export function SidebarNav({ className={cn( "flex items-center rounded-md transition-colors px-3 py-1.5 text-sm", childIsActive - ? "bg-secondary text-primary font-medium" + ? "bg-secondary font-medium" : "text-muted-foreground hover:bg-secondary/50 hover:text-foreground", childIsDisabled && "cursor-not-allowed opacity-60" diff --git a/src/components/private/IdpLoginButtons.tsx b/src/components/private/IdpLoginButtons.tsx index c2ec1f5b..e3af2d06 100644 --- a/src/components/private/IdpLoginButtons.tsx +++ b/src/components/private/IdpLoginButtons.tsx @@ -57,9 +57,15 @@ export default function IdpLoginButtons({ let redirectToUrl: string | undefined; try { + console.log( + "generating", + idpId, + redirect || "/", + orgId + ); const response = await generateOidcUrlProxy( idpId, - redirect || "/auth/org?gotoapp=app", + redirect || "/", orgId ); @@ -70,7 +76,6 @@ export default function IdpLoginButtons({ } const data = response.data; - console.log("Redirecting to:", data?.redirectUrl); if (data?.redirectUrl) { redirectToUrl = data.redirectUrl; } diff --git a/src/components/private/ValidateSessionTransferToken.tsx b/src/components/private/ValidateSessionTransferToken.tsx index fcb6a026..c83b61ba 100644 --- a/src/components/private/ValidateSessionTransferToken.tsx +++ b/src/components/private/ValidateSessionTransferToken.tsx @@ -12,6 +12,7 @@ import { TransferSessionResponse } from "@server/routers/auth/types"; type ValidateSessionTransferTokenParams = { token: string; + redirect?: string; }; export default function ValidateSessionTransferToken( @@ -49,7 +50,9 @@ export default function ValidateSessionTransferToken( } if (doRedirect) { - redirect(env.app.dashboardUrl); + // add redirect param to dashboardUrl if provided + const fullUrl = `${env.app.dashboardUrl}${props.redirect || ""}`; + router.push(fullUrl); } } diff --git a/src/contexts/orgContext.ts b/src/contexts/orgContext.ts index e5141bde..99cc8ff4 100644 --- a/src/contexts/orgContext.ts +++ b/src/contexts/orgContext.ts @@ -3,6 +3,7 @@ import { createContext } from "react"; export interface OrgContextType { org: GetOrgResponse; + updateOrg: (updatedOrg: Partial) => void; } const OrgContext = createContext(undefined); diff --git a/src/providers/OrgProvider.tsx b/src/providers/OrgProvider.tsx index 122e0127..34c8c7ef 100644 --- a/src/providers/OrgProvider.tsx +++ b/src/providers/OrgProvider.tsx @@ -10,15 +10,37 @@ interface OrgProviderProps { org: GetOrgResponse | null; } -export function OrgProvider({ children, org }: OrgProviderProps) { +export function OrgProvider({ children, org: serverOrg }: OrgProviderProps) { const t = useTranslations(); - if (!org) { + if (!serverOrg) { throw new Error(t("orgErrorNoProvided")); } + const [org, setOrg] = useState(serverOrg); + + const updateOrg = (updatedOrg: Partial) => { + if (!org) { + throw new Error(t("orgErrorNoUpdate")); + } + setOrg((prev) => { + if (!prev) { + return prev; + } + return { + ...prev, + org: { + ...prev.org, + ...updatedOrg + } + }; + }); + }; + return ( - {children} + + {children} + ); } From afc19f192b112c94e89e266afc821d3c0dba38d1 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 19 Dec 2025 21:57:44 -0500 Subject: [PATCH 145/153] visual enhancements to sidebar --- src/components/LayoutMobileMenu.tsx | 7 +-- src/components/LayoutSidebar.tsx | 12 ++++- src/components/OrgSelector.tsx | 17 +++--- src/components/ProductUpdates.tsx | 4 ++ src/components/SidebarNav.tsx | 80 ++++++++++++++++------------ src/components/ValidateOidcToken.tsx | 25 ++------- src/components/ui/alert.tsx | 2 +- 7 files changed, 78 insertions(+), 69 deletions(-) diff --git a/src/components/LayoutMobileMenu.tsx b/src/components/LayoutMobileMenu.tsx index 49644ff6..7c4da0bc 100644 --- a/src/components/LayoutMobileMenu.tsx +++ b/src/components/LayoutMobileMenu.tsx @@ -73,13 +73,14 @@ export function LayoutMobileMenu({ {t("navbarDescription")}
-
+
-
+
+
{!isAdminPage && user.serverAdmin && (
@@ -113,7 +114,7 @@ export function LayoutMobileMenu({ />
-
+
{env?.app?.version && (
diff --git a/src/components/LayoutSidebar.tsx b/src/components/LayoutSidebar.tsx index c578614d..5d3609d5 100644 --- a/src/components/LayoutSidebar.tsx +++ b/src/components/LayoutSidebar.tsx @@ -109,14 +109,18 @@ export function LayoutSidebar({ isSidebarCollapsed ? "w-16" : "w-64" )} > -
+
-
+
+
{!isAdminPage && user.serverAdmin && (
@@ -153,8 +157,12 @@ export function LayoutSidebar({ isCollapsed={isSidebarCollapsed} />
+ {/* Fade gradient at bottom to indicate scrollable content */} +
+
+
{canShowProductUpdates && (
diff --git a/src/components/OrgSelector.tsx b/src/components/OrgSelector.tsx index 042500dd..b2939a90 100644 --- a/src/components/OrgSelector.tsx +++ b/src/components/OrgSelector.tsx @@ -1,6 +1,5 @@ "use client"; -import { Button } from "@app/components/ui/button"; import { Command, CommandEmpty, @@ -52,13 +51,14 @@ export function OrgSelector({ const orgSelectorContent = ( - +
diff --git a/src/components/ProductUpdates.tsx b/src/components/ProductUpdates.tsx index 630d0800..e3346b4f 100644 --- a/src/components/ProductUpdates.tsx +++ b/src/components/ProductUpdates.tsx @@ -88,6 +88,10 @@ export default function ProductUpdates({ (update) => !productUpdatesRead.includes(update.id) ); + if (filteredUpdates.length === 0 && !showNewVersionPopup) { + return null; + } + return (
{item.icon && ( - {item.icon} + + {item.icon} + )} -
- {t(item.title)} +
+ + {t(item.title)} + {item.isBeta && ( - + {t("beta")} - + )}
@@ -256,7 +257,11 @@ export function SidebarNav({ href={isDisabled ? "#" : hydratedHref} className={cn( "flex items-center rounded-md transition-colors", - isCollapsed ? "px-2 py-2 justify-center" : level === 0 ? "px-3 py-2" : "px-3 py-1.5", + isCollapsed + ? "px-2 py-2 justify-center" + : level === 0 + ? "px-3 py-1.5" + : "px-3 py-1", isActive ? "bg-secondary font-medium" : "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground", @@ -284,21 +289,21 @@ export function SidebarNav({ )} {!isCollapsed && ( <> -
+
{t(item.title)} {item.isBeta && ( - + {t("beta")} - + )}
{build === "enterprise" && item.showEE && !isUnlocked() && ( - + {t("licenseBadge")} )} @@ -309,27 +314,31 @@ export function SidebarNav({
{item.icon && ( - {item.icon} + + {item.icon} + )} -
+
{t(item.title)} {item.isBeta && ( - + {t("beta")} - + )}
{build === "enterprise" && item.showEE && !isUnlocked() && ( - {t("licenseBadge")} + + {t("licenseBadge")} + )}
); @@ -422,23 +431,23 @@ export function SidebarNav({ {childItem.icon} )} -
+
{t(childItem.title)} {childItem.isBeta && ( - + {t("beta")} - + )}
{build === "enterprise" && childItem.showEE && !isUnlocked() && ( - + {t( "licenseBadge" )} @@ -481,7 +490,10 @@ export function SidebarNav({ {...props} > {sections.map((section, sectionIndex) => ( -
0 && "mt-4")}> +
0 && "mt-4")} + > {!isCollapsed && (
{t(`${section.heading}`)} diff --git a/src/components/ValidateOidcToken.tsx b/src/components/ValidateOidcToken.tsx index 3677f625..bd862c11 100644 --- a/src/components/ValidateOidcToken.tsx +++ b/src/components/ValidateOidcToken.tsx @@ -56,11 +56,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { if (props.providerError?.error) { const providerMessage = props.providerError.description || - t("idpErrorOidcProviderRejected", { - error: props.providerError.error, - defaultValue: - "The identity provider returned an error: {error}." - }); + "The identity provider returned an error: {error}."; const suffix = props.providerError.uri ? ` (${props.providerError.uri})` : ""; @@ -76,10 +72,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { if (!isCancelled) { setIsProviderError(false); setError( - t("idpErrorOidcMissingCode", { - defaultValue: - "The identity provider did not return an authorization code." - }) + "The identity provider did not return an authorization code." ); setLoading(false); } @@ -90,10 +83,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { if (!isCancelled) { setIsProviderError(false); setError( - t("idpErrorOidcMissingState", { - defaultValue: - "The login request is missing state information. Please restart the login process." - }) + "The login request is missing state information. Please restart the login process." ); setLoading(false); } @@ -159,12 +149,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { console.error(e); if (!isCancelled) { setIsProviderError(false); - setError( - t("idpErrorOidcTokenValidating", { - defaultValue: - "An unexpected error occurred. Please try again." - }) - ); + setError("An unexpected error occurred. Please try again."); } } finally { if (!isCancelled) { @@ -181,7 +166,7 @@ export default function ValidateOidcToken(props: ValidateOidcTokenParams) { }, []); return ( -
+
diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 08761eba..6ec5f0b7 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -11,7 +11,7 @@ const alertVariants = cva( default: "bg-card border text-foreground", neutral: "bg-card bg-muted border text-foreground", destructive: - "border-destructive/50 border bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive", + "border-destructive/50 border bg-destructive/8 text-destructive dark:border-destructive/50 [&>svg]:text-destructive", success: "border-green-500/50 border bg-green-500/10 text-green-500 dark:border-success [&>svg]:text-green-500", info: "border-blue-500/50 border bg-blue-500/10 text-blue-800 dark:text-blue-400 dark:border-blue-400 [&>svg]:text-blue-500", From 9ffa39141695d62c7efa8f59e7356b0e5c7e6ca8 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sat, 20 Dec 2025 12:00:58 -0500 Subject: [PATCH 146/153] improve clean redirects --- src/app/auth/login/page.tsx | 1 + src/components/private/IdpLoginButtons.tsx | 4 +- src/lib/cleanRedirect.ts | 96 ++++++++++++++++++---- 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx index 7ef77807..bd6327fd 100644 --- a/src/app/auth/login/page.tsx +++ b/src/app/auth/login/page.tsx @@ -66,6 +66,7 @@ export default async function Page(props: { let redirectUrl: string | undefined = undefined; if (searchParams.redirect) { redirectUrl = cleanRedirect(searchParams.redirect as string); + searchParams.redirect = redirectUrl; } let loginIdps: LoginFormIDP[] = []; diff --git a/src/components/private/IdpLoginButtons.tsx b/src/components/private/IdpLoginButtons.tsx index e3af2d06..b855683a 100644 --- a/src/components/private/IdpLoginButtons.tsx +++ b/src/components/private/IdpLoginButtons.tsx @@ -15,6 +15,7 @@ import { useSearchParams } from "next/navigation"; import { useRouter } from "next/navigation"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; export type LoginFormIDP = { idpId: number; @@ -63,9 +64,10 @@ export default function IdpLoginButtons({ redirect || "/", orgId ); + const safeRedirect = cleanRedirect(redirect || "/"); const response = await generateOidcUrlProxy( idpId, - redirect || "/", + safeRedirect, orgId ); diff --git a/src/lib/cleanRedirect.ts b/src/lib/cleanRedirect.ts index 048b3bdc..02a8dde1 100644 --- a/src/lib/cleanRedirect.ts +++ b/src/lib/cleanRedirect.ts @@ -1,22 +1,86 @@ -type PatternConfig = { - name: string; - regex: RegExp; +type CleanRedirectOptions = { + fallback?: string; + maxRedirectDepth?: number; }; -const patterns: PatternConfig[] = [ - { name: "Invite Token", regex: /^\/invite\?token=[a-zA-Z0-9-]+$/ }, - { name: "Setup", regex: /^\/setup$/ }, - { name: "Resource Auth Portal", regex: /^\/auth\/resource\/\d+$/ }, - { - name: "Device Login", - regex: /^\/auth\/login\/device(\?code=[a-zA-Z0-9-]+)?$/ - } -]; +const ALLOWED_QUERY_PARAMS = new Set([ + "forceLogin", + "code", + "token", + "redirect" +]); + +const DUMMY_BASE = "https://internal.local"; + +export function cleanRedirect( + input: string, + options: CleanRedirectOptions = {} +): string { + const { fallback = "/", maxRedirectDepth = 2 } = options; -export function cleanRedirect(input: string, fallback?: string): string { if (!input || typeof input !== "string") { - return "/"; + return fallback; + } + + try { + return sanitizeUrl(input, fallback, maxRedirectDepth); + } catch { + return fallback; } - const isAccepted = patterns.some((pattern) => pattern.regex.test(input)); - return isAccepted ? input : fallback || "/"; +} + +function sanitizeUrl( + input: string, + fallback: string, + remainingRedirectDepth: number +): string { + if ( + input.startsWith("javascript:") || + input.startsWith("data:") || + input.startsWith("//") + ) { + return fallback; + } + + const url = new URL(input, DUMMY_BASE); + + // Must be a relative/internal path + if (url.origin !== DUMMY_BASE) { + return fallback; + } + + if (!url.pathname.startsWith("/")) { + return fallback; + } + + const cleanParams = new URLSearchParams(); + + for (const [key, value] of url.searchParams.entries()) { + if (!ALLOWED_QUERY_PARAMS.has(key)) { + continue; + } + + if (key === "redirect") { + if (remainingRedirectDepth <= 0) { + continue; + } + + const cleanedRedirect = sanitizeUrl( + value, + "", + remainingRedirectDepth - 1 + ); + + if (cleanedRedirect) { + cleanParams.set("redirect", cleanedRedirect); + } + + continue; + } + + cleanParams.set(key, value); + } + + const queryString = cleanParams.toString(); + return queryString ? `${url.pathname}?${queryString}` : url.pathname; } From 3d8153aeb1c7b7a4664d4e2eff2e7f3ab4384589 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 01:22:22 +0000 Subject: [PATCH 147/153] Bump the prod-minor-updates group across 1 directory with 7 updates Bumps the prod-minor-updates group with 7 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.948.0` | `3.955.0` | | [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `16.0.8` | `16.1.0` | | [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `0.559.0` | `0.562.0` | | [next-intl](https://github.com/amannn/next-intl) | `4.5.8` | `4.6.1` | | [react-day-picker](https://github.com/gpbl/react-day-picker) | `9.12.0` | `9.13.0` | | [stripe](https://github.com/stripe/stripe-node) | `20.0.0` | `20.1.0` | | [zod](https://github.com/colinhacks/zod) | `4.1.13` | `4.2.1` | Updates `@aws-sdk/client-s3` from 3.948.0 to 3.955.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.955.0/clients/client-s3) Updates `eslint-config-next` from 16.0.8 to 16.1.0 - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/commits/v16.1.0/packages/eslint-config-next) Updates `lucide-react` from 0.559.0 to 0.562.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/0.562.0/packages/lucide-react) Updates `next-intl` from 4.5.8 to 4.6.1 - [Release notes](https://github.com/amannn/next-intl/releases) - [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md) - [Commits](https://github.com/amannn/next-intl/compare/v4.5.8...v4.6.1) Updates `react-day-picker` from 9.12.0 to 9.13.0 - [Release notes](https://github.com/gpbl/react-day-picker/releases) - [Changelog](https://github.com/gpbl/react-day-picker/blob/main/CHANGELOG.md) - [Commits](https://github.com/gpbl/react-day-picker/compare/v9.12.0...v9.13.0) Updates `stripe` from 20.0.0 to 20.1.0 - [Release notes](https://github.com/stripe/stripe-node/releases) - [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md) - [Commits](https://github.com/stripe/stripe-node/compare/v20.0.0...v20.1.0) Updates `zod` from 4.1.13 to 4.2.1 - [Release notes](https://github.com/colinhacks/zod/releases) - [Commits](https://github.com/colinhacks/zod/compare/v4.1.13...v4.2.1) --- updated-dependencies: - dependency-name: "@aws-sdk/client-s3" dependency-version: 3.955.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: eslint-config-next dependency-version: 16.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: lucide-react dependency-version: 0.562.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: next-intl dependency-version: 4.6.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: react-day-picker dependency-version: 9.13.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: stripe dependency-version: 20.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: zod dependency-version: 4.2.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates ... Signed-off-by: dependabot[bot] --- package-lock.json | 1664 +++++++++++++++++++++++++++++---------------- package.json | 14 +- 2 files changed, 1101 insertions(+), 577 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3a18c31..5206a52b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "SEE LICENSE IN LICENSE AND README.md", "dependencies": { "@asteasolutions/zod-to-openapi": "8.2.0", - "@aws-sdk/client-s3": "3.948.0", + "@aws-sdk/client-s3": "3.955.0", "@faker-js/faker": "10.1.0", "@headlessui/react": "2.2.9", "@hookform/resolvers": "5.2.2", @@ -60,7 +60,7 @@ "date-fns": "4.1.0", "drizzle-orm": "0.45.0", "eslint": "9.39.1", - "eslint-config-next": "16.0.8", + "eslint-config-next": "16.1.0", "express": "5.2.1", "express-rate-limit": "8.2.1", "glob": "13.0.0", @@ -72,11 +72,11 @@ "jmespath": "0.16.0", "js-yaml": "4.1.1", "jsonwebtoken": "9.0.3", - "lucide-react": "0.559.0", + "lucide-react": "0.562.0", "maxmind": "5.0.1", "moment": "2.30.1", "next": "15.5.9", - "next-intl": "4.5.8", + "next-intl": "4.6.1", "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", @@ -89,7 +89,7 @@ "posthog-node": "5.17.2", "qrcode.react": "4.2.0", "react": "19.2.3", - "react-day-picker": "9.12.0", + "react-day-picker": "9.13.0", "react-dom": "19.2.3", "react-easy-sort": "1.8.0", "react-hook-form": "7.68.0", @@ -99,7 +99,7 @@ "reodotdev": "1.0.0", "resend": "6.6.0", "semver": "7.7.3", - "stripe": "20.0.0", + "stripe": "20.1.0", "swagger-ui-express": "5.0.1", "tailwind-merge": "3.4.0", "topojson-client": "3.1.0", @@ -112,7 +112,7 @@ "ws": "8.18.3", "yaml": "2.8.2", "yargs": "18.0.0", - "zod": "4.1.13", + "zod": "4.2.1", "zod-validation-error": "5.0.0" }, "devDependencies": { @@ -396,65 +396,65 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.948.0.tgz", - "integrity": "sha512-uvEjds8aYA9SzhBS8RKDtsDUhNV9VhqKiHTcmvhM7gJO92q0WTn8/QeFTdNyLc6RxpiDyz+uBxS7PcdNiZzqfA==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.955.0.tgz", + "integrity": "sha512-bFvSM6UB0R5hpWfXzHI3BlKwT2qYHto9JoDtzSr5FxVguTMzJyr+an11VT1Hi5wgO03luXEeXeloURFvaMs6TQ==", "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.947.0", - "@aws-sdk/credential-provider-node": "3.948.0", - "@aws-sdk/middleware-bucket-endpoint": "3.936.0", - "@aws-sdk/middleware-expect-continue": "3.936.0", - "@aws-sdk/middleware-flexible-checksums": "3.947.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-location-constraint": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-sdk-s3": "3.947.0", - "@aws-sdk/middleware-ssec": "3.936.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/signature-v4-multi-region": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/eventstream-serde-browser": "^4.2.5", - "@smithy/eventstream-serde-config-resolver": "^4.3.5", - "@smithy/eventstream-serde-node": "^4.2.5", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-blob-browser": "^4.2.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/hash-stream-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/md5-js": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/credential-provider-node": "3.955.0", + "@aws-sdk/middleware-bucket-endpoint": "3.953.0", + "@aws-sdk/middleware-expect-continue": "3.953.0", + "@aws-sdk/middleware-flexible-checksums": "3.954.0", + "@aws-sdk/middleware-host-header": "3.953.0", + "@aws-sdk/middleware-location-constraint": "3.953.0", + "@aws-sdk/middleware-logger": "3.953.0", + "@aws-sdk/middleware-recursion-detection": "3.953.0", + "@aws-sdk/middleware-sdk-s3": "3.954.0", + "@aws-sdk/middleware-ssec": "3.953.0", + "@aws-sdk/middleware-user-agent": "3.954.0", + "@aws-sdk/region-config-resolver": "3.953.0", + "@aws-sdk/signature-v4-multi-region": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@aws-sdk/util-user-agent-browser": "3.953.0", + "@aws-sdk/util-user-agent-node": "3.954.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/core": "^3.19.0", + "@smithy/eventstream-serde-browser": "^4.2.6", + "@smithy/eventstream-serde-config-resolver": "^4.3.6", + "@smithy/eventstream-serde-node": "^4.2.6", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/hash-blob-browser": "^4.2.7", + "@smithy/hash-node": "^4.2.6", + "@smithy/hash-stream-node": "^4.2.6", + "@smithy/invalid-dependency": "^4.2.6", + "@smithy/md5-js": "^4.2.6", + "@smithy/middleware-content-length": "^4.2.6", + "@smithy/middleware-endpoint": "^4.4.0", + "@smithy/middleware-retry": "^4.4.16", + "@smithy/middleware-serde": "^4.2.7", + "@smithy/middleware-stack": "^4.2.6", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/util-stream": "^4.5.6", + "@smithy/util-defaults-mode-browser": "^4.3.15", + "@smithy/util-defaults-mode-node": "^4.2.18", + "@smithy/util-endpoints": "^3.2.6", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-retry": "^4.2.6", + "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", - "@smithy/util-waiter": "^4.2.5", + "@smithy/util-waiter": "^4.2.6", "tslib": "^2.6.2" }, "engines": { @@ -462,47 +462,47 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.948.0.tgz", - "integrity": "sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.955.0.tgz", + "integrity": "sha512-+nym5boDFt2ksba0fElocMKxCFJbJcd31PI3502hoI1N5VK7HyxkQeBtQJ64JYomvw8eARjWWC13hkB0LtZILw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/middleware-host-header": "3.953.0", + "@aws-sdk/middleware-logger": "3.953.0", + "@aws-sdk/middleware-recursion-detection": "3.953.0", + "@aws-sdk/middleware-user-agent": "3.954.0", + "@aws-sdk/region-config-resolver": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@aws-sdk/util-user-agent-browser": "3.953.0", + "@aws-sdk/util-user-agent-node": "3.954.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/core": "^3.19.0", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/hash-node": "^4.2.6", + "@smithy/invalid-dependency": "^4.2.6", + "@smithy/middleware-content-length": "^4.2.6", + "@smithy/middleware-endpoint": "^4.4.0", + "@smithy/middleware-retry": "^4.4.16", + "@smithy/middleware-serde": "^4.2.7", + "@smithy/middleware-stack": "^4.2.6", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/util-defaults-mode-browser": "^4.3.15", + "@smithy/util-defaults-mode-node": "^4.2.18", + "@smithy/util-endpoints": "^3.2.6", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-retry": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -511,22 +511,22 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.947.0.tgz", - "integrity": "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.954.0.tgz", + "integrity": "sha512-5oYO5RP+mvCNXNj8XnF9jZo0EP0LTseYOJVNQYcii1D9DJqzHL3HJWurYh7cXxz7G7eDyvVYA01O9Xpt34TdoA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@aws-sdk/xml-builder": "3.930.0", - "@smithy/core": "^3.18.7", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/xml-builder": "3.953.0", + "@smithy/core": "^3.19.0", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/signature-v4": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-middleware": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -535,15 +535,15 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.947.0.tgz", - "integrity": "sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.954.0.tgz", + "integrity": "sha512-2HNkqBjfsvyoRuPAiFh86JBFMFyaCNhL4VyH6XqwTGKZffjG7hdBmzXPy7AT7G3oFh1k/1Zc27v0qxaKoK7mBA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -551,20 +551,20 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.947.0.tgz", - "integrity": "sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.954.0.tgz", + "integrity": "sha512-CrWD5300+NE1OYRnSVDxoG7G0b5cLIZb7yp+rNQ5Jq/kqnTmyJXpVAsivq+bQIDaGzPXhadzpAMIoo7K/aHaag==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/util-stream": "^4.5.6", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", + "@smithy/util-stream": "^4.5.7", "tslib": "^2.6.2" }, "engines": { @@ -572,24 +572,24 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.948.0.tgz", - "integrity": "sha512-Cl//Qh88e8HBL7yYkJNpF5eq76IO6rq8GsatKcfVBm7RFVxCqYEPSSBtkHdbtNwQdRQqAMXc6E/lEB/CZUDxnA==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.955.0.tgz", + "integrity": "sha512-90isLovxsPzaaSx3IIUZuxym6VXrsRetnQ3AuHr2kiTFk2pIzyIwmi+gDcUaLXQ5nNBoSj1Z/4+i1vhxa1n2DQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/credential-provider-env": "3.947.0", - "@aws-sdk/credential-provider-http": "3.947.0", - "@aws-sdk/credential-provider-login": "3.948.0", - "@aws-sdk/credential-provider-process": "3.947.0", - "@aws-sdk/credential-provider-sso": "3.948.0", - "@aws-sdk/credential-provider-web-identity": "3.948.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/credential-provider-env": "3.954.0", + "@aws-sdk/credential-provider-http": "3.954.0", + "@aws-sdk/credential-provider-login": "3.955.0", + "@aws-sdk/credential-provider-process": "3.954.0", + "@aws-sdk/credential-provider-sso": "3.955.0", + "@aws-sdk/credential-provider-web-identity": "3.955.0", + "@aws-sdk/nested-clients": "3.955.0", + "@aws-sdk/types": "3.953.0", + "@smithy/credential-provider-imds": "^4.2.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -597,18 +597,18 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-login": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.948.0.tgz", - "integrity": "sha512-gcKO2b6eeTuZGp3Vvgr/9OxajMrD3W+FZ2FCyJox363ZgMoYJsyNid1vuZrEuAGkx0jvveLXfwiVS0UXyPkgtw==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.955.0.tgz", + "integrity": "sha512-xlkmSvg8oDN5LIxLAq3N1QWK8F8gUAsBWZlp1IX8Lr5XhcKI3GVarIIUcZrvCy1NjzCd/LDXYdNL6MRlNP4bAw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/nested-clients": "3.955.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -616,22 +616,22 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.948.0.tgz", - "integrity": "sha512-ep5vRLnrRdcsP17Ef31sNN4g8Nqk/4JBydcUJuFRbGuyQtrZZrVT81UeH2xhz6d0BK6ejafDB9+ZpBjXuWT5/Q==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.955.0.tgz", + "integrity": "sha512-XIL4QB+dPOJA6DRTmYZL52wFcLTslb7V1ydS4FCNT2DVLhkO4ExkPP+pe5YmIpzt/Our1ugS+XxAs3e6BtyFjA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.947.0", - "@aws-sdk/credential-provider-http": "3.947.0", - "@aws-sdk/credential-provider-ini": "3.948.0", - "@aws-sdk/credential-provider-process": "3.947.0", - "@aws-sdk/credential-provider-sso": "3.948.0", - "@aws-sdk/credential-provider-web-identity": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/credential-provider-env": "3.954.0", + "@aws-sdk/credential-provider-http": "3.954.0", + "@aws-sdk/credential-provider-ini": "3.955.0", + "@aws-sdk/credential-provider-process": "3.954.0", + "@aws-sdk/credential-provider-sso": "3.955.0", + "@aws-sdk/credential-provider-web-identity": "3.955.0", + "@aws-sdk/types": "3.953.0", + "@smithy/credential-provider-imds": "^4.2.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -639,16 +639,16 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.947.0.tgz", - "integrity": "sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.954.0.tgz", + "integrity": "sha512-Y1/0O2LgbKM8iIgcVj/GNEQW6p90LVTCOzF2CI1pouoKqxmZ/1F7F66WHoa6XUOfKaCRj/R6nuMR3om9ThaM5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -656,18 +656,18 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.948.0.tgz", - "integrity": "sha512-gqLhX1L+zb/ZDnnYbILQqJ46j735StfWV5PbDjxRzBKS7GzsiYoaf6MyHseEopmWrez5zl5l6aWzig7UpzSeQQ==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.955.0.tgz", + "integrity": "sha512-Y99KI73Fn8JnB4RY5Ls6j7rd5jmFFwnY9WLHIWeJdc+vfwL6Bb1uWKW3+m/B9+RC4Xoz2nQgtefBcdWq5Xx8iw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.948.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/token-providers": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/client-sso": "3.955.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/token-providers": "3.955.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -675,17 +675,46 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.948.0.tgz", - "integrity": "sha512-MvYQlXVoJyfF3/SmnNzOVEtANRAiJIObEUYYyjTqKZTmcRIVVky0tPuG26XnB8LmTYgtESwJIZJj/Eyyc9WURQ==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.955.0.tgz", + "integrity": "sha512-+lFxkZ2Vz3qp/T68ZONKzWVTQvomTu7E6tts1dfAbEcDt62Y/nPCByq/C2hQj+TiN05HrUx+yTJaGHBklhkbqA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/nested-clients": "3.955.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.953.0.tgz", + "integrity": "sha512-jTGhfkONav+r4E6HLOrl5SzBqDmPByUYCkyB/c/3TVb8jX3wAZx8/q9bphKpCh+G5ARi3IdbSisgkZrJYqQ19Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.953.0", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.953.0.tgz", + "integrity": "sha512-PlWdVYgcuptkIC0ZKqVUhWNtSHXJSx7U9V8J7dJjRmsXC40X7zpEycvrkzDMJjeTDGcCceYbyYAg/4X1lkcIMw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.953.0", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -693,15 +722,15 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.948.0.tgz", - "integrity": "sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.953.0.tgz", + "integrity": "sha512-cmIJx0gWeesUKK4YwgE+VQL3mpACr3/J24fbwnc1Z5tntC86b+HQFzU5vsBDw6lLwyD46dBgWdsXFh1jL+ZaFw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", + "@aws-sdk/types": "3.953.0", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -709,23 +738,23 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.947.0.tgz", - "integrity": "sha512-DS2tm5YBKhPW2PthrRBDr6eufChbwXe0NjtTZcYDfUCXf0OR+W6cIqyKguwHMJ+IyYdey30AfVw9/Lb5KB8U8A==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.954.0.tgz", + "integrity": "sha512-274CNmnRjknmfFb2o0Azxic54fnujaA8AYSeRUOho3lN48TVzx85eAFWj2kLgvUJO88pE3jBDPWboKQiQdXeUQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-arn-parser": "3.893.0", - "@smithy/core": "^3.18.7", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-arn-parser": "3.953.0", + "@smithy/core": "^3.19.0", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/signature-v4": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-stream": "^4.5.6", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -734,17 +763,17 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.947.0.tgz", - "integrity": "sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.954.0.tgz", + "integrity": "sha512-5PX8JDe3dB2+MqXeGIhmgFnm2rbVsSxhz+Xyuu1oxLtbOn+a9UDA+sNBufEBjt3UxWy5qwEEY1fxdbXXayjlGg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@smithy/core": "^3.18.7", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@smithy/core": "^3.19.0", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -752,47 +781,47 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/nested-clients": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.948.0.tgz", - "integrity": "sha512-zcbJfBsB6h254o3NuoEkf0+UY1GpE9ioiQdENWv7odo69s8iaGBEQ4BDpsIMqcuiiUXw1uKIVNxCB1gUGYz8lw==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.955.0.tgz", + "integrity": "sha512-RBi6CQHbPF09kqXAoiEOOPkVnSoU5YppKoOt/cgsWfoMHwC+7itIrEv+yRD62h14jIjF3KngVIQIrBRbX3o3/Q==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/middleware-host-header": "3.953.0", + "@aws-sdk/middleware-logger": "3.953.0", + "@aws-sdk/middleware-recursion-detection": "3.953.0", + "@aws-sdk/middleware-user-agent": "3.954.0", + "@aws-sdk/region-config-resolver": "3.953.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-endpoints": "3.953.0", + "@aws-sdk/util-user-agent-browser": "3.953.0", + "@aws-sdk/util-user-agent-node": "3.954.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/core": "^3.19.0", + "@smithy/fetch-http-handler": "^5.3.7", + "@smithy/hash-node": "^4.2.6", + "@smithy/invalid-dependency": "^4.2.6", + "@smithy/middleware-content-length": "^4.2.6", + "@smithy/middleware-endpoint": "^4.4.0", + "@smithy/middleware-retry": "^4.4.16", + "@smithy/middleware-serde": "^4.2.7", + "@smithy/middleware-stack": "^4.2.6", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/node-http-handler": "^4.4.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/util-defaults-mode-browser": "^4.3.15", + "@smithy/util-defaults-mode-node": "^4.2.18", + "@smithy/util-endpoints": "^3.2.6", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-retry": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -800,17 +829,33 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.947.0.tgz", - "integrity": "sha512-UaYmzoxf9q3mabIA2hc4T6x5YSFUG2BpNjAZ207EA1bnQMiK+d6vZvb83t7dIWL/U1de1sGV19c1C81Jf14rrA==", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.953.0.tgz", + "integrity": "sha512-5MJgnsc+HLO+le0EK1cy92yrC7kyhGZSpaq8PcQvKs9qtXCXT5Tb6tMdkr5Y07JxYsYOV1omWBynvL6PWh08tQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@smithy/config-resolver": "^4.4.4", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.954.0.tgz", + "integrity": "sha512-GJJbUaSlGrMSRWui3Oz8ByygpQlzDGm195yTKirgGyu4tfYrFr/QWrWT42EUktY/L4Irev1pdHTuLS+AGHO1gw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@smithy/protocol-http": "^5.3.6", + "@smithy/signature-v4": "^5.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -818,33 +863,86 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.948.0.tgz", - "integrity": "sha512-V487/kM4Teq5dcr1t5K6eoUKuqlGr9FRWL3MIMukMERJXHZvio6kox60FZ/YtciRHRI75u14YUqm2Dzddcu3+A==", + "version": "3.955.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.955.0.tgz", + "integrity": "sha512-LVpWkxXvMPgZofP2Gc8XBfQhsyecBMVARDHWMvks6vPbCLSTM7dw6H1HI9qbGNCurYcyc2xBRAkEDhChQlbPPg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/nested-clients": "3.955.0", + "@aws-sdk/types": "3.953.0", + "@smithy/property-provider": "^4.2.6", + "@smithy/shared-ini-file-loader": "^4.4.1", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.947.0.tgz", - "integrity": "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==", + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/types": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.953.0.tgz", + "integrity": "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.953.0.tgz", + "integrity": "sha512-9hqdKkn4OvYzzaLryq2xnwcrPc8ziY34i9szUdgBfSqEC6pBxbY9/lLXmrgzfwMSL2Z7/v2go4Od0p5eukKLMQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.953.0.tgz", + "integrity": "sha512-rjaS6jrFksopXvNg6YeN+D1lYwhcByORNlFuYesFvaQNtPOufbE5tJL4GJ3TMXyaY0uFR28N5BHHITPyWWfH/g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.953.0", + "@smithy/types": "^4.10.0", + "@smithy/url-parser": "^4.2.6", + "@smithy/util-endpoints": "^3.2.6", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.953.0.tgz", + "integrity": "sha512-UF5NeqYesWuFao+u7LJvpV1SJCaLml5BtFZKUdTnNNMeN6jvV+dW/eQoFGpXF94RCqguX0XESmRuRRPQp+/rzQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.953.0", + "@smithy/types": "^4.10.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.954.0.tgz", + "integrity": "sha512-fB5S5VOu7OFkeNzcblQlez4AjO5hgDFaa7phYt7716YWisY3RjAaQPlxgv+G3GltHHDJIfzEC5aRxdf62B9zMg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.954.0", + "@aws-sdk/types": "3.953.0", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -859,6 +957,20 @@ } } }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/xml-builder": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.953.0.tgz", + "integrity": "sha512-Zmrj21jQ2OeOJGr9spPiN00aQvXa/WUqRXcTVENhrMt+OFoSOfDFpYhUj9NQ09QmQ8KMWFoWuWW6iKurNqLvAA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.10.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-sesv2": { "version": "3.946.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.946.0.tgz", @@ -1153,16 +1265,16 @@ } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.936.0.tgz", - "integrity": "sha512-XLSVVfAorUxZh6dzF+HTOp4R1B5EQcdpGcPliWr0KUj2jukgjZEcqbBmjyMF/p9bmyQsONX80iURF1HLAlW0qg==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.953.0.tgz", + "integrity": "sha512-YHVRIOowtGIl/L2WuS83FgRlm31tU0aL1yryWaFtF+AFjA5BIeiFkxIZqaRGxJpJvFEBdohsyq6Ipv5mgWfezg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-arn-parser": "3.893.0", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/util-arn-parser": "3.953.0", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" }, @@ -1170,15 +1282,53 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.936.0.tgz", - "integrity": "sha512-Eb4ELAC23bEQLJmUMYnPWcjD3FZIsmz2svDiXEcxRkQU9r7NRID7pM7C5NPH94wOfiCk0b2Y8rVyFXW0lGQwbA==", + "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/types": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.953.0.tgz", + "integrity": "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/util-arn-parser": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.953.0.tgz", + "integrity": "sha512-9hqdKkn4OvYzzaLryq2xnwcrPc8ziY34i9szUdgBfSqEC6pBxbY9/lLXmrgzfwMSL2Z7/v2go4Od0p5eukKLMQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.953.0.tgz", + "integrity": "sha512-BQTVXrypQ0rbb7au/Hk4IS5GaJZlwk6O44Rjk6Kxb0IvGQhSurNTuesFiJx1sLbf+w+T31saPtODcfQQERqhCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.953.0", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-expect-continue/node_modules/@aws-sdk/types": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.953.0.tgz", + "integrity": "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -1186,22 +1336,22 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.947.0.tgz", - "integrity": "sha512-kXXxS2raNESNO+zR0L4YInVjhcGGNI2Mx0AE1ThRhDkAt2se3a+rGf9equ9YvOqA1m8Jl/GSI8cXYvSxXmS9Ag==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.954.0.tgz", + "integrity": "sha512-hHOPDJyxucNodkgapLhA0VdwDBwVYN9DX20aA6j+3nwutAlZ5skaV7Bw0W3YC7Fh/ieDKKhcSZulONd4lVTwMg==", "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.947.0", - "@aws-sdk/types": "3.936.0", + "@aws-sdk/core": "3.954.0", + "@aws-sdk/types": "3.953.0", "@smithy/is-array-buffer": "^4.2.0", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-stream": "^4.5.6", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/types": "^4.10.0", + "@smithy/util-middleware": "^4.2.6", + "@smithy/util-stream": "^4.5.7", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -1210,22 +1360,22 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/core": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.947.0.tgz", - "integrity": "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==", + "version": "3.954.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.954.0.tgz", + "integrity": "sha512-5oYO5RP+mvCNXNj8XnF9jZo0EP0LTseYOJVNQYcii1D9DJqzHL3HJWurYh7cXxz7G7eDyvVYA01O9Xpt34TdoA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@aws-sdk/xml-builder": "3.930.0", - "@smithy/core": "^3.18.7", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@aws-sdk/xml-builder": "3.953.0", + "@smithy/core": "^3.19.0", + "@smithy/node-config-provider": "^4.3.6", + "@smithy/property-provider": "^4.2.6", + "@smithy/protocol-http": "^5.3.6", + "@smithy/signature-v4": "^5.3.6", + "@smithy/smithy-client": "^4.10.1", + "@smithy/types": "^4.10.0", "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-middleware": "^4.2.6", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -1233,10 +1383,38 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/types": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.953.0.tgz", + "integrity": "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/xml-builder": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.953.0.tgz", + "integrity": "sha512-Zmrj21jQ2OeOJGr9spPiN00aQvXa/WUqRXcTVENhrMt+OFoSOfDFpYhUj9NQ09QmQ8KMWFoWuWW6iKurNqLvAA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.10.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/middleware-host-header": { "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.936.0.tgz", "integrity": "sha512-tAaObaAnsP1XnLGndfkGWFuzrJYuk9W0b/nLvol66t8FZExIAf/WdkT2NNAWOYxljVs++oHnyHBCxIlaHrzSiw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1249,13 +1427,26 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.936.0.tgz", - "integrity": "sha512-SCMPenDtQMd9o5da9JzkHz838w3327iqXk3cbNnXWqnNRx6unyW8FL0DZ84gIY12kAyVHz5WEqlWuekc15ehfw==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.953.0.tgz", + "integrity": "sha512-h0urrbteIQEybyIISaJfQLZ/+/lJPRzPWAQT4epvzfgv/4MKZI7K83dK7SfTwAooVKFBHiCMok2Cf0iHDt07Kw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint/node_modules/@aws-sdk/types": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.953.0.tgz", + "integrity": "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -1266,6 +1457,7 @@ "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.936.0.tgz", "integrity": "sha512-aPSJ12d3a3Ea5nyEnLbijCaaYJT2QjQ9iW+zGh5QcZYXmOGWbKVyPSxmVOboZQG+c1M8t6d2O7tqrwzIq8L8qw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1320,13 +1512,26 @@ } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.936.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.936.0.tgz", - "integrity": "sha512-/GLC9lZdVp05ozRik5KsuODR/N7j+W+2TbfdFL3iS+7un+gnP6hC8RDOZd6WhpZp7drXQ9guKiTAxkZQwzS8DA==", + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.953.0.tgz", + "integrity": "sha512-OrhG1kcQ9zZh3NS3RovR028N0+UndQ957zF1k5HPLeFLwFwQN1uPOufzzPzAyXIIKtR69ARFsQI4mstZS4DMvw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.936.0", - "@smithy/types": "^4.9.0", + "@aws-sdk/types": "3.953.0", + "@smithy/types": "^4.10.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec/node_modules/@aws-sdk/types": { + "version": "3.953.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.953.0.tgz", + "integrity": "sha512-M9Iwg9kTyqTErI0vOTVVpcnTHWzS3VplQppy8MuL02EE+mJ0BIwpWfsaAPQW+/XnVpdNpWZTsHcNE29f1+hR8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.10.0", "tslib": "^2.6.2" }, "engines": { @@ -1406,6 +1611,7 @@ "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.936.0.tgz", "integrity": "sha512-wOKhzzWsshXGduxO4pqSiNyL9oUtk4BEvjWm9aaq6Hmfdoydq6v6t0rAGHWPjFwy9z2haovGRi3C8IxdMB4muw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1472,6 +1678,7 @@ "version": "3.893.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -1484,6 +1691,7 @@ "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.936.0.tgz", "integrity": "sha512-0Zx3Ntdpu+z9Wlm7JKUBOzS9EunwKAb4KdGUQQxDqh5Lc3ta5uBoub+FgmVuzwnmBu9U1Os8UuwVTH0Lgu+P5w==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1512,6 +1720,7 @@ "version": "3.936.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.936.0.tgz", "integrity": "sha512-eZ/XF6NxMtu+iCma58GRNRxSq4lHo6zHQLOZRIeL/ghqYJirqHdenMOwrzPettj60KWlv827RVebP9oNVrwZbw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.936.0", @@ -1549,6 +1758,7 @@ "version": "3.930.0", "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.930.0.tgz", "integrity": "sha512-YIfkD17GocxdmlUVc3ia52QhcWuRIUJonbF8A2CYfcWNV3HzvAqpcPeC0bYUhkK+8e8YO1ARnLKZQE0TlwzorA==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.9.0", @@ -3841,9 +4051,9 @@ "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.0.8.tgz", - "integrity": "sha512-1miV0qXDcLUaOdHridVPCh4i39ElRIAraseVIbb3BEqyZ5ol9sPyjTP/GNTPV5rBxqxjF6/vv5zQTVbhiNaLqA==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.0.tgz", + "integrity": "sha512-sooC/k0LCF4/jLXYHpgfzJot04lZQqsttn8XJpTguP8N3GhqXN3wSkh68no2OcZzS/qeGwKDFTqhZ8WofdXmmQ==", "license": "MIT", "dependencies": { "fast-glob": "3.3.1" @@ -4645,6 +4855,313 @@ "integrity": "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==", "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==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "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" + } + }, + "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==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "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", @@ -7462,12 +7979,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.5.tgz", - "integrity": "sha512-j7HwVkBw68YW8UmFRcjZOmssE77Rvk0GWAIN1oFBhsaovQmZWYCIcGa9/pwRB0ExI8Sk9MWNALTjftjHZea7VA==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.7.tgz", + "integrity": "sha512-rzMY6CaKx2qxrbYbqjXWS0plqEy7LOdKHS0bg4ixJ6aoGDPNUcLWk/FRNuCILh7GKLG9TFUXYYeQQldMBBwuyw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7500,16 +8017,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.3.tgz", - "integrity": "sha512-ezHLe1tKLUxDJo2LHtDuEDyWXolw8WGOR92qb4bQdWq/zKenO5BvctZGrVJBK08zjezSk7bmbKFOXIVyChvDLw==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.5.tgz", + "integrity": "sha512-HAGoUAFYsUkoSckuKbCPayECeMim8pOu+yLy1zOxt1sifzEbrsRpYa+mKcMdiHKMeiqOibyPG0sFJnmaV/OGEg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-endpoints": "^3.2.7", + "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" }, "engines": { @@ -7517,18 +8034,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.18.7", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.18.7.tgz", - "integrity": "sha512-axG9MvKhMWOhFbvf5y2DuyTxQueO0dkedY9QC3mAfndLosRI/9LJv8WaL0mw7ubNhsO4IuXX9/9dYGPFvHrqlw==", + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.20.0.tgz", + "integrity": "sha512-WsSHCPq/neD5G/MkK4csLI5Y5Pkd9c1NMfpYEKeghSGaD4Ja1qLIohRQf2D5c1Uy5aXp76DeKHkzWZ9KAlHroQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.6", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-stream": "^4.5.6", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-stream": "^4.5.8", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" @@ -7538,15 +8055,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.5.tgz", - "integrity": "sha512-BZwotjoZWn9+36nimwm/OLIcVe+KYRwzMjfhd4QT7QxPm9WY0HiOV8t/Wlh+HVUif0SBVV7ksq8//hPaBC/okQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.7.tgz", + "integrity": "sha512-CmduWdCiILCRNbQWFR0OcZlUPVtyE49Sr8yYL0rZQ4D/wKxiNzBNS/YHemvnbkIWj623fplgkexUd/c9CAKdoA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", "tslib": "^2.6.2" }, "engines": { @@ -7554,13 +8071,13 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.5.tgz", - "integrity": "sha512-Ogt4Zi9hEbIP17oQMd68qYOHUzmH47UkK7q7Gl55iIm9oKt27MUGrC5JfpMroeHjdkOliOA4Qt3NQ1xMq/nrlA==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.7.tgz", + "integrity": "sha512-DrpkEoM3j9cBBWhufqBwnbbn+3nf1N9FP6xuVJ+e220jbactKuQgaZwjwP5CP1t+O94brm2JgVMD2atMGX3xIQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" }, @@ -7569,13 +8086,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.5.tgz", - "integrity": "sha512-HohfmCQZjppVnKX2PnXlf47CW3j92Ki6T/vkAT2DhBR47e89pen3s4fIa7otGTtrVxmj7q+IhH0RnC5kpR8wtw==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.7.tgz", + "integrity": "sha512-ujzPk8seYoDBmABDE5YqlhQZAXLOrtxtJLrbhHMKjBoG5b4dK4i6/mEU+6/7yXIAkqOO8sJ6YxZl+h0QQ1IJ7g==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/eventstream-serde-universal": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7583,12 +8100,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.5.tgz", - "integrity": "sha512-ibjQjM7wEXtECiT6my1xfiMH9IcEczMOS6xiCQXoUIYSj5b1CpBbJ3VYbdwDy8Vcg5JHN7eFpOCGk8nyZAltNQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.7.tgz", + "integrity": "sha512-x7BtAiIPSaNaWuzm24Q/mtSkv+BrISO/fmheiJ39PKRNH3RmH2Hph/bUKSOBOBC9unqfIYDhKTHwpyZycLGPVQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7596,13 +8113,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.5.tgz", - "integrity": "sha512-+elOuaYx6F2H6x1/5BQP5ugv12nfJl66GhxON8+dWVUEDJ9jah/A0tayVdkLRP0AeSac0inYkDz5qBFKfVp2Gg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.7.tgz", + "integrity": "sha512-roySCtHC5+pQq5lK4be1fZ/WR6s/AxnPaLfCODIPArtN2du8s5Ot4mKVK3pPtijL/L654ws592JHJ1PbZFF6+A==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/eventstream-serde-universal": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7610,13 +8127,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.5.tgz", - "integrity": "sha512-G9WSqbST45bmIFaeNuP/EnC19Rhp54CcVdX9PDL1zyEB514WsDVXhlyihKlGXnRycmHNmVv88Bvvt4EYxWef/Q==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.7.tgz", + "integrity": "sha512-QVD+g3+icFkThoy4r8wVFZMsIP08taHVKjE6Jpmz8h5CgX/kk6pTODq5cht0OMtcapUx+xrPzUTQdA+TmO0m1g==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/eventstream-codec": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7624,14 +8141,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.6", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.6.tgz", - "integrity": "sha512-3+RG3EA6BBJ/ofZUeTFJA7mHfSYrZtQIrDP9dI8Lf7X6Jbos2jptuLrAAteDiFVrmbEmLSuRG/bUKzfAXk7dhg==", + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.8.tgz", + "integrity": "sha512-h/Fi+o7mti4n8wx1SR6UHWLaakwHRx29sizvp8OOm7iqwKGFneT06GCSFhml6Bha5BT6ot5pj3CYZnCHhGC2Rg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/querystring-builder": "^4.2.7", + "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, @@ -7640,14 +8157,14 @@ } }, "node_modules/@smithy/hash-blob-browser": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.6.tgz", - "integrity": "sha512-8P//tA8DVPk+3XURk2rwcKgYwFvwGwmJH/wJqQiSKwXZtf/LiZK+hbUZmPj/9KzM+OVSwe4o85KTp5x9DUZTjw==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.8.tgz", + "integrity": "sha512-07InZontqsM1ggTCPSRgI7d8DirqRrnpL7nIACT4PW0AWrgDiHhjGZzbAE5UtRSiU0NISGUYe7/rri9ZeWyDpw==", "license": "Apache-2.0", "dependencies": { "@smithy/chunked-blob-reader": "^5.2.0", "@smithy/chunked-blob-reader-native": "^4.2.1", - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7655,12 +8172,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.5.tgz", - "integrity": "sha512-DpYX914YOfA3UDT9CN1BM787PcHfWRBB43fFGCYrZFUH0Jv+5t8yYl+Pd5PW4+QzoGEDvn5d5QIO4j2HyYZQSA==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.7.tgz", + "integrity": "sha512-PU/JWLTBCV1c8FtB8tEFnY4eV1tSfBc7bDBADHfn1K+uRbPgSJ9jnJp0hyjiFN2PMdPzxsf1Fdu0eo9fJ760Xw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -7670,12 +8187,12 @@ } }, "node_modules/@smithy/hash-stream-node": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.5.tgz", - "integrity": "sha512-6+do24VnEyvWcGdHXomlpd0m8bfZePpUKBy7m311n+JuRwug8J4dCanJdTymx//8mi0nlkflZBvJe+dEO/O12Q==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.7.tgz", + "integrity": "sha512-ZQVoAwNYnFMIbd4DUc517HuwNelJUY6YOzwqrbcAgCnVn+79/OK7UjwA93SPpdTOpKDVkLIzavWm/Ck7SmnDPQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -7684,12 +8201,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.5.tgz", - "integrity": "sha512-2L2erASEro1WC5nV+plwIMxrTXpvpfzl4e+Nre6vBVRR2HKeGGcvpJyyL3/PpiSg+cJG2KpTmZmq934Olb6e5A==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.7.tgz", + "integrity": "sha512-ncvgCr9a15nPlkhIUx3CU4d7E7WEuVJOV7fS7nnK2hLtPK9tYRBkMHQbhXU1VvvKeBm/O0x26OEoBq+ngFpOEQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7709,12 +8226,12 @@ } }, "node_modules/@smithy/md5-js": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.5.tgz", - "integrity": "sha512-Bt6jpSTMWfjCtC0s79gZ/WZ1w90grfmopVOWqkI2ovhjpD5Q2XRXuecIPB9689L2+cCySMbaXDhBPU56FKNDNg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.7.tgz", + "integrity": "sha512-Wv6JcUxtOLTnxvNjDnAiATUsk8gvA6EeS8zzHig07dotpByYsLot+m0AaQEniUBjx97AC41MQR4hW0baraD1Xw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, @@ -7723,13 +8240,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.5.tgz", - "integrity": "sha512-Y/RabVa5vbl5FuHYV2vUCwvh/dqzrEY/K2yWPSqvhFUwIY0atLqO4TienjBXakoy4zrKAMCZwg+YEqmH7jaN7A==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.7.tgz", + "integrity": "sha512-GszfBfCcvt7kIbJ41LuNa5f0wvQCHhnGx/aDaZJCCT05Ld6x6U2s0xsc/0mBFONBZjQJp2U/0uSJ178OXOwbhg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7737,18 +8254,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.3.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.14.tgz", - "integrity": "sha512-v0q4uTKgBM8dsqGjqsabZQyH85nFaTnFcgpWU1uydKFsdyyMzfvOkNum9G7VK+dOP01vUnoZxIeRiJ6uD0kjIg==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.1.tgz", + "integrity": "sha512-gpLspUAoe6f1M6H0u4cVuFzxZBrsGZmjx2O9SigurTx4PbntYa4AJ+o0G0oGm1L2oSX6oBhcGHwrfJHup2JnJg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.18.7", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-middleware": "^4.2.5", + "@smithy/core": "^3.20.0", + "@smithy/middleware-serde": "^4.2.8", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", + "@smithy/url-parser": "^4.2.7", + "@smithy/util-middleware": "^4.2.7", "tslib": "^2.6.2" }, "engines": { @@ -7756,18 +8273,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.14.tgz", - "integrity": "sha512-Z2DG8Ej7FyWG1UA+7HceINtSLzswUgs2np3sZX0YBBxCt+CXG4QUxv88ZDS3+2/1ldW7LqtSY1UO/6VQ1pND8Q==", + "version": "4.4.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.17.tgz", + "integrity": "sha512-MqbXK6Y9uq17h+4r0ogu/sBT6V/rdV+5NvYL7ZV444BKfQygYe8wAhDrVXagVebN6w2RE0Fm245l69mOsPGZzg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/service-error-classification": "^4.2.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/service-error-classification": "^4.2.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", + "@smithy/util-middleware": "^4.2.7", + "@smithy/util-retry": "^4.2.7", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, @@ -7776,13 +8293,13 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.6.tgz", - "integrity": "sha512-VkLoE/z7e2g8pirwisLz8XJWedUSY8my/qrp81VmAdyrhi94T+riBfwP+AOEEFR9rFTSonC/5D2eWNmFabHyGQ==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.8.tgz", + "integrity": "sha512-8rDGYen5m5+NV9eHv9ry0sqm2gI6W7mc1VSFMtn6Igo25S507/HaOX9LTHAS2/J32VXD0xSzrY0H5FJtOMS4/w==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7790,12 +8307,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.5.tgz", - "integrity": "sha512-bYrutc+neOyWxtZdbB2USbQttZN0mXaOyYLIsaTbJhFsfpXyGWUxJpEuO1rJ8IIJm2qH4+xJT0mxUSsEDTYwdQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.7.tgz", + "integrity": "sha512-bsOT0rJ+HHlZd9crHoS37mt8qRRN/h9jRve1SXUhVbkRzu0QaNYZp1i1jha4n098tsvROjcwfLlfvcFuJSXEsw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7803,14 +8320,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.5.tgz", - "integrity": "sha512-UTurh1C4qkVCtqggI36DGbLB2Kv8UlcFdMXDcWMbqVY2uRg0XmT9Pb4Vj6oSQ34eizO1fvR0RnFV4Axw4IrrAg==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.7.tgz", + "integrity": "sha512-7r58wq8sdOcrwWe+klL9y3bc4GW1gnlfnFOuL7CXa7UzfhzhxKuzNdtqgzmTV+53lEp9NXh5hY/S4UgjLOzPfw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/shared-ini-file-loader": "^4.4.2", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7818,15 +8335,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.5.tgz", - "integrity": "sha512-CMnzM9R2WqlqXQGtIlsHMEZfXKJVTIrqCNoSd/QpAyp+Dw0a1Vps13l6ma1fH8g7zSPNsA59B/kWgeylFuA/lw==", + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.7.tgz", + "integrity": "sha512-NELpdmBOO6EpZtWgQiHjoShs1kmweaiNuETUpuup+cmm/xJYjT4eUjfhrXRP4jCOaAsS3c3yPsP3B+K+/fyPCQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/querystring-builder": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/abort-controller": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/querystring-builder": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7834,12 +8351,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.5.tgz", - "integrity": "sha512-8iLN1XSE1rl4MuxvQ+5OSk/Zb5El7NJZ1td6Tn+8dQQHIjp59Lwl6bd0+nzw6SKm2wSSriH2v/I9LPzUic7EOg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.7.tgz", + "integrity": "sha512-jmNYKe9MGGPoSl/D7JDDs1C8b3dC8f/w78LbaVfoTtWy4xAd5dfjaFG9c9PWPihY4ggMQNQSMtzU77CNgAJwmA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7847,12 +8364,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.5.tgz", - "integrity": "sha512-RlaL+sA0LNMp03bf7XPbFmT5gN+w3besXSWMkA8rcmxLSVfiEXElQi4O2IWwPfxzcHkxqrwBFMbngB8yx/RvaQ==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.7.tgz", + "integrity": "sha512-1r07pb994I20dD/c2seaZhoCuNYm0rWrvBxhCQ70brNh11M5Ml2ew6qJVo0lclB3jMIXirD4s2XRXRe7QEi0xA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7860,12 +8377,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.5.tgz", - "integrity": "sha512-y98otMI1saoajeik2kLfGyRp11e5U/iJYH/wLCh3aTV/XutbGT9nziKGkgCaMD1ghK7p6htHMm6b6scl9JRUWg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.7.tgz", + "integrity": "sha512-eKONSywHZxK4tBxe2lXEysh8wbBdvDWiA+RIuaxZSgCMmA0zMgoDpGLJhnyj+c0leOQprVnXOmcB4m+W9Rw7sg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, @@ -7874,12 +8391,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.5.tgz", - "integrity": "sha512-031WCTdPYgiQRYNPXznHXof2YM0GwL6SeaSyTH/P72M1Vz73TvCNH2Nq8Iu2IEPq9QP2yx0/nrw5YmSeAi/AjQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.7.tgz", + "integrity": "sha512-3X5ZvzUHmlSTHAXFlswrS6EGt8fMSIxX/c3Rm1Pni3+wYWB6cjGocmRIoqcQF9nU5OgGmL0u7l9m44tSUpfj9w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7887,24 +8404,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.5.tgz", - "integrity": "sha512-8fEvK+WPE3wUAcDvqDQG1Vk3ANLR8Px979te96m84CbKAjBVf25rPYSzb4xU4hlTyho7VhOGnh5i62D/JVF0JQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.7.tgz", + "integrity": "sha512-YB7oCbukqEb2Dlh3340/8g8vNGbs/QsNNRms+gv3N2AtZz9/1vSBx6/6tpwQpZMEJFs7Uq8h4mmOn48ZZ72MkA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0" + "@smithy/types": "^4.11.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.0.tgz", - "integrity": "sha512-5WmZ5+kJgJDjwXXIzr1vDTG+RhF9wzSODQBfkrQ2VVkYALKGvZX1lgVSxEkgicSAFnFhPj5rudJV0zoinqS0bA==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.2.tgz", + "integrity": "sha512-M7iUUff/KwfNunmrgtqBfvZSzh3bmFgv/j/t1Y1dQ+8dNo34br1cqVEqy6v0mYEgi0DkGO7Xig0AnuOaEGVlcg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -7912,16 +8429,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.5.tgz", - "integrity": "sha512-xSUfMu1FT7ccfSXkoLl/QRQBi2rOvi3tiBZU2Tdy3I6cgvZ6SEi9QNey+lqps/sJRnogIS+lq+B1gxxbra2a/w==", + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.7.tgz", + "integrity": "sha512-9oNUlqBlFZFOSdxgImA6X5GFuzE7V2H7VG/7E70cdLhidFbdtvxxt81EHgykGK5vq5D3FafH//X+Oy31j3CKOg==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.5", + "@smithy/util-middleware": "^4.2.7", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -7931,17 +8448,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.9.10", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.10.tgz", - "integrity": "sha512-Jaoz4Jw1QYHc1EFww/E6gVtNjhoDU+gwRKqXP6C3LKYqqH2UQhP8tMP3+t/ePrhaze7fhLE8vS2q6vVxBANFTQ==", + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.2.tgz", + "integrity": "sha512-D5z79xQWpgrGpAHb054Fn2CCTQZpog7JELbVQ6XAvXs5MNKWf28U9gzSBlJkOyMl9LA1TZEjRtwvGXfP0Sl90g==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.18.7", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "@smithy/util-stream": "^4.5.6", + "@smithy/core": "^3.20.0", + "@smithy/middleware-endpoint": "^4.4.1", + "@smithy/middleware-stack": "^4.2.7", + "@smithy/protocol-http": "^5.3.7", + "@smithy/types": "^4.11.0", + "@smithy/util-stream": "^4.5.8", "tslib": "^2.6.2" }, "engines": { @@ -7949,9 +8466,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.9.0.tgz", - "integrity": "sha512-MvUbdnXDTwykR8cB1WZvNNwqoWVaTRA0RLlLmf/cIFNMM2cKWz01X4Ly6SMC4Kks30r8tT3Cty0jmeWfiuyHTA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.11.0.tgz", + "integrity": "sha512-mlrmL0DRDVe3mNrjTcVcZEgkFmufITfUAPBEA+AHYiIeYyJebso/He1qLbP3PssRe22KUzLRpQSdBPbXdgZ2VA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -7961,13 +8478,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.5.tgz", - "integrity": "sha512-VaxMGsilqFnK1CeBX+LXnSuaMx4sTL/6znSZh2829txWieazdVxr54HmiyTsIbpOTLcf5nYpq9lpzmwRdxj6rQ==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.7.tgz", + "integrity": "sha512-/RLtVsRV4uY3qPWhBDsjwahAtt3x2IsMGnP5W1b2VZIe+qgCqkLxI1UOHDZp1Q1QSOrdOR32MF3Ph2JfWT1VHg==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/querystring-parser": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -8038,14 +8555,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.13", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.13.tgz", - "integrity": "sha512-hlVLdAGrVfyNei+pKIgqDTxfu/ZI2NSyqj4IDxKd5bIsIqwR/dSlkxlPaYxFiIaDVrBy0he8orsFy+Cz119XvA==", + "version": "4.3.16", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.16.tgz", + "integrity": "sha512-/eiSP3mzY3TsvUOYMeL4EqUX6fgUOj2eUOU4rMMgVbq67TiRLyxT7Xsjxq0bW3OwuzK009qOwF0L2OgJqperAQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@smithy/property-provider": "^4.2.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -8053,17 +8570,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.16", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.16.tgz", - "integrity": "sha512-F1t22IUiJLHrxW9W1CQ6B9PN+skZ9cqSuzB18Eh06HrJPbjsyZ7ZHecAKw80DQtyGTRcVfeukKaCRYebFwclbg==", + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.19.tgz", + "integrity": "sha512-3a4+4mhf6VycEJyHIQLypRbiwG6aJvbQAeRAVXydMmfweEPnLLabRbdyo/Pjw8Rew9vjsh5WCdhmDaHkQnhhhA==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.3", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", + "@smithy/config-resolver": "^4.4.5", + "@smithy/credential-provider-imds": "^4.2.7", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/property-provider": "^4.2.7", + "@smithy/smithy-client": "^4.10.2", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -8071,13 +8588,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.5.tgz", - "integrity": "sha512-3O63AAWu2cSNQZp+ayl9I3NapW1p1rR5mlVHcF6hAB1dPZUQFfRPYtplWX/3xrzWthPGj5FqB12taJJCfH6s8A==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.7.tgz", + "integrity": "sha512-s4ILhyAvVqhMDYREeTS68R43B1V5aenV5q/V1QpRQJkCXib5BPRo4s7uNdzGtIKxaPHCfU/8YkvPAEvTpxgspg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", + "@smithy/node-config-provider": "^4.3.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -8097,12 +8614,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.5.tgz", - "integrity": "sha512-6Y3+rvBF7+PZOc40ybeZMcGln6xJGVeY60E7jy9Mv5iKpMJpHgRE6dKy9ScsVxvfAYuEX4Q9a65DQX90KaQ3bA==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.7.tgz", + "integrity": "sha512-i1IkpbOae6NvIKsEeLLM9/2q4X+M90KV3oCFgWQI4q0Qz+yUZvsr+gZPdAEAtFhWQhAHpTsJO8DRJPuwVyln+w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.9.0", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -8110,13 +8627,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.5.tgz", - "integrity": "sha512-GBj3+EZBbN4NAqJ/7pAhsXdfzdlznOh8PydUijy6FpNIMnHPSMO2/rP4HKu+UFeikJxShERk528oy7GT79YiJg==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.7.tgz", + "integrity": "sha512-SvDdsQyF5CIASa4EYVT02LukPHVzAgUA4kMAuZ97QJc2BpAqZfA4PINB8/KOoCXEw9tsuv/jQjMeaHFvxdLNGg==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/service-error-classification": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -8124,14 +8641,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.6.tgz", - "integrity": "sha512-qWw/UM59TiaFrPevefOZ8CNBKbYEP6wBAIlLqxn3VAIo9rgnTNc4ASbVrqDmhuwI87usnjhdQrxodzAGFFzbRQ==", + "version": "4.5.8", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.8.tgz", + "integrity": "sha512-ZnnBhTapjM0YPGUSmOs0Mcg/Gg87k503qG4zU2v/+Js2Gu+daKOJMeqcQns8ajepY8tgzzfYxl6kQyZKml6O2w==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/types": "^4.9.0", + "@smithy/fetch-http-handler": "^5.3.8", + "@smithy/node-http-handler": "^4.4.7", + "@smithy/types": "^4.11.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", @@ -8168,13 +8685,13 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.5.tgz", - "integrity": "sha512-Dbun99A3InifQdIrsXZ+QLcC0PGBPAdrl4cj1mTgJvyc9N2zf7QSxg8TBkzsCmGJdE3TLbO9ycwpY0EkWahQ/g==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.7.tgz", + "integrity": "sha512-vHJFXi9b7kUEpHWUCY3Twl+9NPOZvQ0SAi+Ewtn48mbiJk4JY9MZmKQjGB4SCvVb9WPiSphZJYY6RIbs+grrzw==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.5", - "@smithy/types": "^4.9.0", + "@smithy/abort-controller": "^4.2.7", + "@smithy/types": "^4.11.0", "tslib": "^2.6.2" }, "engines": { @@ -13313,12 +13830,12 @@ } }, "node_modules/eslint-config-next": { - "version": "16.0.8", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.0.8.tgz", - "integrity": "sha512-8J5cOAboXIV3f8OD6BOyj7Fik6n/as7J4MboiUSExWruf/lCu1OPR3ZVSdnta6WhzebrmAATEmNSBZsLWA6kbg==", + "version": "16.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.0.tgz", + "integrity": "sha512-RlPb8E2uO/Ix/w3kizxz6+6ogw99WqtNzTG0ArRZ5NEkIYcsfRb8U0j7aTG7NjRvcrsak5QtUSuxGNN2UcA58g==", "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.0.8", + "@next/eslint-plugin-next": "16.1.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -15932,9 +16449,9 @@ } }, "node_modules/lucide-react": { - "version": "0.559.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.559.0.tgz", - "integrity": "sha512-3ymrkBPXWk3U2bwUDg6TdA6hP5iGDMgPEAMLhchEgTQmA+g0Zk24tOtKtXMx35w1PizTmsBC3RhP88QYm+7mHQ==", + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -16344,9 +16861,9 @@ } }, "node_modules/next-intl": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.5.8.tgz", - "integrity": "sha512-BdN6494nvt09WtmW5gbWdwRhDDHC/Sg7tBMhN7xfYds3vcRCngSDXat81gmJkblw9jYOv8zXzzFJyu5VYXnJzg==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.6.1.tgz", + "integrity": "sha512-KlWgWtKLBPUsTPgxqwyjws1wCMD2QKxLlVjeeGj53DC1JWfKmBShKOrhIP0NznZrRQ0GleeoDUeHSETmyyIFeA==", "funding": [ { "type": "individual", @@ -16356,11 +16873,12 @@ "license": "MIT", "dependencies": { "@formatjs/intl-localematcher": "^0.5.4", + "@parcel/watcher": "^2.4.1", "@swc/core": "^1.15.2", "negotiator": "^1.0.0", - "next-intl-swc-plugin-extractor": "^4.5.8", - "po-parser": "^1.0.2", - "use-intl": "^4.5.8" + "next-intl-swc-plugin-extractor": "^4.6.1", + "po-parser": "^2.0.0", + "use-intl": "^4.6.1" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", @@ -16374,9 +16892,9 @@ } }, "node_modules/next-intl-swc-plugin-extractor": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.5.8.tgz", - "integrity": "sha512-hscCKUv+5GQ0CCNbvqZ8gaxnAGToCgDTbL++jgCq8SCk/ljtZDEeQZcMk46Nm6Ynn49Q/JKF4Npo/Sq1mpbusA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.6.1.tgz", + "integrity": "sha512-+HHNeVERfSvuPDF7LYVn3pxst5Rf7EYdUTw7C7WIrYhcLaKiZ1b9oSRkTQddAN3mifDMCfHqO4kAQ/pcKiBl3A==", "license": "MIT" }, "node_modules/next-themes": { @@ -16456,6 +16974,12 @@ "node": ">=10" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-cache": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz", @@ -19358,9 +19882,9 @@ } }, "node_modules/po-parser": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-1.0.2.tgz", - "integrity": "sha512-yTIQL8PZy7V8c0psPoJUx7fayez+Mo/53MZgX9MPuPHx+Dt+sRPNuRbI+6Oqxnddhkd68x4Nlgon/zizL1Xg+w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/po-parser/-/po-parser-2.0.0.tgz", + "integrity": "sha512-SZvoKi3PoI/hHa2V9je9CW7Xgxl4dvO74cvaa6tWShIHT51FkPxje6pt0gTJznJrU67ix91nDaQp2hUxkOYhKA==", "license": "MIT" }, "node_modules/possible-typed-array-names": { @@ -19747,9 +20271,9 @@ } }, "node_modules/react-day-picker": { - "version": "9.12.0", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.12.0.tgz", - "integrity": "sha512-t8OvG/Zrciso5CQJu5b1A7yzEmebvST+S3pOVQJWxwjjVngyG/CA2htN/D15dLI4uTEuLLkbZyS4YYt480FAtA==", + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.0.tgz", + "integrity": "sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ==", "license": "MIT", "dependencies": { "@date-fns/tz": "^1.4.1", @@ -21936,9 +22460,9 @@ } }, "node_modules/stripe": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.0.0.tgz", - "integrity": "sha512-EaZeWpbJOCcDytdjKSwdrL5BxzbDGNueiCfHjHXlPdBQvLqoxl6AAivC35SPzTmVXJb5duXQlXFGS45H0+e6Gg==", + "version": "20.1.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.1.0.tgz", + "integrity": "sha512-o1VNRuMkY76ZCq92U3EH3/XHm/WHp7AerpzDs4Zyo8uE5mFL4QUcv/2SudWsSnhBSp4moO2+ZoGCZ7mT8crPmQ==", "license": "MIT", "dependencies": { "qs": "^6.11.0" @@ -22780,9 +23304,9 @@ } }, "node_modules/use-intl": { - "version": "4.5.8", - "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.5.8.tgz", - "integrity": "sha512-rWPV2Sirw55BQbA/7ndUBtsikh8WXwBrUkZJ1mD35+emj/ogPPqgCZdv1DdrEFK42AjF1g5w8d3x8govhqPH6Q==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.6.1.tgz", + "integrity": "sha512-mUIj6QvJZ7Rk33mLDxRziz1YiBBAnIji8YW4TXXMdYHtaPEbVucrXD3iKQGAqJhbVn0VnjrEtIKYO1B18mfSJw==", "license": "MIT", "dependencies": { "@formatjs/fast-memoize": "^2.2.0", @@ -23319,9 +23843,9 @@ } }, "node_modules/zod": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", - "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.2.1.tgz", + "integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==", "license": "MIT", "peer": true, "funding": { diff --git a/package.json b/package.json index 267c78eb..bc9d84ff 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ }, "dependencies": { "@asteasolutions/zod-to-openapi": "8.2.0", - "@aws-sdk/client-s3": "3.948.0", + "@aws-sdk/client-s3": "3.955.0", "@faker-js/faker": "10.1.0", "@headlessui/react": "2.2.9", "@hookform/resolvers": "5.2.2", @@ -84,7 +84,7 @@ "date-fns": "4.1.0", "drizzle-orm": "0.45.0", "eslint": "9.39.1", - "eslint-config-next": "16.0.8", + "eslint-config-next": "16.1.0", "express": "5.2.1", "express-rate-limit": "8.2.1", "glob": "13.0.0", @@ -96,11 +96,11 @@ "jmespath": "0.16.0", "js-yaml": "4.1.1", "jsonwebtoken": "9.0.3", - "lucide-react": "0.559.0", + "lucide-react": "0.562.0", "maxmind": "5.0.1", "moment": "2.30.1", "next": "15.5.9", - "next-intl": "4.5.8", + "next-intl": "4.6.1", "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", @@ -113,7 +113,7 @@ "posthog-node": "5.17.2", "qrcode.react": "4.2.0", "react": "19.2.3", - "react-day-picker": "9.12.0", + "react-day-picker": "9.13.0", "react-dom": "19.2.3", "react-easy-sort": "1.8.0", "react-hook-form": "7.68.0", @@ -123,7 +123,7 @@ "reodotdev": "1.0.0", "resend": "6.6.0", "semver": "7.7.3", - "stripe": "20.0.0", + "stripe": "20.1.0", "swagger-ui-express": "5.0.1", "tailwind-merge": "3.4.0", "topojson-client": "3.1.0", @@ -136,7 +136,7 @@ "ws": "8.18.3", "yaml": "2.8.2", "yargs": "18.0.0", - "zod": "4.1.13", + "zod": "4.2.1", "zod-validation-error": "5.0.0" }, "devDependencies": { From dd137580857e39009cdc907375c3b1e7552a2e93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 01:19:18 +0000 Subject: [PATCH 148/153] Bump the dev-patch-updates group across 1 directory with 4 updates Bumps the dev-patch-updates group with 4 updates in the / directory: [@dotenvx/dotenvx](https://github.com/dotenvx/dotenvx), [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss), [esbuild](https://github.com/evanw/esbuild) and [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss). Updates `@dotenvx/dotenvx` from 1.51.1 to 1.51.2 - [Release notes](https://github.com/dotenvx/dotenvx/releases) - [Changelog](https://github.com/dotenvx/dotenvx/blob/main/CHANGELOG.md) - [Commits](https://github.com/dotenvx/dotenvx/compare/v1.51.1...v1.51.2) Updates `@tailwindcss/postcss` from 4.1.17 to 4.1.18 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.18/packages/@tailwindcss-postcss) Updates `esbuild` from 0.27.1 to 0.27.2 - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md) - [Commits](https://github.com/evanw/esbuild/compare/v0.27.1...v0.27.2) Updates `tailwindcss` from 4.1.17 to 4.1.18 - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.1.18/packages/tailwindcss) --- updated-dependencies: - dependency-name: "@dotenvx/dotenvx" dependency-version: 1.51.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@tailwindcss/postcss" dependency-version: 4.1.18 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: esbuild dependency-version: 0.27.2 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: tailwindcss dependency-version: 4.1.18 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates ... Signed-off-by: dependabot[bot] --- package-lock.json | 378 +++++++++++++++++++++++----------------------- package.json | 8 +- 2 files changed, 193 insertions(+), 193 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5206a52b..e4a23ed4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -116,9 +116,9 @@ "zod-validation-error": "5.0.0" }, "devDependencies": { - "@dotenvx/dotenvx": "1.51.1", + "@dotenvx/dotenvx": "1.51.2", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@tailwindcss/postcss": "4.1.17", + "@tailwindcss/postcss": "4.1.18", "@tanstack/react-query-devtools": "5.91.1", "@types/better-sqlite3": "7.6.13", "@types/cookie-parser": "1.4.10", @@ -143,12 +143,12 @@ "@types/yargs": "17.0.35", "babel-plugin-react-compiler": "1.0.0", "drizzle-kit": "0.31.8", - "esbuild": "0.27.1", + "esbuild": "0.27.2", "esbuild-node-externals": "1.20.1", "postcss": "8.5.6", "prettier": "3.7.4", "react-email": "5.0.7", - "tailwindcss": "4.1.17", + "tailwindcss": "4.1.18", "tsc-alias": "1.8.16", "tsx": "4.21.0", "typescript": "5.9.3", @@ -2153,9 +2153,9 @@ "license": "MIT" }, "node_modules/@dotenvx/dotenvx": { - "version": "1.51.1", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.1.tgz", - "integrity": "sha512-fqcQxcxC4LOaUlW8IkyWw8x0yirlLUkbxohz9OnWvVWjf73J5yyw7jxWnkOJaUKXZotcGEScDox9MU6rSkcDgg==", + "version": "1.51.2", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.2.tgz", + "integrity": "sha512-+693mNflujDZxudSEqSNGpn92QgFhJlBn9q2mDQ9yGWyHuz3hZ8B5g3EXCwdAz4DMJAI+OFCIbfEFZS+YRdrEA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2682,9 +2682,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", - "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -2699,9 +2699,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", - "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -2716,9 +2716,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", - "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -2733,9 +2733,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", - "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -2750,9 +2750,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", - "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -2767,9 +2767,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", - "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -2784,9 +2784,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", - "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -2801,9 +2801,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", - "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -2818,9 +2818,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", - "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -2835,9 +2835,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", - "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -2852,9 +2852,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", - "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -2869,9 +2869,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", - "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -2886,9 +2886,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", - "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -2903,9 +2903,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", - "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -2920,9 +2920,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", - "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -2937,9 +2937,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", - "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -2954,9 +2954,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", - "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -2971,9 +2971,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", - "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ "arm64" ], @@ -2988,9 +2988,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", - "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -3005,9 +3005,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", - "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], @@ -3022,9 +3022,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", - "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -3039,9 +3039,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", - "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "cpu": [ "arm64" ], @@ -3056,9 +3056,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", - "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -3073,9 +3073,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", - "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -3090,9 +3090,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", - "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -3107,9 +3107,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", - "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -8974,9 +8974,9 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz", - "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8986,37 +8986,37 @@ "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.17" + "tailwindcss": "4.1.18" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz", - "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", "dev": true, "license": "MIT", "engines": { "node": ">= 10" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.17", - "@tailwindcss/oxide-darwin-arm64": "4.1.17", - "@tailwindcss/oxide-darwin-x64": "4.1.17", - "@tailwindcss/oxide-freebsd-x64": "4.1.17", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.17", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.17", - "@tailwindcss/oxide-linux-x64-musl": "4.1.17", - "@tailwindcss/oxide-wasm32-wasi": "4.1.17", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.17" + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz", - "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", "cpu": [ "arm64" ], @@ -9031,9 +9031,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz", - "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", "cpu": [ "arm64" ], @@ -9048,9 +9048,9 @@ } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz", - "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", "cpu": [ "x64" ], @@ -9065,9 +9065,9 @@ } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz", - "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", "cpu": [ "x64" ], @@ -9082,9 +9082,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz", - "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", "cpu": [ "arm" ], @@ -9099,9 +9099,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz", - "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", "cpu": [ "arm64" ], @@ -9116,9 +9116,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz", - "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", "cpu": [ "arm64" ], @@ -9133,9 +9133,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz", - "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", "cpu": [ "x64" ], @@ -9150,9 +9150,9 @@ } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz", - "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", "cpu": [ "x64" ], @@ -9167,9 +9167,9 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz", - "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -9185,10 +9185,10 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.6.0", - "@emnapi/runtime": "^1.6.0", + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.0.7", + "@napi-rs/wasm-runtime": "^1.1.0", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.4.0" }, @@ -9197,7 +9197,7 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.6.0", + "version": "1.7.1", "dev": true, "inBundle": true, "license": "MIT", @@ -9208,7 +9208,7 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.6.0", + "version": "1.7.1", "dev": true, "inBundle": true, "license": "MIT", @@ -9228,14 +9228,14 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.0.7", + "version": "1.1.0", "dev": true, "inBundle": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.5.0", - "@emnapi/runtime": "^1.5.0", + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, @@ -9257,9 +9257,9 @@ "optional": true }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz", - "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", "cpu": [ "arm64" ], @@ -9274,9 +9274,9 @@ } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz", - "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", "cpu": [ "x64" ], @@ -9291,17 +9291,17 @@ } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz", - "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.17", - "@tailwindcss/oxide": "4.1.17", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", "postcss": "^8.4.41", - "tailwindcss": "4.1.17" + "tailwindcss": "4.1.18" } }, "node_modules/@tanstack/query-core": { @@ -13457,9 +13457,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", "dev": true, "license": "MIT", "dependencies": { @@ -13671,9 +13671,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.1", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", - "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -13685,32 +13685,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.1", - "@esbuild/android-arm": "0.27.1", - "@esbuild/android-arm64": "0.27.1", - "@esbuild/android-x64": "0.27.1", - "@esbuild/darwin-arm64": "0.27.1", - "@esbuild/darwin-x64": "0.27.1", - "@esbuild/freebsd-arm64": "0.27.1", - "@esbuild/freebsd-x64": "0.27.1", - "@esbuild/linux-arm": "0.27.1", - "@esbuild/linux-arm64": "0.27.1", - "@esbuild/linux-ia32": "0.27.1", - "@esbuild/linux-loong64": "0.27.1", - "@esbuild/linux-mips64el": "0.27.1", - "@esbuild/linux-ppc64": "0.27.1", - "@esbuild/linux-riscv64": "0.27.1", - "@esbuild/linux-s390x": "0.27.1", - "@esbuild/linux-x64": "0.27.1", - "@esbuild/netbsd-arm64": "0.27.1", - "@esbuild/netbsd-x64": "0.27.1", - "@esbuild/openbsd-arm64": "0.27.1", - "@esbuild/openbsd-x64": "0.27.1", - "@esbuild/openharmony-arm64": "0.27.1", - "@esbuild/sunos-x64": "0.27.1", - "@esbuild/win32-arm64": "0.27.1", - "@esbuild/win32-ia32": "0.27.1", - "@esbuild/win32-x64": "0.27.1" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/esbuild-node-externals": { @@ -22651,9 +22651,9 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", - "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", "license": "MIT", "peer": true }, diff --git a/package.json b/package.json index bc9d84ff..c0c4caec 100644 --- a/package.json +++ b/package.json @@ -140,9 +140,9 @@ "zod-validation-error": "5.0.0" }, "devDependencies": { - "@dotenvx/dotenvx": "1.51.1", + "@dotenvx/dotenvx": "1.51.2", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@tailwindcss/postcss": "4.1.17", + "@tailwindcss/postcss": "4.1.18", "@tanstack/react-query-devtools": "5.91.1", "@types/better-sqlite3": "7.6.13", "@types/cookie-parser": "1.4.10", @@ -167,12 +167,12 @@ "@types/js-yaml": "4.0.9", "babel-plugin-react-compiler": "1.0.0", "drizzle-kit": "0.31.8", - "esbuild": "0.27.1", + "esbuild": "0.27.2", "esbuild-node-externals": "1.20.1", "postcss": "8.5.6", "prettier": "3.7.4", "react-email": "5.0.7", - "tailwindcss": "4.1.17", + "tailwindcss": "4.1.18", "tsc-alias": "1.8.16", "tsx": "4.21.0", "typescript": "5.9.3", From 981d777a6582c0803bb6fb760e895b9915dd9ca7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Dec 2025 16:48:36 +0000 Subject: [PATCH 149/153] Bump the prod-patch-updates group across 1 directory with 6 updates Bumps the prod-patch-updates group with 6 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@react-email/components](https://github.com/resend/react-email/tree/HEAD/packages/components) | `1.0.1` | `1.0.2` | | [@react-email/tailwind](https://github.com/resend/react-email/tree/HEAD/packages/tailwind) | `2.0.1` | `2.0.2` | | [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms) | `0.5.10` | `0.5.11` | | [drizzle-orm](https://github.com/drizzle-team/drizzle-orm) | `0.45.0` | `0.45.1` | | [eslint](https://github.com/eslint/eslint) | `9.39.1` | `9.39.2` | | [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.17.2` | `5.17.4` | Updates `@react-email/components` from 1.0.1 to 1.0.2 - [Release notes](https://github.com/resend/react-email/releases) - [Changelog](https://github.com/resend/react-email/blob/canary/packages/components/CHANGELOG.md) - [Commits](https://github.com/resend/react-email/commits/@react-email/components@1.0.2/packages/components) Updates `@react-email/tailwind` from 2.0.1 to 2.0.2 - [Release notes](https://github.com/resend/react-email/releases) - [Changelog](https://github.com/resend/react-email/blob/canary/packages/tailwind/CHANGELOG.md) - [Commits](https://github.com/resend/react-email/commits/@react-email/tailwind@2.0.2/packages/tailwind) Updates `@tailwindcss/forms` from 0.5.10 to 0.5.11 - [Release notes](https://github.com/tailwindlabs/tailwindcss-forms/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss-forms/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss-forms/compare/v0.5.10...v0.5.11) Updates `drizzle-orm` from 0.45.0 to 0.45.1 - [Release notes](https://github.com/drizzle-team/drizzle-orm/releases) - [Commits](https://github.com/drizzle-team/drizzle-orm/compare/0.45.0...0.45.1) Updates `eslint` from 9.39.1 to 9.39.2 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v9.39.1...v9.39.2) Updates `posthog-node` from 5.17.2 to 5.17.4 - [Release notes](https://github.com/PostHog/posthog-js/releases) - [Changelog](https://github.com/PostHog/posthog-js/blob/main/packages/node/CHANGELOG.md) - [Commits](https://github.com/PostHog/posthog-js/commits/posthog-node@5.17.4/packages/node) --- updated-dependencies: - dependency-name: "@react-email/components" dependency-version: 1.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-patch-updates - dependency-name: "@react-email/tailwind" dependency-version: 2.0.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-patch-updates - dependency-name: "@tailwindcss/forms" dependency-version: 0.5.11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-patch-updates - dependency-name: drizzle-orm dependency-version: 0.45.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-patch-updates - dependency-name: eslint dependency-version: 9.39.2 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-patch-updates - dependency-name: posthog-node dependency-version: 5.17.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: prod-patch-updates ... Signed-off-by: dependabot[bot] --- package-lock.json | 72 +++++++++++++++++++++++------------------------ package.json | 12 ++++---- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index e4a23ed4..8cdfda25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,12 +36,12 @@ "@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.1", + "@react-email/components": "1.0.2", "@react-email/render": "2.0.0", - "@react-email/tailwind": "2.0.1", + "@react-email/tailwind": "2.0.2", "@simplewebauthn/browser": "13.2.2", "@simplewebauthn/server": "13.2.2", - "@tailwindcss/forms": "0.5.10", + "@tailwindcss/forms": "0.5.11", "@tanstack/react-query": "5.90.12", "@tanstack/react-table": "8.21.3", "arctic": "3.7.0", @@ -58,8 +58,8 @@ "crypto-js": "4.2.0", "d3": "7.9.0", "date-fns": "4.1.0", - "drizzle-orm": "0.45.0", - "eslint": "9.39.1", + "drizzle-orm": "0.45.1", + "eslint": "9.39.2", "eslint-config-next": "16.1.0", "express": "5.2.1", "express-rate-limit": "8.2.1", @@ -86,7 +86,7 @@ "nprogress": "0.2.0", "oslo": "1.2.1", "pg": "8.16.3", - "posthog-node": "5.17.2", + "posthog-node": "5.17.4", "qrcode.react": "4.2.0", "react": "19.2.3", "react-day-picker": "9.13.0", @@ -3236,9 +3236,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5322,9 +5322,9 @@ } }, "node_modules/@posthog/core": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.7.1.tgz", - "integrity": "sha512-kjK0eFMIpKo9GXIbts8VtAknsoZ18oZorANdtuTj1CbgS28t4ZVq//HAWhnxEuXRTrtkd+SUJ6Ux3j2Af8NCuA==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.8.1.tgz", + "integrity": "sha512-jfzBtQIk9auRi/biO+G/gumK5KxqsD5wOr7XpYMROE/I3pazjP4zIziinp21iQuIQJMXrDvwt9Af3njgOGwtew==", "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6" @@ -7626,9 +7626,9 @@ } }, "node_modules/@react-email/components": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.1.tgz", - "integrity": "sha512-HnL0Y/up61sOBQT2cQg9N/kCoW0bP727gDs2MkFWQYELg6+iIHidMDvENXFC0f1ZE6hTB+4t7sszptvTcJWsDA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.2.tgz", + "integrity": "sha512-VKQR/motrySQMvy+ZUwPjdeD9iI9mCt8cfXuJAX8cK16rtzkEe12yq6/pXyW7c6qEMj7d+PNsoAcO+3AbJSfPg==", "license": "MIT", "dependencies": { "@react-email/body": "0.2.0", @@ -7649,11 +7649,11 @@ "@react-email/render": "2.0.0", "@react-email/row": "0.0.12", "@react-email/section": "0.0.16", - "@react-email/tailwind": "2.0.1", + "@react-email/tailwind": "2.0.2", "@react-email/text": "0.1.5" }, "engines": { - "node": ">=22.0.0" + "node": ">=20.0.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" @@ -7821,15 +7821,15 @@ } }, "node_modules/@react-email/tailwind": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.1.tgz", - "integrity": "sha512-/xq0IDYVY7863xPY7cdI45Xoz7M6CnIQBJcQvbqN7MNVpopfH9f+mhjayV1JGfKaxlGWuxfLKhgi9T2shsnEFg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.2.tgz", + "integrity": "sha512-ooi1H77+w+MN3a3Yps66GYTMoo9PvLtzJ1bTEI+Ta58MUUEQOcdxxXPwbnox+xj2kSwv0g/B63qquNTabKI8Bw==", "license": "MIT", "dependencies": { - "tailwindcss": "^4.1.12" + "tailwindcss": "^4.1.18" }, "engines": { - "node": ">=22.0.0" + "node": ">=20.0.0" }, "peerDependencies": { "@react-email/body": "0.2.0", @@ -8962,9 +8962,9 @@ } }, "node_modules/@tailwindcss/forms": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz", - "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==", + "version": "0.5.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz", + "integrity": "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==", "license": "MIT", "dependencies": { "mini-svg-data-uri": "^1.2.3" @@ -13114,9 +13114,9 @@ } }, "node_modules/drizzle-orm": { - "version": "0.45.0", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.0.tgz", - "integrity": "sha512-lyd9VRk3SXKRjV/gQckQzmJgkoYMvVG3A2JAV0vh3L+Lwk+v9+rK5Gj0H22y+ZBmxsrRBgJ5/RbQCN7DWd1dtQ==", + "version": "0.45.1", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.1.tgz", + "integrity": "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA==", "license": "Apache-2.0", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", @@ -13770,9 +13770,9 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "license": "MIT", "peer": true, "dependencies": { @@ -13782,7 +13782,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -19965,12 +19965,12 @@ } }, "node_modules/posthog-node": { - "version": "5.17.2", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.17.2.tgz", - "integrity": "sha512-lz3YJOr0Nmiz0yHASaINEDHqoV+0bC3eD8aZAG+Ky292dAnVYul+ga/dMX8KCBXg8hHfKdxw0SztYD5j6dgUqQ==", + "version": "5.17.4", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.17.4.tgz", + "integrity": "sha512-hrd+Do/DMt40By12ESIDUfD81V9OASjq9XHjycZrGiD8cX/ZwCIVSJLUb7nQmvSCWcKII+u+nnPVuc4LjTDl9g==", "license": "MIT", "dependencies": { - "@posthog/core": "1.7.1" + "@posthog/core": "1.8.1" }, "engines": { "node": ">=20" diff --git a/package.json b/package.json index c0c4caec..af6a8dde 100644 --- a/package.json +++ b/package.json @@ -60,12 +60,12 @@ "@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.1", + "@react-email/components": "1.0.2", "@react-email/render": "2.0.0", - "@react-email/tailwind": "2.0.1", + "@react-email/tailwind": "2.0.2", "@simplewebauthn/browser": "13.2.2", "@simplewebauthn/server": "13.2.2", - "@tailwindcss/forms": "0.5.10", + "@tailwindcss/forms": "0.5.11", "@tanstack/react-query": "5.90.12", "@tanstack/react-table": "8.21.3", "arctic": "3.7.0", @@ -82,8 +82,8 @@ "crypto-js": "4.2.0", "d3": "7.9.0", "date-fns": "4.1.0", - "drizzle-orm": "0.45.0", - "eslint": "9.39.1", + "drizzle-orm": "0.45.1", + "eslint": "9.39.2", "eslint-config-next": "16.1.0", "express": "5.2.1", "express-rate-limit": "8.2.1", @@ -110,7 +110,7 @@ "nprogress": "0.2.0", "oslo": "1.2.1", "pg": "8.16.3", - "posthog-node": "5.17.2", + "posthog-node": "5.17.4", "qrcode.react": "4.2.0", "react": "19.2.3", "react-day-picker": "9.13.0", From 4f154d212e98a73d94f6f851224030039bec4b89 Mon Sep 17 00:00:00 2001 From: Thomas Wilde Date: Tue, 16 Dec 2025 11:18:54 -0700 Subject: [PATCH 150/153] Add ASN-based resource rule matching - Add MaxMind ASN database integration - Implement ASN lookup and matching in resource rule verification - Add curated list of 100+ major ASNs (cloud, ISP, CDN, mobile carriers) - Add ASN dropdown selector in resource rules UI with search functionality - Support custom ASN input for unlisted ASNs - Add 'ALL ASNs' special case handling (AS0) - Cache ASN lookups with 5-minute TTL for performance - Update validation schemas to support ASN match type This allows administrators to create resource access rules based on Autonomous System Numbers, similar to existing country-based rules. Useful for restricting access by ISP, cloud provider, or mobile carrier. --- server/db/asns.ts | 321 ++++++++++++++++++ server/db/maxmindAsn.ts | 13 + server/lib/asn.ts | 29 ++ server/lib/config.ts | 4 + server/lib/readConfigFile.ts | 3 +- server/routers/badger/verifySession.ts | 63 +++- server/routers/resource/createResourceRule.ts | 2 +- server/routers/resource/updateResourceRule.ts | 2 +- .../resources/proxy/[niceId]/rules/page.tsx | 246 +++++++++++++- src/lib/pullEnv.ts | 3 +- src/lib/types/env.ts | 1 + 11 files changed, 678 insertions(+), 9 deletions(-) create mode 100644 server/db/asns.ts create mode 100644 server/db/maxmindAsn.ts create mode 100644 server/lib/asn.ts diff --git a/server/db/asns.ts b/server/db/asns.ts new file mode 100644 index 00000000..f78577f5 --- /dev/null +++ b/server/db/asns.ts @@ -0,0 +1,321 @@ +// Curated list of major ASNs (Cloud Providers, CDNs, ISPs, etc.) +// This is not exhaustive - there are 100,000+ ASNs globally +// Users can still enter any ASN manually in the input field +export const MAJOR_ASNS = [ + { + name: "ALL ASNs", + code: "ALL", + asn: 0 // Special value that will match all + }, + // Major Cloud Providers + { + name: "Google LLC", + code: "AS15169", + asn: 15169 + }, + { + name: "Amazon AWS", + code: "AS16509", + asn: 16509 + }, + { + name: "Amazon AWS (EC2)", + code: "AS14618", + asn: 14618 + }, + { + name: "Microsoft Azure", + code: "AS8075", + asn: 8075 + }, + { + name: "Microsoft Corporation", + code: "AS8068", + asn: 8068 + }, + { + name: "DigitalOcean", + code: "AS14061", + asn: 14061 + }, + { + name: "Linode", + code: "AS63949", + asn: 63949 + }, + { + name: "Hetzner Online", + code: "AS24940", + asn: 24940 + }, + { + name: "OVH SAS", + code: "AS16276", + asn: 16276 + }, + { + name: "Oracle Cloud", + code: "AS31898", + asn: 31898 + }, + { + name: "Alibaba Cloud", + code: "AS45102", + asn: 45102 + }, + { + name: "IBM Cloud", + code: "AS36351", + asn: 36351 + }, + + // CDNs + { + name: "Cloudflare", + code: "AS13335", + asn: 13335 + }, + { + name: "Fastly", + code: "AS54113", + asn: 54113 + }, + { + name: "Akamai Technologies", + code: "AS20940", + asn: 20940 + }, + { + name: "Akamai (Primary)", + code: "AS16625", + asn: 16625 + }, + + // Mobile Carriers - US + { + name: "T-Mobile USA", + code: "AS21928", + asn: 21928 + }, + { + name: "Verizon Wireless", + code: "AS6167", + asn: 6167 + }, + { + name: "AT&T Mobility", + code: "AS20057", + asn: 20057 + }, + { + name: "Sprint (T-Mobile)", + code: "AS1239", + asn: 1239 + }, + { + name: "US Cellular", + code: "AS6430", + asn: 6430 + }, + + // Mobile Carriers - Europe + { + name: "Vodafone UK", + code: "AS25135", + asn: 25135 + }, + { + name: "EE (UK)", + code: "AS12576", + asn: 12576 + }, + { + name: "Three UK", + code: "AS29194", + asn: 29194 + }, + { + name: "O2 UK", + code: "AS13285", + asn: 13285 + }, + { + name: "Telefonica Spain Mobile", + code: "AS12430", + asn: 12430 + }, + + // Mobile Carriers - Asia + { + name: "NTT DoCoMo (Japan)", + code: "AS9605", + asn: 9605 + }, + { + name: "SoftBank Mobile (Japan)", + code: "AS17676", + asn: 17676 + }, + { + name: "SK Telecom (Korea)", + code: "AS9318", + asn: 9318 + }, + { + name: "KT Corporation Mobile (Korea)", + code: "AS4766", + asn: 4766 + }, + { + name: "Airtel India", + code: "AS24560", + asn: 24560 + }, + { + name: "China Mobile", + code: "AS9808", + asn: 9808 + }, + + // Major US ISPs + { + name: "AT&T Services", + code: "AS7018", + asn: 7018 + }, + { + name: "Comcast Cable", + code: "AS7922", + asn: 7922 + }, + { + name: "Verizon", + code: "AS701", + asn: 701 + }, + { + name: "Cox Communications", + code: "AS22773", + asn: 22773 + }, + { + name: "Charter Communications", + code: "AS20115", + asn: 20115 + }, + { + name: "CenturyLink", + code: "AS209", + asn: 209 + }, + + // Major European ISPs + { + name: "Deutsche Telekom", + code: "AS3320", + asn: 3320 + }, + { + name: "Vodafone", + code: "AS1273", + asn: 1273 + }, + { + name: "British Telecom", + code: "AS2856", + asn: 2856 + }, + { + name: "Orange", + code: "AS3215", + asn: 3215 + }, + { + name: "Telefonica", + code: "AS12956", + asn: 12956 + }, + + // Major Asian ISPs + { + name: "China Telecom", + code: "AS4134", + asn: 4134 + }, + { + name: "China Unicom", + code: "AS4837", + asn: 4837 + }, + { + name: "NTT Communications", + code: "AS2914", + asn: 2914 + }, + { + name: "KDDI Corporation", + code: "AS2516", + asn: 2516 + }, + { + name: "Reliance Jio (India)", + code: "AS55836", + asn: 55836 + }, + + // VPN/Proxy Providers + { + name: "Private Internet Access", + code: "AS46562", + asn: 46562 + }, + { + name: "NordVPN", + code: "AS202425", + asn: 202425 + }, + { + name: "Mullvad VPN", + code: "AS213281", + asn: 213281 + }, + + // Social Media / Major Tech + { + name: "Facebook/Meta", + code: "AS32934", + asn: 32934 + }, + { + name: "Twitter/X", + code: "AS13414", + asn: 13414 + }, + { + name: "Apple", + code: "AS714", + asn: 714 + }, + { + name: "Netflix", + code: "AS2906", + asn: 2906 + }, + + // Academic/Research + { + name: "MIT", + code: "AS3", + asn: 3 + }, + { + name: "Stanford University", + code: "AS32", + asn: 32 + }, + { + name: "CERN", + code: "AS513", + asn: 513 + } +]; diff --git a/server/db/maxmindAsn.ts b/server/db/maxmindAsn.ts new file mode 100644 index 00000000..13951262 --- /dev/null +++ b/server/db/maxmindAsn.ts @@ -0,0 +1,13 @@ +import maxmind, { AsnResponse, Reader } from "maxmind"; +import config from "@server/lib/config"; + +let maxmindAsnLookup: Reader | null; +if (config.getRawConfig().server.maxmind_asn_path) { + maxmindAsnLookup = await maxmind.open( + config.getRawConfig().server.maxmind_asn_path! + ); +} else { + maxmindAsnLookup = null; +} + +export { maxmindAsnLookup }; diff --git a/server/lib/asn.ts b/server/lib/asn.ts new file mode 100644 index 00000000..18a39c46 --- /dev/null +++ b/server/lib/asn.ts @@ -0,0 +1,29 @@ +import logger from "@server/logger"; +import { maxmindAsnLookup } from "@server/db/maxmindAsn"; + +export async function getAsnForIp(ip: string): Promise { + try { + if (!maxmindAsnLookup) { + logger.debug( + "MaxMind ASN DB path not configured, cannot perform ASN lookup" + ); + return; + } + + const result = maxmindAsnLookup.get(ip); + + if (!result || !result.autonomous_system_number) { + return; + } + + logger.debug( + `ASN lookup successful for IP ${ip}: AS${result.autonomous_system_number}` + ); + + return result.autonomous_system_number; + } catch (error) { + logger.error("Error performing ASN lookup:", error); + } + + return; +} diff --git a/server/lib/config.ts b/server/lib/config.ts index 9874518e..405db2d1 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -99,6 +99,10 @@ export class Config { process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path; } + if (parsedConfig.server.maxmind_asn_path) { + process.env.MAXMIND_ASN_PATH = parsedConfig.server.maxmind_asn_path; + } + this.rawConfig = parsedConfig; } diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 365bcb13..da567820 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -133,7 +133,8 @@ export const configSchema = z .optional(), trust_proxy: z.int().gte(0).optional().default(1), secret: z.string().pipe(z.string().min(8)).optional(), - maxmind_db_path: z.string().optional() + maxmind_db_path: z.string().optional(), + maxmind_asn_path: z.string().optional() }) .optional() .default({ diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index d7fe9190..0e3a3489 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -29,6 +29,7 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import { getCountryCodeForIp } from "@server/lib/geoip"; +import { getAsnForIp } from "@server/lib/asn"; import { getOrgTierData } from "#dynamic/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import { verifyPassword } from "@server/auth/password"; @@ -128,6 +129,10 @@ export async function verifyResourceSession( ? await getCountryCodeFromIp(clientIp) : undefined; + const ipAsn = clientIp + ? await getAsnFromIp(clientIp) + : undefined; + let cleanHost = host; // if the host ends with :port, strip it if (cleanHost.match(/:[0-9]{1,5}$/)) { @@ -216,7 +221,8 @@ export async function verifyResourceSession( resource.resourceId, clientIp, path, - ipCC + ipCC, + ipAsn ); if (action == "ACCEPT") { @@ -910,7 +916,8 @@ async function checkRules( resourceId: number, clientIp: string | undefined, path: string | undefined, - ipCC?: string + ipCC?: string, + ipAsn?: number ): Promise<"ACCEPT" | "DROP" | "PASS" | undefined> { const ruleCacheKey = `rules:${resourceId}`; @@ -954,6 +961,12 @@ async function checkRules( (await isIpInGeoIP(ipCC, rule.value)) ) { return rule.action as any; + } else if ( + clientIp && + rule.match == "ASN" && + (await isIpInAsn(ipAsn, rule.value)) + ) { + return rule.action as any; } } @@ -1090,6 +1103,52 @@ async function isIpInGeoIP( return ipCountryCode?.toUpperCase() === checkCountryCode.toUpperCase(); } +async function isIpInAsn( + ipAsn: number | undefined, + checkAsn: string +): Promise { + // Handle "ALL" special case + if (checkAsn === "ALL" || checkAsn === "AS0") { + return true; + } + + if (!ipAsn) { + return false; + } + + // Normalize the check ASN - remove "AS" prefix if present and convert to number + const normalizedCheckAsn = checkAsn.toUpperCase().replace(/^AS/, ""); + const checkAsnNumber = parseInt(normalizedCheckAsn, 10); + + if (isNaN(checkAsnNumber)) { + logger.warn(`Invalid ASN format in rule: ${checkAsn}`); + return false; + } + + const match = ipAsn === checkAsnNumber; + logger.debug( + `ASN check: IP ASN ${ipAsn} ${match ? "matches" : "does not match"} rule ASN ${checkAsnNumber}` + ); + + return match; +} + +async function getAsnFromIp(ip: string): Promise { + const asnCacheKey = `asn:${ip}`; + + let cachedAsn: number | undefined = cache.get(asnCacheKey); + + if (!cachedAsn) { + cachedAsn = await getAsnForIp(ip); // do it locally + // Cache for longer since IP ASN doesn't change frequently + if (cachedAsn) { + cache.set(asnCacheKey, cachedAsn, 300); // 5 minutes + } + } + + return cachedAsn; +} + async function getCountryCodeFromIp(ip: string): Promise { const geoIpCacheKey = `geoip:${ip}`; diff --git a/server/routers/resource/createResourceRule.ts b/server/routers/resource/createResourceRule.ts index 3f86665b..a516d14a 100644 --- a/server/routers/resource/createResourceRule.ts +++ b/server/routers/resource/createResourceRule.ts @@ -17,7 +17,7 @@ import { OpenAPITags, registry } from "@server/openApi"; const createResourceRuleSchema = z.strictObject({ action: z.enum(["ACCEPT", "DROP", "PASS"]), - match: z.enum(["CIDR", "IP", "PATH", "COUNTRY"]), + match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN"]), value: z.string().min(1), priority: z.int(), enabled: z.boolean().optional() diff --git a/server/routers/resource/updateResourceRule.ts b/server/routers/resource/updateResourceRule.ts index cae3f16e..b443bd1c 100644 --- a/server/routers/resource/updateResourceRule.ts +++ b/server/routers/resource/updateResourceRule.ts @@ -25,7 +25,7 @@ const updateResourceRuleParamsSchema = z.strictObject({ const updateResourceRuleSchema = z .strictObject({ action: z.enum(["ACCEPT", "DROP", "PASS"]).optional(), - match: z.enum(["CIDR", "IP", "PATH", "COUNTRY"]).optional(), + match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN"]).optional(), value: z.string().min(1).optional(), priority: z.int(), enabled: z.boolean().optional() diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx index a58953e7..003b7f0e 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx @@ -75,6 +75,7 @@ import { Switch } from "@app/components/ui/switch"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { COUNTRIES } from "@server/db/countries"; +import { MAJOR_ASNS } from "@server/db/asns"; import { Command, CommandEmpty, @@ -117,11 +118,15 @@ export default function ResourceRules(props: { const [countrySelectValue, setCountrySelectValue] = useState(""); const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false); + const [openAddRuleAsnSelect, setOpenAddRuleAsnSelect] = + useState(false); const router = useRouter(); const t = useTranslations(); const { env } = useEnvContext(); 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 RuleAction = { ACCEPT: t("alwaysAllow"), @@ -133,7 +138,8 @@ export default function ResourceRules(props: { PATH: t("path"), IP: "IP", CIDR: t("ipAddressRange"), - COUNTRY: t("country") + COUNTRY: t("country"), + ASN: "ASN" } as const; const addRuleForm = useForm({ @@ -172,6 +178,30 @@ export default function ResourceRules(props: { }, []); async function addRule(data: z.infer) { + // Normalize ASN value + if (data.match === "ASN") { + const originalValue = data.value.toUpperCase(); + + // Handle special "ALL" case + if (originalValue === "ALL" || originalValue === "AS0") { + data.value = "ALL"; + } else { + // Remove AS prefix if present + const normalized = originalValue.replace(/^AS/, ""); + if (!/^\d+$/.test(normalized)) { + toast({ + variant: "destructive", + title: "Invalid ASN", + description: + "ASN must be a number, optionally prefixed with 'AS' (e.g., AS15169 or 15169), or 'ALL'" + }); + return; + } + // Add "AS" prefix for consistent storage + data.value = "AS" + normalized; + } + } + const isDuplicate = rules.some( (rule) => rule.action === data.action && @@ -280,6 +310,8 @@ export default function ResourceRules(props: { return t("rulesMatchUrl"); case "COUNTRY": return t("rulesMatchCountry"); + case "ASN": + return "Enter an Autonomous System Number (e.g., AS15169 or 15169)"; } } @@ -505,12 +537,12 @@ export default function ResourceRules(props: { ) @@ -592,6 +629,93 @@ export default function ResourceRules(props: { + ) : 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" + /> +
+
+
) : ( )} + {isMaxmindAsnAvailable && ( + + { + RuleMatch.ASN + } + + )} @@ -924,6 +1055,115 @@ export default function ResourceRules(props: { + ) : 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" + /> +
+
+
) : ( )} diff --git a/src/lib/pullEnv.ts b/src/lib/pullEnv.ts index 4e7e2981..dbe47bd5 100644 --- a/src/lib/pullEnv.ts +++ b/src/lib/pullEnv.ts @@ -15,7 +15,8 @@ export function pullEnv(): Env { resourceAccessTokenHeadersToken: process.env .RESOURCE_ACCESS_TOKEN_HEADERS_TOKEN as string, reoClientId: process.env.REO_CLIENT_ID as string, - maxmind_db_path: process.env.MAXMIND_DB_PATH as string + maxmind_db_path: process.env.MAXMIND_DB_PATH as string, + maxmind_asn_path: process.env.MAXMIND_ASN_PATH as string }, app: { environment: process.env.ENVIRONMENT as string, diff --git a/src/lib/types/env.ts b/src/lib/types/env.ts index d4b62d10..e40ac5d3 100644 --- a/src/lib/types/env.ts +++ b/src/lib/types/env.ts @@ -19,6 +19,7 @@ export type Env = { resourceAccessTokenHeadersToken: string; reoClientId?: string; maxmind_db_path?: string; + maxmind_asn_path?: string; }; email: { emailEnabled: boolean; From 4ecca88856cd1d5e7102ce2f4b49e76484b5bcb9 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 20 Dec 2025 11:58:12 -0500 Subject: [PATCH 151/153] Add asn option to blueprint type --- server/lib/blueprints/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index 0f0edded..2cd487fb 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -74,7 +74,7 @@ export const AuthSchema = z.object({ export const RuleSchema = z.object({ action: z.enum(["allow", "deny", "pass"]), - match: z.enum(["cidr", "path", "ip", "country"]), + match: z.enum(["cidr", "path", "ip", "country", "asn"]), value: z.string() }); From 13ddf307811d115e3b62da75e78dd7e667a5e0a0 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 20 Dec 2025 12:01:50 -0500 Subject: [PATCH 152/153] Add hybrid route --- server/private/routers/hybrid.ts | 65 ++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index 3accc500..751a1a0c 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -76,6 +76,7 @@ import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes"; import { maxmindLookup } from "@server/db/maxmind"; import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken"; import semver from "semver"; +import { maxmindAsnLookup } from "@server/db/maxmindAsn"; // Zod schemas for request validation const getResourceByDomainParamsSchema = z.strictObject({ @@ -1238,6 +1239,70 @@ hybridRouter.get( } ); +const asnIpLookupParamsSchema = z.object({ + ip: z.union([z.ipv4(), z.ipv6()]) +}); +hybridRouter.get( + "/asnip/:ip", + async (req: Request, res: Response, next: NextFunction) => { + try { + const parsedParams = asnIpLookupParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { ip } = parsedParams.data; + + if (!maxmindAsnLookup) { + return next( + createHttpError( + HttpCode.SERVICE_UNAVAILABLE, + "ASNIP service is not available" + ) + ); + } + + const result = maxmindAsnLookup.get(ip); + + if (!result || !result.autonomous_system_number) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "ASNIP information not found" + ) + ); + } + + const { autonomous_system_number } = result; + + logger.debug( + `ASNIP lookup successful for IP ${ip}: ${autonomous_system_number}` + ); + + return response(res, { + data: { asn: autonomous_system_number }, + success: true, + error: false, + message: "GeoIP lookup successful", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to validate resource session token" + ) + ); + } + } +); + // GERBIL ROUTERS const getConfigSchema = z.object({ publicKey: z.string(), From b80757a12904937352bf8deddf4633e932b3ef26 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 20 Dec 2025 12:06:21 -0500 Subject: [PATCH 153/153] Add blueprint validation --- server/lib/blueprints/types.ts | 70 +++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index 2cd487fb..df6d7bb0 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -72,11 +72,71 @@ export const AuthSchema = z.object({ "auto-login-idp": z.int().positive().optional() }); -export const RuleSchema = z.object({ - action: z.enum(["allow", "deny", "pass"]), - match: z.enum(["cidr", "path", "ip", "country", "asn"]), - value: z.string() -}); +export const RuleSchema = z + .object({ + action: z.enum(["allow", "deny", "pass"]), + match: z.enum(["cidr", "path", "ip", "country", "asn"]), + value: z.string() + }) + .refine( + (rule) => { + if (rule.match === "ip") { + // Check if it's a valid IP address (v4 or v6) + return z.union([z.ipv4(), z.ipv6()]).safeParse(rule.value) + .success; + } + return true; + }, + { + path: ["value"], + message: "Value must be a valid IP address when match is 'ip'" + } + ) + .refine( + (rule) => { + if (rule.match === "cidr") { + // Check if it's a valid CIDR (v4 or v6) + return z.union([z.cidrv4(), z.cidrv6()]).safeParse(rule.value) + .success; + } + return true; + }, + { + path: ["value"], + message: "Value must be a valid CIDR notation when match is 'cidr'" + } + ) + .refine( + (rule) => { + if (rule.match === "country") { + // Check if it's a valid 2-letter country code + return /^[A-Z]{2}$/.test(rule.value); + } + return true; + }, + { + path: ["value"], + message: + "Value must be a 2-letter country code when match is 'country'" + } + ) + .refine( + (rule) => { + if (rule.match === "asn") { + // Check if it's either AS format or just a number + const asNumberPattern = /^AS\d+$/i; + const isASFormat = asNumberPattern.test(rule.value); + const isNumeric = /^\d+$/.test(rule.value); + return isASFormat || isNumeric; + } + return true; + }, + { + path: ["value"], + message: + "Value must be either 'AS' format or a number when match is 'asn'" + } + ); export const HeaderSchema = z.object({ name: z.string().min(1),