From 0abc561bb843d07b19ef44456846525af672bb09 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 11 Nov 2025 02:22:26 +0100 Subject: [PATCH 0001/1422] =?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 26d8bbea1..d51f22106 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 0002/1422] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20add=20org=20a?= =?UTF-8?q?uth=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 ffbe820cc..15c1942b2 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 13453d2e4..6e5e49e7d 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 834da7b32..223fcaa48 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 0003/1422] =?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 6e5e49e7d..e259835d1 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 0004/1422] =?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 178a9bb97..0a45b32ea 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 70b2ef549..2d2abca34 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 15c1942b2..90cd19849 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 e259835d1..5c293ffdb 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 000000000..2ab90bdb7 --- /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 82b2c9991..fc501d820 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 000000000..fc695eeb2 --- /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 078cc6600..7cbac226f 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 0005/1422] =?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 0a45b32ea..178a9bb97 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 d08457e57..411ada446 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 000000000..478cc0c36 --- /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 000000000..03c03ac97 --- /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 000000000..2a01a8798 --- /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 000000000..e6fa04f4d --- /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 00ad117f6..6d1cf6dfb 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 5c2359024..0099aeeab 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 538c7fde6..c4048bcc8 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 2ab90bdb7..790655137 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 fc501d820..5ace97caa 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 000000000..89632d97b --- /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 000000000..dbffee5da --- /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 5cef9f0e6..6c4613e99 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 e51c5096c..d679e7b5e 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 0006/1422] =?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 178a9bb97..b790ed20f 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 411ada446..d08457e57 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 17d262c61..f9911095c 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 90cd19849..0b750d4ee 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 653967700..e74964c2b 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 5c293ffdb..c96fefc5c 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 478cc0c36..000000000 --- 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 03c03ac97..000000000 --- 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 2a01a8798..000000000 --- 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 e6fa04f4d..000000000 --- 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 6d1cf6dfb..2a32d9acb 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 000000000..03bdf8050 --- /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 000000000..e06705457 --- /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 2372ddfa9..d3ae65405 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 000000000..51aa73926 --- /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 26f59cab1..652ed4e9b 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 000000000..a0c883f04 --- /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 790655137..000000000 --- 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 5ace97caa..9472eb524 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 fdedba5c7..5dd5ccb8c 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 fc695eeb2..000000000 --- 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 95097a33c..e6cd017d9 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 0007/1422] =?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 b790ed20f..7b395d0a7 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 e6cd017d9..b20a18763 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 0008/1422] =?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 7cdea07a5..6cdbf23c0 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 af64e440b..9634a3de2 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 0009/1422] =?UTF-8?q?=E2=9C=A8=20save=20and=20update=20bra?= =?UTF-8?q?nding?= 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 7b395d0a7..2531350e8 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 03bdf8050..1fb243b04 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 e06705457..262e9ce82 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 51aa73926..e553d14d9 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 a0c883f04..0030afe1c 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 14474f128..39cac35ad 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 0010/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20do=20not=20manua?= =?UTF-8?q?lly=20track=20the=20loading=20state=20in=20`ConfirmDeleteDialog?= =?UTF-8?q?`?= 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 e2bc271f1..0ae36bf76 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 0011/1422] =?UTF-8?q?=E2=9C=A8=20apply=20auth=20branding?= =?UTF-8?q?=20to=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 e553d14d9..fc2125381 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 d51f22106..eeb01eaa7 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 39cac35ad..6d013816e 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 540b8e0e2..86a49496e 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 0012/1422] =?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 0030afe1c..139449bf0 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 000000000..7de991ca3 --- /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 81ddf58e7..e9e7b7310 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 000000000..e5754f076 --- /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 0013/1422] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20Add=20`primar?= =?UTF-8?q?yColor`=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 f9911095c..7707f3fd8 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 e74964c2b..e296ba4da 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 0014/1422] =?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 1d8dea215..2ac24e5ad 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 7de991ca3..000000000 --- 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 0015/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8Fshow=20org=20page?= =?UTF-8?q?=20branding=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 6d013816e..76890d2b4 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 0016/1422] =?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 e9e7b7310..33e2292f3 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 e5754f076..6b11a6fcb 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 0017/1422] =?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 7707f3fd8..1f30dbf5d 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 e296ba4da..930566659 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 76890d2b4..7460b067d 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 0018/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20only=20include?= =?UTF-8?q?=20org=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 fc2125381..1f5908dc0 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 0019/1422] =?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 480da7e40..c679b6c86 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 0020/1422] =?UTF-8?q?=E2=9C=A8add=20primary=20color=20bran?= =?UTF-8?q?ding=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 2531350e8..e43610f37 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 1f5908dc0..495c15fcb 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 7460b067d..580215fcc 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 0021/1422] =?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 eeb01eaa7..32b702984 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 0022/1422] =?UTF-8?q?=E2=9C=A8show=20toast=20message=20whe?= =?UTF-8?q?n=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 e43610f37..7c3844c91 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 b20a18763..2203fde20 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 0023/1422] =?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 5dd5ccb8c..84f876d0f 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 0024/1422] =?UTF-8?q?=F0=9F=90=9B=20fix=20`cols`=20and=20s?= =?UTF-8?q?ome=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 24f510dcd..64fbd7263 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 56acc9673..66be584bb 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 0026/1422] =?UTF-8?q?=E2=9C=A8apply=20branding=20to=20org?= =?UTF-8?q?=20auth=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 946326394..823f75a6a 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 6ef9ca81c..8a253d072 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 2ac24e5ad..afd9d0c4d 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 66be584bb..aad61e25e 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 000000000..598056e3c --- /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 0027/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8Fonly=20apply=20org?= =?UTF-8?q?=20branding=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 afd9d0c4d..71d31c01a 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 aad61e25e..59edf6792 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 0028/1422] =?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 d190bbb08..d030337df 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 0029/1422] =?UTF-8?q?=E2=8F=AA=20revert=20package.json=20c?= =?UTF-8?q?hanges?= 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 d030337df..b97e1b30e 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 90f8665d3..6ca203767 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 0030/1422] =?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 9dcba7104..4ff33734a 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 0031/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8Fremove=20unused=20i?= =?UTF-8?q?mports?= 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 2203fde20..4235368b8 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 0032/1422] =?UTF-8?q?=F0=9F=92=A1remove=20useless=20commen?= =?UTF-8?q?ts?= 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 580215fcc..95b7994d3 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 0033/1422] =?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 86a49496e..139d76b43 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 0034/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20correctly=20appl?= =?UTF-8?q?y=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 64fbd7263..50a83611c 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 0035/1422] =?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 59edf6792..15d3507f3 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 0036/1422] =?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 9472eb524..f7ece91d6 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 000000000..e9b05e400 --- /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 0037/1422] =?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 f7ece91d6..812b94918 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 0038/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20adapt=20zod=20sc?= =?UTF-8?q?hema=20to=20v4=20and=20move=20=20form=20description=20below=20t?= =?UTF-8?q?he=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 495c15fcb..f9f9d08c1 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 95b7994d3..04a6cbcb7 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 0039/1422] =?UTF-8?q?=F0=9F=9A=9A=20rename=20`SecurityFeat?= =?UTF-8?q?uresAlert`=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 2531659b9..30ba7d765 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 0040/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Also=20check=20f?= =?UTF-8?q?or=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 6b11a6fcb..d8173e6e6 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 0041/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20check=20for=20di?= =?UTF-8?q?sabled=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 84f876d0f..7928dc471 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 0042/1422] =?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 7928dc471..54d221884 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 0043/1422] =?UTF-8?q?=F0=9F=9B=82=20=20check=20for=20subsc?= =?UTF-8?q?ription=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 04a6cbcb7..136e97f1c 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 4235368b8..aff6662a0 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 0044/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20load=20branding?= =?UTF-8?q?=20only=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 4ff33734a..e6d870753 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 e9b05e400..251b92193 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 0045/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20check=20for=20li?= =?UTF-8?q?cence=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 71d31c01a..06910f6a4 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 251b92193..9440330b6 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 0046/1422] =?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 45ff43219..a3e3c3914 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 46ed27a218569f784b3cd4869c4453cac44a6ab5 Mon Sep 17 00:00:00 2001 From: Julien Breton Date: Mon, 1 Dec 2025 01:18:09 +0100 Subject: [PATCH 0047/1422] Fix: Extend Basic Auth compatibility with browsers --- messages/bg-BG.json | 2 + messages/cs-CZ.json | 2 + messages/de-DE.json | 2 + messages/en-US.json | 2 + messages/es-ES.json | 2 + messages/fr-FR.json | 36 +-- messages/it-IT.json | 2 + messages/ko-KR.json | 2 + messages/nb-NO.json | 2 + messages/nl-NL.json | 2 + messages/pl-PL.json | 2 + messages/pt-PT.json | 2 + messages/ru-RU.json | 2 + messages/tr-TR.json | 2 + messages/zh-CN.json | 2 + server/db/pg/schema/schema.ts | 9 + server/db/queries/verifySessionQueries.ts | 14 +- server/db/sqlite/schema/schema.ts | 233 +++++++++--------- server/lib/blueprints/proxyResources.ts | 79 ++++-- server/lib/blueprints/types.ts | 3 +- server/private/routers/hybrid.ts | 12 +- server/routers/badger/logRequestAudit.ts | 2 +- server/routers/badger/verifySession.ts | 62 ++++- .../routers/resource/getResourceAuthInfo.ts | 111 +++++---- server/routers/resource/listResources.ts | 10 +- .../routers/resource/setResourceHeaderAuth.ts | 43 ++-- .../[niceId]/authentication/page.tsx | 3 +- src/components/SetResourceHeaderAuthForm.tsx | 93 ++++--- src/components/SetResourcePasswordForm.tsx | 20 +- src/components/SetResourcePincodeForm.tsx | 20 +- src/components/SwitchInput.tsx | 49 +++- 31 files changed, 527 insertions(+), 300 deletions(-) diff --git a/messages/bg-BG.json b/messages/bg-BG.json index 7c31feb5e..eb6e748a5 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Създайте своя организация, сайт и ресурси", "setupNewOrg": "Нова организация", "setupCreateOrg": "Създаване на организация", diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index ff1a69005..61d9722e5 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Vytvořte si organizaci, lokalitu a služby", "setupNewOrg": "Nová organizace", "setupCreateOrg": "Vytvořit organizaci", diff --git a/messages/de-DE.json b/messages/de-DE.json index 15a56f2de..0486aa11b 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Erstelle eine Organisation, einen Standort und Ressourcen", "setupNewOrg": "Neue Organisation", "setupCreateOrg": "Organisation erstellen", diff --git a/messages/en-US.json b/messages/en-US.json index cf066c3d7..dc09f0c2c 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Create your organization, site, and resources", "setupNewOrg": "New Organization", "setupCreateOrg": "Create Organization", diff --git a/messages/es-ES.json b/messages/es-ES.json index 1b33c928e..fb97b367b 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Crea tu organización, sitio y recursos", "setupNewOrg": "Nueva organización", "setupCreateOrg": "Crear organización", diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 276fa9bdb..a523a7515 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Activez cette option pour forcer une réponse 401 lorsqu'un jeton d'authentification est manquant. Cette option est nécessaire pour les navigateurs et certaines bibliothèques HTTP qui n'envoient pas d'informations d'identification sans challenge du serveur.", + "headerAuthCompatibility": "Compatibilité étendue", "setupCreate": "Créez votre organisation, vos nœuds et vos ressources", "setupNewOrg": "Nouvelle organisation", "setupCreateOrg": "Créer une organisation", @@ -1834,23 +1836,23 @@ "rewritePathDescription": "Réécrivez éventuellement le chemin avant de le transmettre à la cible.", "continueToApplication": "Continuer vers l'application", "checkingInvite": "Vérification de l'invitation", - "setResourceHeaderAuth": "Définir l\\'authentification d\\'en-tête de la ressource", - "resourceHeaderAuthRemove": "Supprimer l'authentification de l'en-tête", - "resourceHeaderAuthRemoveDescription": "Authentification de l'en-tête supprimée avec succès.", - "resourceErrorHeaderAuthRemove": "Échec de la suppression de l'authentification de l'en-tête", - "resourceErrorHeaderAuthRemoveDescription": "Impossible de supprimer l'authentification de l'en-tête de la ressource.", - "resourceHeaderAuthProtectionEnabled": "Authentification de l'en-tête activée", - "resourceHeaderAuthProtectionDisabled": "L'authentification de l'en-tête est désactivée", - "headerAuthRemove": "Supprimer l'authentification de l'en-tête", - "headerAuthAdd": "Ajouter l'authentification de l'en-tête", - "resourceErrorHeaderAuthSetup": "Impossible de définir l'authentification de l'en-tête", - "resourceErrorHeaderAuthSetupDescription": "Impossible de définir l'authentification de l'en-tête pour la ressource.", - "resourceHeaderAuthSetup": "Authentification de l'en-tête définie avec succès", - "resourceHeaderAuthSetupDescription": "L'authentification de l'en-tête a été définie avec succès.", - "resourceHeaderAuthSetupTitle": "Authentification de l'en-tête", - "resourceHeaderAuthSetupTitleDescription": "Définissez les identifiants d'authentification de base (nom d'utilisateur et mot de passe) pour protéger cette ressource avec l'authentification de l'en-tête HTTP. Accédez-y en utilisant le format https://username:password@resource.example.com", - "resourceHeaderAuthSubmit": "Authentification de l'en-tête", - "actionSetResourceHeaderAuth": "Authentification de l'en-tête", + "setResourceHeaderAuth": "Définir l\\'authentification via en-tête de la ressource", + "resourceHeaderAuthRemove": "Supprimer l'authentification via en-tête", + "resourceHeaderAuthRemoveDescription": "En-tête d'authentification supprimée avec succès.", + "resourceErrorHeaderAuthRemove": "Échec de la suppression de l'authentification via en-tête", + "resourceErrorHeaderAuthRemoveDescription": "Impossible de supprimer l'authentification via en-tête sur la ressource.", + "resourceHeaderAuthProtectionEnabled": "Authentification par en-tête Activée", + "resourceHeaderAuthProtectionDisabled": "Authentification par en-tête Désactivée", + "headerAuthRemove": "Supprimer l'authentification via en-tête", + "headerAuthAdd": "Ajouter une en-tête d'authentification", + "resourceErrorHeaderAuthSetup": "Impossible de définir l'authentification via en-tête", + "resourceErrorHeaderAuthSetupDescription": "Impossible de définir l'authentification via en-tête pour la ressource.", + "resourceHeaderAuthSetup": "Authentification via en-tête définie avec succès", + "resourceHeaderAuthSetupDescription": "L'authentification via en-tête a été définie avec succès.", + "resourceHeaderAuthSetupTitle": "Authentification via en-tête", + "resourceHeaderAuthSetupTitleDescription": "Définissez les identifiants d'authentification de base (nom d'utilisateur et mot de passe) pour protéger cette ressource avec l'authentification via en-tête HTTP. Accédez-y en utilisant le format https://username:password@resource.example.com", + "resourceHeaderAuthSubmit": "Activer la protection via en-tête", + "actionSetResourceHeaderAuth": "Authentification via en-tête", "enterpriseEdition": "Édition Entreprise", "unlicensed": "Sans licence", "beta": "Bêta", diff --git a/messages/it-IT.json b/messages/it-IT.json index 08f1e4053..798ead372 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Crea la tua organizzazione, sito e risorse", "setupNewOrg": "Nuova Organizzazione", "setupCreateOrg": "Crea Organizzazione", diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 2ce7f7e68..75e572812 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "조직, 사이트 및 리소스를 생성하십시오.", "setupNewOrg": "새 조직", "setupCreateOrg": "조직 생성", diff --git a/messages/nb-NO.json b/messages/nb-NO.json index c2f8f791d..06539ff2e 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Lag din organisasjon, område og dine ressurser", "setupNewOrg": "Ny Organisasjon", "setupCreateOrg": "Opprett organisasjon", diff --git a/messages/nl-NL.json b/messages/nl-NL.json index 73ba2acc3..ea093df79 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Maak uw organisatie, site en bronnen aan", "setupNewOrg": "Nieuwe organisatie", "setupCreateOrg": "Nieuwe organisatie aanmaken", diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 6c401dfa3..4c84a1f7d 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Utwórz swoją organizację, witrynę i zasoby", "setupNewOrg": "Nowa organizacja", "setupCreateOrg": "Utwórz organizację", diff --git a/messages/pt-PT.json b/messages/pt-PT.json index ed9a45510..bd70cb0a2 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Crie sua organização, site e recursos", "setupNewOrg": "Nova organização", "setupCreateOrg": "Criar Organização", diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 23c521c19..8b0089007 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Создайте свою организацию, сайт и ресурсы", "setupNewOrg": "Новая организация", "setupCreateOrg": "Создать организацию", diff --git a/messages/tr-TR.json b/messages/tr-TR.json index 7fc8c5ff8..0e34a40f0 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "Organizasyonunuzu, sitenizi ve kaynaklarınızı oluşturun", "setupNewOrg": "Yeni Organizasyon", "setupCreateOrg": "Organizasyon Oluştur", diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 6b13a9120..dc48aaa72 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -1,4 +1,6 @@ { + "headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.", + "headerAuthCompatibility": "Extended compatibility", "setupCreate": "创建您的第一个组织、网站和资源", "setupNewOrg": "新建组织", "setupCreateOrg": "创建组织", diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index ffbe820cc..7a6af1e14 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -419,6 +419,14 @@ export const resourceHeaderAuth = pgTable("resourceHeaderAuth", { headerAuthHash: varchar("headerAuthHash").notNull() }); +export const resourceHeaderAuthExtendedCompatibility = pgTable("resourceHeaderAuthExtendedCompatibility", { + headerAuthExtendedCompatibilityId: serial("headerAuthExtendedCompatibilityId").primaryKey(), + resourceId: integer("resourceId") + .notNull() + .references(() => resources.resourceId, { onDelete: "cascade" }), + extendedCompatibilityIsActivated: boolean("extendedCompatibilityIsActivated").notNull().default(false), +}); + export const resourceAccessToken = pgTable("resourceAccessToken", { accessTokenId: varchar("accessTokenId").primaryKey(), orgId: varchar("orgId") @@ -781,6 +789,7 @@ export type ResourceSession = InferSelectModel; export type ResourcePincode = InferSelectModel; export type ResourcePassword = InferSelectModel; export type ResourceHeaderAuth = InferSelectModel; +export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel; export type ResourceOtp = InferSelectModel; export type ResourceAccessToken = InferSelectModel; export type ResourceWhitelist = InferSelectModel; diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts index 85bd7cc7e..381185e5c 100644 --- a/server/db/queries/verifySessionQueries.ts +++ b/server/db/queries/verifySessionQueries.ts @@ -1,4 +1,6 @@ -import { db, loginPage, LoginPage, loginPageOrg, Org, orgs } from "@server/db"; +import { + db, loginPage, LoginPage, loginPageOrg, Org, orgs, +} from "@server/db"; import { Resource, ResourcePassword, @@ -14,7 +16,9 @@ import { sessions, userOrgs, userResources, - users + users, + ResourceHeaderAuthExtendedCompatibility, + resourceHeaderAuthExtendedCompatibility } from "@server/db"; import { and, eq } from "drizzle-orm"; @@ -23,6 +27,7 @@ export type ResourceWithAuth = { pincode: ResourcePincode | null; password: ResourcePassword | null; headerAuth: ResourceHeaderAuth | null; + headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null org: Org; }; @@ -52,6 +57,10 @@ export async function getResourceByDomain( resourceHeaderAuth, eq(resourceHeaderAuth.resourceId, resources.resourceId) ) + .leftJoin( + resourceHeaderAuthExtendedCompatibility, + eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId) + ) .innerJoin( orgs, eq(orgs.orgId, resources.orgId) @@ -68,6 +77,7 @@ export async function getResourceByDomain( pincode: result.resourcePincode, password: result.resourcePassword, headerAuth: result.resourceHeaderAuth, + headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility, org: result.orgs }; } diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 13453d2e4..11cb7a9b8 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -1,32 +1,32 @@ -import { randomUUID } from "crypto"; -import { InferSelectModel } from "drizzle-orm"; -import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core"; -import { boolean } from "yargs"; +import {randomUUID} from "crypto"; +import {InferSelectModel} from "drizzle-orm"; +import {sqliteTable, text, integer, index} from "drizzle-orm/sqlite-core"; +import {boolean} from "yargs"; export const domains = sqliteTable("domains", { domainId: text("domainId").primaryKey(), baseDomain: text("baseDomain").notNull(), - configManaged: integer("configManaged", { mode: "boolean" }) + configManaged: integer("configManaged", {mode: "boolean"}) .notNull() .default(false), type: text("type"), // "ns", "cname", "wildcard" - verified: integer("verified", { mode: "boolean" }).notNull().default(false), - failed: integer("failed", { mode: "boolean" }).notNull().default(false), + verified: integer("verified", {mode: "boolean"}).notNull().default(false), + failed: integer("failed", {mode: "boolean"}).notNull().default(false), tries: integer("tries").notNull().default(0), certResolver: text("certResolver"), - preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" }) + preferWildcardCert: integer("preferWildcardCert", {mode: "boolean"}) }); export const dnsRecords = sqliteTable("dnsRecords", { - id: integer("id").primaryKey({ autoIncrement: true }), + id: integer("id").primaryKey({autoIncrement: true}), domainId: text("domainId") .notNull() - .references(() => domains.domainId, { onDelete: "cascade" }), + .references(() => domains.domainId, {onDelete: "cascade"}), 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), }); @@ -35,7 +35,7 @@ export const orgs = sqliteTable("orgs", { name: text("name").notNull(), subnet: text("subnet"), createdAt: text("createdAt"), - requireTwoFactor: integer("requireTwoFactor", { mode: "boolean" }), + requireTwoFactor: integer("requireTwoFactor", {mode: "boolean"}), maxSessionLengthHours: integer("maxSessionLengthHours"), // hours passwordExpiryDays: integer("passwordExpiryDays"), // days settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever @@ -52,23 +52,23 @@ export const orgs = sqliteTable("orgs", { export const userDomains = sqliteTable("userDomains", { userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), domainId: text("domainId") .notNull() - .references(() => domains.domainId, { onDelete: "cascade" }) + .references(() => domains.domainId, {onDelete: "cascade"}) }); export const orgDomains = sqliteTable("orgDomains", { orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), + .references(() => orgs.orgId, {onDelete: "cascade"}), domainId: text("domainId") .notNull() - .references(() => domains.domainId, { onDelete: "cascade" }) + .references(() => domains.domainId, {onDelete: "cascade"}) }); export const sites = sqliteTable("sites", { - siteId: integer("siteId").primaryKey({ autoIncrement: true }), + siteId: integer("siteId").primaryKey({autoIncrement: true}), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -85,7 +85,7 @@ export const sites = sqliteTable("sites", { megabytesOut: integer("bytesOut").default(0), lastBandwidthUpdate: text("lastBandwidthUpdate"), type: text("type").notNull(), // "newt" or "wireguard" - online: integer("online", { mode: "boolean" }).notNull().default(false), + online: integer("online", {mode: "boolean"}).notNull().default(false), // exit node stuff that is how to connect to the site when it has a wg server address: text("address"), // this is the address of the wireguard interface in newt @@ -93,15 +93,15 @@ export const sites = sqliteTable("sites", { publicKey: text("publicKey"), // TODO: Fix typo in publicKey lastHolePunch: integer("lastHolePunch"), listenPort: integer("listenPort"), - dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" }) + dockerSocketEnabled: integer("dockerSocketEnabled", {mode: "boolean"}) .notNull() .default(true), remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access }); export const resources = sqliteTable("resources", { - resourceId: integer("resourceId").primaryKey({ autoIncrement: true }), - resourceGuid: text("resourceGuid", { length: 36 }) + resourceId: integer("resourceId").primaryKey({autoIncrement: true}), + resourceGuid: text("resourceGuid", {length: 36}) .unique() .notNull() .$defaultFn(() => randomUUID()), @@ -117,38 +117,38 @@ export const resources = sqliteTable("resources", { domainId: text("domainId").references(() => domains.domainId, { onDelete: "set null" }), - ssl: integer("ssl", { mode: "boolean" }).notNull().default(false), - blockAccess: integer("blockAccess", { mode: "boolean" }) + ssl: integer("ssl", {mode: "boolean"}).notNull().default(false), + blockAccess: integer("blockAccess", {mode: "boolean"}) .notNull() .default(false), - sso: integer("sso", { mode: "boolean" }).notNull().default(true), - http: integer("http", { mode: "boolean" }).notNull().default(true), + sso: integer("sso", {mode: "boolean"}).notNull().default(true), + http: integer("http", {mode: "boolean"}).notNull().default(true), protocol: text("protocol").notNull(), proxyPort: integer("proxyPort"), - emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" }) + emailWhitelistEnabled: integer("emailWhitelistEnabled", {mode: "boolean"}) .notNull() .default(false), - applyRules: integer("applyRules", { mode: "boolean" }) + applyRules: integer("applyRules", {mode: "boolean"}) .notNull() .default(false), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), - stickySession: integer("stickySession", { mode: "boolean" }) + enabled: integer("enabled", {mode: "boolean"}).notNull().default(true), + stickySession: integer("stickySession", {mode: "boolean"}) .notNull() .default(false), tlsServerName: text("tlsServerName"), setHostHeader: text("setHostHeader"), - enableProxy: integer("enableProxy", { mode: "boolean" }).default(true), + enableProxy: integer("enableProxy", {mode: "boolean"}).default(true), skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { 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", { - targetId: integer("targetId").primaryKey({ autoIncrement: true }), + targetId: integer("targetId").primaryKey({autoIncrement: true}), resourceId: integer("resourceId") .references(() => resources.resourceId, { onDelete: "cascade" @@ -163,7 +163,7 @@ export const targets = sqliteTable("targets", { method: text("method"), port: integer("port").notNull(), internalPort: integer("internalPort"), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + enabled: integer("enabled", {mode: "boolean"}).notNull().default(true), path: text("path"), pathMatchType: text("pathMatchType"), // exact, prefix, regex rewritePath: text("rewritePath"), // if set, rewrites the path to this value before sending to the target @@ -177,8 +177,8 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", { }), targetId: integer("targetId") .notNull() - .references(() => targets.targetId, { onDelete: "cascade" }), - hcEnabled: integer("hcEnabled", { mode: "boolean" }) + .references(() => targets.targetId, {onDelete: "cascade"}), + hcEnabled: integer("hcEnabled", {mode: "boolean"}) .notNull() .default(false), hcPath: text("hcPath"), @@ -199,7 +199,7 @@ export const targetHealthCheck = sqliteTable("targetHealthCheck", { }); export const exitNodes = sqliteTable("exitNodes", { - exitNodeId: integer("exitNodeId").primaryKey({ autoIncrement: true }), + exitNodeId: integer("exitNodeId").primaryKey({autoIncrement: true}), name: text("name").notNull(), address: text("address").notNull(), // this is the address of the wireguard interface in gerbil endpoint: text("endpoint").notNull(), // this is how to reach gerbil externally - gets put into the wireguard config @@ -207,7 +207,7 @@ export const exitNodes = sqliteTable("exitNodes", { listenPort: integer("listenPort").notNull(), reachableAt: text("reachableAt"), // this is the internal address of the gerbil http server for command control maxConnections: integer("maxConnections"), - online: integer("online", { mode: "boolean" }).notNull().default(false), + online: integer("online", {mode: "boolean"}).notNull().default(false), lastPing: integer("lastPing"), type: text("type").default("gerbil"), // gerbil, remoteExitNode region: text("region") @@ -220,17 +220,17 @@ export const siteResources = sqliteTable("siteResources", { }), siteId: integer("siteId") .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }), + .references(() => sites.siteId, {onDelete: "cascade"}), orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), + .references(() => orgs.orgId, {onDelete: "cascade"}), niceId: text("niceId").notNull(), name: text("name").notNull(), protocol: text("protocol").notNull(), proxyPort: integer("proxyPort").notNull(), destinationPort: integer("destinationPort").notNull(), destinationIp: text("destinationIp").notNull(), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true) + enabled: integer("enabled", {mode: "boolean"}).notNull().default(true) }); export const users = sqliteTable("user", { @@ -243,20 +243,20 @@ export const users = sqliteTable("user", { onDelete: "cascade" }), passwordHash: text("passwordHash"), - twoFactorEnabled: integer("twoFactorEnabled", { mode: "boolean" }) + twoFactorEnabled: integer("twoFactorEnabled", {mode: "boolean"}) .notNull() .default(false), twoFactorSetupRequested: integer("twoFactorSetupRequested", { mode: "boolean" }).default(false), twoFactorSecret: text("twoFactorSecret"), - emailVerified: integer("emailVerified", { mode: "boolean" }) + emailVerified: integer("emailVerified", {mode: "boolean"}) .notNull() .default(false), dateCreated: text("dateCreated").notNull(), termsAcceptedTimestamp: text("termsAcceptedTimestamp"), termsVersion: text("termsVersion"), - serverAdmin: integer("serverAdmin", { mode: "boolean" }) + serverAdmin: integer("serverAdmin", {mode: "boolean"}) .notNull() .default(false), lastPasswordChange: integer("lastPasswordChange") @@ -290,7 +290,7 @@ export const webauthnChallenge = sqliteTable("webauthnChallenge", { export const setupTokens = sqliteTable("setupTokens", { tokenId: text("tokenId").primaryKey(), token: text("token").notNull(), - used: integer("used", { mode: "boolean" }).notNull().default(false), + used: integer("used", {mode: "boolean"}).notNull().default(false), dateCreated: text("dateCreated").notNull(), dateUsed: text("dateUsed") }); @@ -306,7 +306,7 @@ export const newts = sqliteTable("newt", { }); export const clients = sqliteTable("clients", { - clientId: integer("id").primaryKey({ autoIncrement: true }), + clientId: integer("id").primaryKey({autoIncrement: true}), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -323,7 +323,7 @@ export const clients = sqliteTable("clients", { lastBandwidthUpdate: text("lastBandwidthUpdate"), lastPing: integer("lastPing"), type: text("type").notNull(), // "olm" - online: integer("online", { mode: "boolean" }).notNull().default(false), + online: integer("online", {mode: "boolean"}).notNull().default(false), // endpoint: text("endpoint"), lastHolePunch: integer("lastHolePunch") }); @@ -331,11 +331,11 @@ export const clients = sqliteTable("clients", { export const clientSites = sqliteTable("clientSites", { clientId: integer("clientId") .notNull() - .references(() => clients.clientId, { onDelete: "cascade" }), + .references(() => clients.clientId, {onDelete: "cascade"}), siteId: integer("siteId") .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }), - isRelayed: integer("isRelayed", { mode: "boolean" }) + .references(() => sites.siteId, {onDelete: "cascade"}), + isRelayed: integer("isRelayed", {mode: "boolean"}) .notNull() .default(false), endpoint: text("endpoint") @@ -352,10 +352,10 @@ export const olms = sqliteTable("olms", { }); export const twoFactorBackupCodes = sqliteTable("twoFactorBackupCodes", { - codeId: integer("id").primaryKey({ autoIncrement: true }), + codeId: integer("id").primaryKey({autoIncrement: true}), userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), codeHash: text("codeHash").notNull() }); @@ -363,7 +363,7 @@ export const sessions = sqliteTable("session", { sessionId: text("id").primaryKey(), userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), expiresAt: integer("expiresAt").notNull(), issuedAt: integer("issuedAt") }); @@ -372,7 +372,7 @@ export const newtSessions = sqliteTable("newtSession", { sessionId: text("id").primaryKey(), newtId: text("newtId") .notNull() - .references(() => newts.newtId, { onDelete: "cascade" }), + .references(() => newts.newtId, {onDelete: "cascade"}), expiresAt: integer("expiresAt").notNull() }); @@ -380,14 +380,14 @@ export const olmSessions = sqliteTable("clientSession", { sessionId: text("id").primaryKey(), olmId: text("olmId") .notNull() - .references(() => olms.olmId, { onDelete: "cascade" }), + .references(() => olms.olmId, {onDelete: "cascade"}), expiresAt: integer("expiresAt").notNull() }); export const userOrgs = sqliteTable("userOrgs", { userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -396,28 +396,28 @@ export const userOrgs = sqliteTable("userOrgs", { roleId: integer("roleId") .notNull() .references(() => roles.roleId), - isOwner: integer("isOwner", { mode: "boolean" }).notNull().default(false), + isOwner: integer("isOwner", {mode: "boolean"}).notNull().default(false), autoProvisioned: integer("autoProvisioned", { mode: "boolean" }).default(false) }); export const emailVerificationCodes = sqliteTable("emailVerificationCodes", { - codeId: integer("id").primaryKey({ autoIncrement: true }), + codeId: integer("id").primaryKey({autoIncrement: true}), userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), email: text("email").notNull(), code: text("code").notNull(), expiresAt: integer("expiresAt").notNull() }); export const passwordResetTokens = sqliteTable("passwordResetTokens", { - tokenId: integer("id").primaryKey({ autoIncrement: true }), + tokenId: integer("id").primaryKey({autoIncrement: true}), email: text("email").notNull(), userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), tokenHash: text("tokenHash").notNull(), expiresAt: integer("expiresAt").notNull() }); @@ -429,13 +429,13 @@ export const actions = sqliteTable("actions", { }); export const roles = sqliteTable("roles", { - roleId: integer("roleId").primaryKey({ autoIncrement: true }), + roleId: integer("roleId").primaryKey({autoIncrement: true}), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" }) .notNull(), - isAdmin: integer("isAdmin", { mode: "boolean" }), + isAdmin: integer("isAdmin", {mode: "boolean"}), name: text("name").notNull(), description: text("description") }); @@ -443,92 +443,92 @@ export const roles = sqliteTable("roles", { export const roleActions = sqliteTable("roleActions", { roleId: integer("roleId") .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }), + .references(() => roles.roleId, {onDelete: "cascade"}), actionId: text("actionId") .notNull() - .references(() => actions.actionId, { onDelete: "cascade" }), + .references(() => actions.actionId, {onDelete: "cascade"}), orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }) + .references(() => orgs.orgId, {onDelete: "cascade"}) }); export const userActions = sqliteTable("userActions", { userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), actionId: text("actionId") .notNull() - .references(() => actions.actionId, { onDelete: "cascade" }), + .references(() => actions.actionId, {onDelete: "cascade"}), orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }) + .references(() => orgs.orgId, {onDelete: "cascade"}) }); export const roleSites = sqliteTable("roleSites", { roleId: integer("roleId") .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }), + .references(() => roles.roleId, {onDelete: "cascade"}), siteId: integer("siteId") .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }) + .references(() => sites.siteId, {onDelete: "cascade"}) }); export const userSites = sqliteTable("userSites", { userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), siteId: integer("siteId") .notNull() - .references(() => sites.siteId, { onDelete: "cascade" }) + .references(() => sites.siteId, {onDelete: "cascade"}) }); export const userClients = sqliteTable("userClients", { userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), clientId: integer("clientId") .notNull() - .references(() => clients.clientId, { onDelete: "cascade" }) + .references(() => clients.clientId, {onDelete: "cascade"}) }); export const roleClients = sqliteTable("roleClients", { roleId: integer("roleId") .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }), + .references(() => roles.roleId, {onDelete: "cascade"}), clientId: integer("clientId") .notNull() - .references(() => clients.clientId, { onDelete: "cascade" }) + .references(() => clients.clientId, {onDelete: "cascade"}) }); export const roleResources = sqliteTable("roleResources", { roleId: integer("roleId") .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }), + .references(() => roles.roleId, {onDelete: "cascade"}), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) + .references(() => resources.resourceId, {onDelete: "cascade"}) }); export const userResources = sqliteTable("userResources", { userId: text("userId") .notNull() - .references(() => users.userId, { onDelete: "cascade" }), + .references(() => users.userId, {onDelete: "cascade"}), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) + .references(() => resources.resourceId, {onDelete: "cascade"}) }); export const userInvites = sqliteTable("userInvites", { inviteId: text("inviteId").primaryKey(), orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), + .references(() => orgs.orgId, {onDelete: "cascade"}), email: text("email").notNull(), expiresAt: integer("expiresAt").notNull(), tokenHash: text("token").notNull(), roleId: integer("roleId") .notNull() - .references(() => roles.roleId, { onDelete: "cascade" }) + .references(() => roles.roleId, {onDelete: "cascade"}) }); export const resourcePincode = sqliteTable("resourcePincode", { @@ -537,7 +537,7 @@ export const resourcePincode = sqliteTable("resourcePincode", { }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), + .references(() => resources.resourceId, {onDelete: "cascade"}), pincodeHash: text("pincodeHash").notNull(), digitLength: integer("digitLength").notNull() }); @@ -548,7 +548,7 @@ export const resourcePassword = sqliteTable("resourcePassword", { }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), + .references(() => resources.resourceId, {onDelete: "cascade"}), passwordHash: text("passwordHash").notNull() }); @@ -558,18 +558,28 @@ export const resourceHeaderAuth = sqliteTable("resourceHeaderAuth", { }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), + .references(() => resources.resourceId, {onDelete: "cascade"}), headerAuthHash: text("headerAuthHash").notNull() }); +export const resourceHeaderAuthExtendedCompatibility = sqliteTable("resourceHeaderAuthExtendedCompatibility", { + headerAuthExtendedCompatibilityId: integer("headerAuthExtendedCompatibilityId").primaryKey({ + autoIncrement: true + }), + resourceId: integer("resourceId") + .notNull() + .references(() => resources.resourceId, {onDelete: "cascade"}), + extendedCompatibilityIsActivated: integer("extendedCompatibilityIsActivated", {mode: "boolean"}).notNull() +}); + export const resourceAccessToken = sqliteTable("resourceAccessToken", { accessTokenId: text("accessTokenId").primaryKey(), orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), + .references(() => orgs.orgId, {onDelete: "cascade"}), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), + .references(() => resources.resourceId, {onDelete: "cascade"}), tokenHash: text("tokenHash").notNull(), sessionLength: integer("sessionLength").notNull(), expiresAt: integer("expiresAt"), @@ -582,13 +592,13 @@ export const resourceSessions = sqliteTable("resourceSessions", { sessionId: text("id").primaryKey(), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), + .references(() => resources.resourceId, {onDelete: "cascade"}), expiresAt: integer("expiresAt").notNull(), sessionLength: integer("sessionLength").notNull(), - doNotExtend: integer("doNotExtend", { mode: "boolean" }) + doNotExtend: integer("doNotExtend", {mode: "boolean"}) .notNull() .default(false), - isRequestToken: integer("isRequestToken", { mode: "boolean" }), + isRequestToken: integer("isRequestToken", {mode: "boolean"}), userSessionId: text("userSessionId").references(() => sessions.sessionId, { onDelete: "cascade" }), @@ -620,11 +630,11 @@ export const resourceSessions = sqliteTable("resourceSessions", { }); export const resourceWhitelist = sqliteTable("resourceWhitelist", { - whitelistId: integer("id").primaryKey({ autoIncrement: true }), + whitelistId: integer("id").primaryKey({autoIncrement: true}), email: text("email").notNull(), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }) + .references(() => resources.resourceId, {onDelete: "cascade"}) }); export const resourceOtp = sqliteTable("resourceOtp", { @@ -633,7 +643,7 @@ export const resourceOtp = sqliteTable("resourceOtp", { }), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), + .references(() => resources.resourceId, {onDelete: "cascade"}), email: text("email").notNull(), otpHash: text("otpHash").notNull(), expiresAt: integer("expiresAt").notNull() @@ -645,11 +655,11 @@ export const versionMigrations = sqliteTable("versionMigrations", { }); export const resourceRules = sqliteTable("resourceRules", { - ruleId: integer("ruleId").primaryKey({ autoIncrement: true }), + ruleId: integer("ruleId").primaryKey({autoIncrement: true}), resourceId: integer("resourceId") .notNull() - .references(() => resources.resourceId, { onDelete: "cascade" }), - enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), + .references(() => resources.resourceId, {onDelete: "cascade"}), + enabled: integer("enabled", {mode: "boolean"}).notNull().default(true), priority: integer("priority").notNull(), action: text("action").notNull(), // ACCEPT, DROP, PASS match: text("match").notNull(), // CIDR, PATH, IP @@ -657,17 +667,17 @@ export const resourceRules = sqliteTable("resourceRules", { }); export const supporterKey = sqliteTable("supporterKey", { - keyId: integer("keyId").primaryKey({ autoIncrement: true }), + keyId: integer("keyId").primaryKey({autoIncrement: true}), key: text("key").notNull(), githubUsername: text("githubUsername").notNull(), phrase: text("phrase"), tier: text("tier"), - valid: integer("valid", { mode: "boolean" }).notNull().default(false) + valid: integer("valid", {mode: "boolean"}).notNull().default(false) }); // Identity Providers export const idp = sqliteTable("idp", { - idpId: integer("idpId").primaryKey({ autoIncrement: true }), + idpId: integer("idpId").primaryKey({autoIncrement: true}), name: text("name").notNull(), type: text("type").notNull(), defaultRoleMapping: text("defaultRoleMapping"), @@ -687,7 +697,7 @@ export const idpOidcConfig = sqliteTable("idpOidcConfig", { variant: text("variant").notNull().default("oidc"), idpId: integer("idpId") .notNull() - .references(() => idp.idpId, { onDelete: "cascade" }), + .references(() => idp.idpId, {onDelete: "cascade"}), clientId: text("clientId").notNull(), clientSecret: text("clientSecret").notNull(), authUrl: text("authUrl").notNull(), @@ -715,22 +725,22 @@ export const apiKeys = sqliteTable("apiKeys", { apiKeyHash: text("apiKeyHash").notNull(), lastChars: text("lastChars").notNull(), createdAt: text("dateCreated").notNull(), - isRoot: integer("isRoot", { mode: "boolean" }).notNull().default(false) + isRoot: integer("isRoot", {mode: "boolean"}).notNull().default(false) }); export const apiKeyActions = sqliteTable("apiKeyActions", { apiKeyId: text("apiKeyId") .notNull() - .references(() => apiKeys.apiKeyId, { onDelete: "cascade" }), + .references(() => apiKeys.apiKeyId, {onDelete: "cascade"}), actionId: text("actionId") .notNull() - .references(() => actions.actionId, { onDelete: "cascade" }) + .references(() => actions.actionId, {onDelete: "cascade"}) }); export const apiKeyOrg = sqliteTable("apiKeyOrg", { apiKeyId: text("apiKeyId") .notNull() - .references(() => apiKeys.apiKeyId, { onDelete: "cascade" }), + .references(() => apiKeys.apiKeyId, {onDelete: "cascade"}), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -741,10 +751,10 @@ export const apiKeyOrg = sqliteTable("apiKeyOrg", { export const idpOrg = sqliteTable("idpOrg", { idpId: integer("idpId") .notNull() - .references(() => idp.idpId, { onDelete: "cascade" }), + .references(() => idp.idpId, {onDelete: "cascade"}), orgId: text("orgId") .notNull() - .references(() => orgs.orgId, { onDelete: "cascade" }), + .references(() => orgs.orgId, {onDelete: "cascade"}), roleMapping: text("roleMapping"), orgMapping: text("orgMapping") }); @@ -762,19 +772,19 @@ export const blueprints = sqliteTable("blueprints", { name: text("name").notNull(), source: text("source").notNull(), createdAt: integer("createdAt").notNull(), - succeeded: integer("succeeded", { mode: "boolean" }).notNull(), + succeeded: integer("succeeded", {mode: "boolean"}).notNull(), contents: text("contents").notNull(), message: text("message") }); export const requestAuditLog = sqliteTable( "requestAuditLog", { - id: integer("id").primaryKey({ autoIncrement: true }), + id: integer("id").primaryKey({autoIncrement: true}), timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds orgId: text("orgId").references(() => orgs.orgId, { onDelete: "cascade" }), - action: integer("action", { mode: "boolean" }).notNull(), + action: integer("action", {mode: "boolean"}).notNull(), reason: integer("reason").notNull(), actorType: text("actorType"), actor: text("actor"), @@ -791,7 +801,7 @@ export const requestAuditLog = sqliteTable( host: text("host"), path: text("path"), method: text("method"), - tls: integer("tls", { mode: "boolean" }) + tls: integer("tls", {mode: "boolean"}) }, (table) => [ index("idx_requestAuditLog_timestamp").on(table.timestamp), @@ -832,6 +842,7 @@ export type ResourceSession = InferSelectModel; export type ResourcePincode = InferSelectModel; export type ResourcePassword = InferSelectModel; export type ResourceHeaderAuth = InferSelectModel; +export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel; export type ResourceOtp = InferSelectModel; export type ResourceAccessToken = InferSelectModel; export type ResourceWhitelist = InferSelectModel; diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index d85befed4..f6a40438b 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -2,7 +2,7 @@ import { domains, orgDomains, Resource, - resourceHeaderAuth, + resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility, resourcePincode, resourceRules, resourceWhitelist, @@ -16,8 +16,8 @@ import { userResources, users } from "@server/db"; -import { resources, targets, sites } from "@server/db"; -import { eq, and, asc, or, ne, count, isNotNull } from "drizzle-orm"; +import {resources, targets, sites} from "@server/db"; +import {eq, and, asc, or, ne, count, isNotNull} from "drizzle-orm"; import { Config, ConfigSchema, @@ -25,12 +25,12 @@ import { TargetData } from "./types"; import logger from "@server/logger"; -import { createCertificate } from "#dynamic/routers/certificates/createCertificate"; -import { pickPort } from "@server/routers/target/helpers"; -import { resourcePassword } from "@server/db"; -import { hashPassword } from "@server/auth/password"; -import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; -import { get } from "http"; +import {createCertificate} from "#dynamic/routers/certificates/createCertificate"; +import {pickPort} from "@server/routers/target/helpers"; +import {resourcePassword} from "@server/db"; +import {hashPassword} from "@server/auth/password"; +import {isValidCIDR, isValidIP, isValidUrlGlobPattern} from "../validators"; +import {get} from "http"; export type ProxyResourcesResults = { proxyResource: Resource; @@ -63,7 +63,7 @@ export async function updateProxyResources( if (targetSiteId) { // Look up site by niceId [site] = await trx - .select({ siteId: sites.siteId }) + .select({siteId: sites.siteId}) .from(sites) .where( and( @@ -75,7 +75,7 @@ export async function updateProxyResources( } else if (siteId) { // Use the provided siteId directly, but verify it belongs to the org [site] = await trx - .select({ siteId: sites.siteId }) + .select({siteId: sites.siteId}) .from(sites) .where( and(eq(sites.siteId, siteId), eq(sites.orgId, orgId)) @@ -93,7 +93,7 @@ export async function updateProxyResources( let internalPortToCreate; if (!targetData["internal-port"]) { - const { internalPort, targetIps } = await pickPort( + const {internalPort, targetIps} = await pickPort( site.siteId!, trx ); @@ -226,7 +226,7 @@ export async function updateProxyResources( tlsServerName: resourceData["tls-server-name"] || null, emailWhitelistEnabled: resourceData.auth?.[ "whitelist-users" - ] + ] ? resourceData.auth["whitelist-users"].length > 0 : false, headers: headers || null, @@ -285,21 +285,39 @@ export async function updateProxyResources( existingResource.resourceId ) ); + + await trx + .delete(resourceHeaderAuthExtendedCompatibility) + .where( + eq( + resourceHeaderAuthExtendedCompatibility.resourceId, + existingResource.resourceId + ) + ); + if (resourceData.auth?.["basic-auth"]) { const headerAuthUser = resourceData.auth?.["basic-auth"]?.user; const headerAuthPassword = resourceData.auth?.["basic-auth"]?.password; - if (headerAuthUser && headerAuthPassword) { + const headerAuthExtendedCompatibility = + resourceData.auth?.["basic-auth"]?.extendedCompatibility; + if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) { const headerAuthHash = await hashPassword( Buffer.from( `${headerAuthUser}:${headerAuthPassword}` ).toString("base64") ); - await trx.insert(resourceHeaderAuth).values({ - resourceId: existingResource.resourceId, - headerAuthHash - }); + await Promise.all([ + trx.insert(resourceHeaderAuth).values({ + resourceId: existingResource.resourceId, + headerAuthHash + }), + trx.insert(resourceHeaderAuthExtendedCompatibility).values({ + resourceId: existingResource.resourceId, + extendedCompatibilityIsActivated: headerAuthExtendedCompatibility + }) + ]); } } @@ -360,7 +378,7 @@ export async function updateProxyResources( if (targetSiteId) { // Look up site by niceId [site] = await trx - .select({ siteId: sites.siteId }) + .select({siteId: sites.siteId}) .from(sites) .where( and( @@ -372,7 +390,7 @@ export async function updateProxyResources( } else if (siteId) { // Use the provided siteId directly, but verify it belongs to the org [site] = await trx - .select({ siteId: sites.siteId }) + .select({siteId: sites.siteId}) .from(sites) .where( and( @@ -417,7 +435,7 @@ export async function updateProxyResources( if (checkIfTargetChanged(existingTarget, updatedTarget)) { let internalPortToUpdate; if (!targetData["internal-port"]) { - const { internalPort, targetIps } = await pickPort( + const {internalPort, targetIps} = await pickPort( site.siteId!, trx ); @@ -646,18 +664,25 @@ export async function updateProxyResources( const headerAuthUser = resourceData.auth?.["basic-auth"]?.user; const headerAuthPassword = resourceData.auth?.["basic-auth"]?.password; + const headerAuthExtendedCompatibility = resourceData.auth?.["basic-auth"]?.extendedCompatibility; - if (headerAuthUser && headerAuthPassword) { + if (headerAuthUser && headerAuthPassword && headerAuthExtendedCompatibility !== null) { const headerAuthHash = await hashPassword( Buffer.from( `${headerAuthUser}:${headerAuthPassword}` ).toString("base64") ); - await trx.insert(resourceHeaderAuth).values({ - resourceId: newResource.resourceId, - headerAuthHash - }); + await Promise.all([ + trx.insert(resourceHeaderAuth).values({ + resourceId: newResource.resourceId, + headerAuthHash + }), + trx.insert(resourceHeaderAuthExtendedCompatibility).values({ + resourceId: newResource.resourceId, + extendedCompatibilityIsActivated: headerAuthExtendedCompatibility + }), + ]); } } @@ -1004,7 +1029,7 @@ async function getDomain( trx: Transaction ) { const [fullDomainExists] = await trx - .select({ resourceId: resources.resourceId }) + .select({resourceId: resources.resourceId}) .from(resources) .where( and( diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index a5ee5700d..67129140a 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -47,7 +47,8 @@ export const AuthSchema = z.object({ password: z.string().min(1).optional(), "basic-auth": z.object({ user: z.string().min(1), - password: z.string().min(1) + password: z.string().min(1), + extendedCompatibility: z.boolean().default(false) }).optional(), "sso-enabled": z.boolean().optional().default(false), "sso-roles": z diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index f78fb592f..1e373b730 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -36,8 +36,10 @@ import { LoginPage, resourceHeaderAuth, ResourceHeaderAuth, + resourceHeaderAuthExtendedCompatibility, + ResourceHeaderAuthExtendedCompatibility, orgs, - requestAuditLog + requestAuditLog, } from "@server/db"; import { resources, @@ -188,6 +190,7 @@ export type ResourceWithAuth = { pincode: ResourcePincode | null; password: ResourcePassword | null; headerAuth: ResourceHeaderAuth | null; + headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; }; export type UserSessionWithUser = { @@ -511,6 +514,10 @@ hybridRouter.get( resourceHeaderAuth, eq(resourceHeaderAuth.resourceId, resources.resourceId) ) + .leftJoin( + resourceHeaderAuthExtendedCompatibility, + eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId) + ) .where(eq(resources.fullDomain, domain)) .limit(1); @@ -543,7 +550,8 @@ hybridRouter.get( resource: result.resources, pincode: result.resourcePincode, password: result.resourcePassword, - headerAuth: result.resourceHeaderAuth + headerAuth: result.resourceHeaderAuth, + headerAuthExtendedCompatibility: result.resourceHeaderAuthExtendedCompatibility }; return response(res, { diff --git a/server/routers/badger/logRequestAudit.ts b/server/routers/badger/logRequestAudit.ts index 6beb52c6a..c3b2f5126 100644 --- a/server/routers/badger/logRequestAudit.ts +++ b/server/routers/badger/logRequestAudit.ts @@ -9,7 +9,7 @@ Reasons: 100 - Allowed by Rule 101 - Allowed No Auth 102 - Valid Access Token -103 - Valid header auth +103 - Valid Header Auth (HTTP Basic Auth) 104 - Valid Pincode 105 - Valid Password 106 - Valid email diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index d7fe91902..86eaadcd3 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -13,7 +13,7 @@ import { LoginPage, Org, Resource, - ResourceHeaderAuth, + ResourceHeaderAuth, ResourceHeaderAuthExtendedCompatibility, ResourcePassword, ResourcePincode, ResourceRule, @@ -65,6 +65,7 @@ type BasicUserData = { export type VerifyUserResponse = { valid: boolean; + headerAuthChallenged?: boolean; redirectUrl?: string; userData?: BasicUserData; }; @@ -142,6 +143,7 @@ export async function verifyResourceSession( pincode: ResourcePincode | null; password: ResourcePassword | null; headerAuth: ResourceHeaderAuth | null; + headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; org: Org; } | undefined = cache.get(resourceCacheKey); @@ -171,7 +173,7 @@ export async function verifyResourceSession( cache.set(resourceCacheKey, resourceData, 5); } - const { resource, pincode, password, headerAuth } = resourceData; + const { resource, pincode, password, headerAuth, headerAuthExtendedCompatibility } = resourceData; if (!resource) { logger.debug(`Resource not found ${cleanHost}`); @@ -450,7 +452,8 @@ export async function verifyResourceSession( !sso && !pincode && !password && - !resource.emailWhitelistEnabled + !resource.emailWhitelistEnabled && + !headerAuthExtendedCompatibility?.extendedCompatibilityIsActivated ) { logRequestAudit( { @@ -465,13 +468,15 @@ export async function verifyResourceSession( return notAllowed(res); } - } else if (headerAuth) { + } + else if (headerAuth) { // if there are no other auth methods we need to return unauthorized if nothing is provided if ( !sso && !pincode && !password && - !resource.emailWhitelistEnabled + !resource.emailWhitelistEnabled && + !headerAuthExtendedCompatibility?.extendedCompatibilityIsActivated ) { logRequestAudit( { @@ -557,7 +562,7 @@ export async function verifyResourceSession( } if (resourceSession) { - // only run this check if not SSO sesion; SSO session length is checked later + // only run this check if not SSO session; SSO session length is checked later const accessPolicy = await enforceResourceSessionLength( resourceSession, resourceData.org @@ -701,6 +706,11 @@ export async function verifyResourceSession( } } + // If headerAuthExtendedCompatibility is activated but no clientHeaderAuth provided, force client to challenge + if (headerAuthExtendedCompatibility && headerAuthExtendedCompatibility.extendedCompatibilityIsActivated && !clientHeaderAuth){ + return headerAuthChallenged(res, redirectPath, resource.orgId); + } + logger.debug("No more auth to check, resource not allowed"); if (config.getRawConfig().app.log_failed_attempts) { @@ -833,6 +843,46 @@ function allowed(res: Response, userData?: BasicUserData) { return response(res, data); } +async function headerAuthChallenged( + res: Response, + redirectPath?: string, + orgId?: string +) { + let loginPage: LoginPage | null = null; + if (orgId) { + const { tier } = await getOrgTierData(orgId); // returns null in oss + if (tier === TierId.STANDARD) { + loginPage = await getOrgLoginPage(orgId); + } + } + + let redirectUrl: string | undefined = undefined; + if (redirectPath) { + let endpoint: string; + + if (loginPage && loginPage.domainId && loginPage.fullDomain) { + const secure = config + .getRawConfig() + .app.dashboard_url?.startsWith("https"); + const method = secure ? "https" : "http"; + endpoint = `${method}://${loginPage.fullDomain}`; + } else { + endpoint = config.getRawConfig().app.dashboard_url!; + } + redirectUrl = `${endpoint}${redirectPath}`; + } + + const data = { + data: { headerAuthChallenged: true, valid: false, redirectUrl }, + success: true, + error: false, + message: "Access denied", + status: HttpCode.OK + }; + logger.debug(JSON.stringify(data)); + return response(res, data); +} + async function isUserAllowedToAccessResource( userSessionId: string, resource: Resource, diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index 60f8e5862..5602a909b 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -1,23 +1,23 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; +import {Request, Response, NextFunction} from "express"; +import {z} from "zod"; import { db, - resourceHeaderAuth, + resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility, resourcePassword, resourcePincode, resources } from "@server/db"; -import { eq } from "drizzle-orm"; +import {eq} from "drizzle-orm"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { fromError } from "zod-validation-error"; +import {fromError} from "zod-validation-error"; import logger from "@server/logger"; -import { build } from "@server/build"; +import {build} from "@server/build"; const getResourceAuthInfoSchema = z.strictObject({ - resourceGuid: z.string() - }); + resourceGuid: z.string() +}); export type GetResourceAuthInfoResponse = { resourceId: number; @@ -27,6 +27,7 @@ export type GetResourceAuthInfoResponse = { password: boolean; pincode: boolean; headerAuth: boolean; + headerAuthExtendedCompatibility: boolean; sso: boolean; blockAccess: boolean; url: string; @@ -51,54 +52,68 @@ export async function getResourceAuthInfo( ); } - const { resourceGuid } = parsedParams.data; + const {resourceGuid} = parsedParams.data; const isGuidInteger = /^\d+$/.test(resourceGuid); const [result] = isGuidInteger && build === "saas" ? await db - .select() - .from(resources) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) - ) + .select() + .from(resources) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId) + ) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourceId, resources.resourceId) + ) - .leftJoin( - resourceHeaderAuth, - eq( - resourceHeaderAuth.resourceId, - resources.resourceId - ) - ) - .where(eq(resources.resourceId, Number(resourceGuid))) - .limit(1) + .leftJoin( + resourceHeaderAuth, + eq( + resourceHeaderAuth.resourceId, + resources.resourceId + ) + ) + .leftJoin( + resourceHeaderAuthExtendedCompatibility, + eq( + resourceHeaderAuthExtendedCompatibility.resourceId, + resources.resourceId + ) + ) + .where(eq(resources.resourceId, Number(resourceGuid))) + .limit(1) : await db - .select() - .from(resources) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) - ) + .select() + .from(resources) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId) + ) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourceId, resources.resourceId) + ) - .leftJoin( - resourceHeaderAuth, - eq( - resourceHeaderAuth.resourceId, - resources.resourceId - ) - ) - .where(eq(resources.resourceGuid, resourceGuid)) - .limit(1); + .leftJoin( + resourceHeaderAuth, + eq( + resourceHeaderAuth.resourceId, + resources.resourceId + ) + ) + .leftJoin( + resourceHeaderAuthExtendedCompatibility, + eq( + resourceHeaderAuthExtendedCompatibility.resourceId, + resources.resourceId + ) + ) + .where(eq(resources.resourceGuid, resourceGuid)) + .limit(1); const resource = result?.resources; if (!resource) { @@ -110,6 +125,7 @@ export async function getResourceAuthInfo( const pincode = result?.resourcePincode; const password = result?.resourcePassword; const headerAuth = result?.resourceHeaderAuth; + const headerAuthExtendedCompatibility = result?.resourceHeaderAuthExtendedCompatibility; const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; @@ -122,6 +138,7 @@ export async function getResourceAuthInfo( password: password !== null, pincode: pincode !== null, headerAuth: headerAuth !== null, + headerAuthExtendedCompatibility: headerAuthExtendedCompatibility !== null, sso: resource.sso, blockAccess: resource.blockAccess, url, diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index a72dd7634..6d95ec591 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, resourceHeaderAuth } from "@server/db"; +import {db, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility} from "@server/db"; import { resources, userResources, @@ -67,7 +67,7 @@ type JoinedRow = { hcEnabled: boolean | null; }; -// grouped by resource with targets[]) +// grouped by resource with targets[]) export type ResourceWithTargets = { resourceId: number; name: string; @@ -111,7 +111,7 @@ function queryResources(accessibleResourceIds: number[], orgId: string) { domainId: resources.domainId, niceId: resources.niceId, headerAuthId: resourceHeaderAuth.headerAuthId, - + headerAuthExtendedCompatibilityId: resourceHeaderAuthExtendedCompatibility.headerAuthExtendedCompatibilityId, targetId: targets.targetId, targetIp: targets.ip, targetPort: targets.port, @@ -133,6 +133,10 @@ function queryResources(accessibleResourceIds: number[], orgId: string) { resourceHeaderAuth, eq(resourceHeaderAuth.resourceId, resources.resourceId) ) + .leftJoin( + resourceHeaderAuthExtendedCompatibility, + eq(resourceHeaderAuthExtendedCompatibility.resourceId, resources.resourceId) + ) .leftJoin(targets, eq(targets.resourceId, resources.resourceId)) .leftJoin( targetHealthCheck, diff --git a/server/routers/resource/setResourceHeaderAuth.ts b/server/routers/resource/setResourceHeaderAuth.ts index 87ffbacd5..6c8665789 100644 --- a/server/routers/resource/setResourceHeaderAuth.ts +++ b/server/routers/resource/setResourceHeaderAuth.ts @@ -1,23 +1,24 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db, resourceHeaderAuth } from "@server/db"; -import { eq } from "drizzle-orm"; +import {Request, Response, NextFunction} from "express"; +import {z} from "zod"; +import {db, resourceHeaderAuth, resourceHeaderAuthExtendedCompatibility} from "@server/db"; +import {eq} from "drizzle-orm"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { fromError } from "zod-validation-error"; -import { response } from "@server/lib/response"; +import {fromError} from "zod-validation-error"; +import {response} from "@server/lib/response"; import logger from "@server/logger"; -import { hashPassword } from "@server/auth/password"; -import { OpenAPITags, registry } from "@server/openApi"; +import {hashPassword} from "@server/auth/password"; +import {OpenAPITags, registry} from "@server/openApi"; const setResourceAuthMethodsParamsSchema = z.object({ resourceId: z.string().transform(Number).pipe(z.int().positive()) }); const setResourceAuthMethodsBodySchema = z.strictObject({ - user: z.string().min(4).max(100).nullable(), - password: z.string().min(4).max(100).nullable() - }); + user: z.string().min(4).max(100).nullable(), + password: z.string().min(4).max(100).nullable(), + extendedCompatibility: z.boolean().nullable() +}); registry.registerPath({ method: "post", @@ -66,21 +67,29 @@ export async function setResourceHeaderAuth( ); } - const { resourceId } = parsedParams.data; - const { user, password } = parsedBody.data; + const {resourceId} = parsedParams.data; + const {user, password, extendedCompatibility} = parsedBody.data; await db.transaction(async (trx) => { await trx .delete(resourceHeaderAuth) .where(eq(resourceHeaderAuth.resourceId, resourceId)); + await trx.delete(resourceHeaderAuthExtendedCompatibility).where(eq(resourceHeaderAuthExtendedCompatibility.resourceId, resourceId)); - if (user && password) { + if (user && password && extendedCompatibility !== null) { const headerAuthHash = await hashPassword(Buffer.from(`${user}:${password}`).toString("base64")); - await trx - .insert(resourceHeaderAuth) - .values({ resourceId, headerAuthHash }); + await Promise.all([ + trx + .insert(resourceHeaderAuth) + .values({resourceId, headerAuthHash}), + trx + .insert(resourceHeaderAuthExtendedCompatibility) + .values({resourceId, extendedCompatibilityIsActivated: extendedCompatibility}) + ]); } + + }); return response(res, { diff --git a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx index fe5f0ca26..fa4825aec 100644 --- a/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[niceId]/authentication/page.tsx @@ -439,7 +439,8 @@ export default function ResourceAuthenticationPage() { api.post(`/resource/${resource.resourceId}/header-auth`, { user: null, - password: null + password: null, + extendedCompatibility: null, }) .then(() => { toast({ diff --git a/src/components/SetResourceHeaderAuthForm.tsx b/src/components/SetResourceHeaderAuthForm.tsx index b1a75543f..00cd02b10 100644 --- a/src/components/SetResourceHeaderAuthForm.tsx +++ b/src/components/SetResourceHeaderAuthForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { Button } from "@app/components/ui/button"; +import {Button} from "@app/components/ui/button"; import { Form, FormControl, @@ -9,12 +9,12 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; -import { Input } from "@app/components/ui/input"; -import { toast } from "@app/hooks/useToast"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; +import {Input} from "@app/components/ui/input"; +import {toast} from "@app/hooks/useToast"; +import {zodResolver} from "@hookform/resolvers/zod"; +import {useEffect, useState} from "react"; +import {useForm} from "react-hook-form"; +import {z} from "zod"; import { Credenza, CredenzaBody, @@ -25,23 +25,27 @@ import { CredenzaHeader, CredenzaTitle } from "@app/components/Credenza"; -import { formatAxiosError } from "@app/lib/api"; -import { AxiosResponse } from "axios"; -import { Resource } from "@server/db"; -import { createApiClient } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useTranslations } from "next-intl"; +import {formatAxiosError} from "@app/lib/api"; +import {AxiosResponse} from "axios"; +import {Resource} from "@server/db"; +import {createApiClient} from "@app/lib/api"; +import {useEnvContext} from "@app/hooks/useEnvContext"; +import {useTranslations} from "next-intl"; +import {SwitchInput} from "@/components/SwitchInput"; +import {InfoPopup} from "@/components/ui/info-popup"; const setHeaderAuthFormSchema = z.object({ user: z.string().min(4).max(100), - password: z.string().min(4).max(100) + password: z.string().min(4).max(100), + extendedCompatibility: z.boolean() }); type SetHeaderAuthFormValues = z.infer; const defaultValues: Partial = { user: "", - password: "" + password: "", + extendedCompatibility: false }; type SetHeaderAuthFormProps = { @@ -52,11 +56,11 @@ type SetHeaderAuthFormProps = { }; export default function SetResourceHeaderAuthForm({ - open, - setOpen, - resourceId, - onSetHeaderAuth -}: SetHeaderAuthFormProps) { + open, + setOpen, + resourceId, + onSetHeaderAuth + }: SetHeaderAuthFormProps) { const api = createApiClient(useEnvContext()); const t = useTranslations(); @@ -80,18 +84,9 @@ export default function SetResourceHeaderAuthForm({ api.post>(`/resource/${resourceId}/header-auth`, { user: data.user, - password: data.password + password: data.password, + extendedCompatibility: data.extendedCompatibility }) - .catch((e) => { - toast({ - variant: "destructive", - title: t('resourceErrorHeaderAuthSetup'), - description: formatAxiosError( - e, - t('resourceErrorHeaderAuthSetupDescription') - ) - }); - }) .then(() => { toast({ title: t('resourceHeaderAuthSetup'), @@ -102,6 +97,16 @@ export default function SetResourceHeaderAuthForm({ onSetHeaderAuth(); } }) + .catch((e) => { + toast({ + variant: "destructive", + title: t('resourceErrorHeaderAuthSetup'), + description: formatAxiosError( + e, + t('resourceErrorHeaderAuthSetupDescription') + ) + }); + }) .finally(() => setLoading(false)); } @@ -132,7 +137,7 @@ export default function SetResourceHeaderAuthForm({ ( + render={({field}) => ( {t('user')} @@ -142,14 +147,14 @@ export default function SetResourceHeaderAuthForm({ {...field} /> - + )} /> ( + render={({field}) => ( {t('password')} @@ -159,7 +164,25 @@ export default function SetResourceHeaderAuthForm({ {...field} /> - + + + )} + /> + ( + + + + + )} /> diff --git a/src/components/SetResourcePasswordForm.tsx b/src/components/SetResourcePasswordForm.tsx index 07146865c..2fef13805 100644 --- a/src/components/SetResourcePasswordForm.tsx +++ b/src/components/SetResourcePasswordForm.tsx @@ -78,16 +78,6 @@ export default function SetResourcePasswordForm({ api.post>(`/resource/${resourceId}/password`, { password: data.password }) - .catch((e) => { - toast({ - variant: "destructive", - title: t('resourceErrorPasswordSetup'), - description: formatAxiosError( - e, - t('resourceErrorPasswordSetupDescription') - ) - }); - }) .then(() => { toast({ title: t('resourcePasswordSetup'), @@ -98,6 +88,16 @@ export default function SetResourcePasswordForm({ onSetPassword(); } }) + .catch((e) => { + toast({ + variant: "destructive", + title: t('resourceErrorPasswordSetup'), + description: formatAxiosError( + e, + t('resourceErrorPasswordSetupDescription') + ) + }); + }) .finally(() => setLoading(false)); } diff --git a/src/components/SetResourcePincodeForm.tsx b/src/components/SetResourcePincodeForm.tsx index d58d0c85d..5306a2978 100644 --- a/src/components/SetResourcePincodeForm.tsx +++ b/src/components/SetResourcePincodeForm.tsx @@ -84,16 +84,6 @@ export default function SetResourcePincodeForm({ api.post>(`/resource/${resourceId}/pincode`, { pincode: data.pincode }) - .catch((e) => { - toast({ - variant: "destructive", - title: t('resourceErrorPincodeSetup'), - description: formatAxiosError( - e, - t('resourceErrorPincodeSetupDescription') - ) - }); - }) .then(() => { toast({ title: t('resourcePincodeSetup'), @@ -104,6 +94,16 @@ export default function SetResourcePincodeForm({ onSetPincode(); } }) + .catch((e) => { + toast({ + variant: "destructive", + title: t('resourceErrorPincodeSetup'), + description: formatAxiosError( + e, + t('resourceErrorPincodeSetupDescription') + ) + }); + }) .finally(() => setLoading(false)); } diff --git a/src/components/SwitchInput.tsx b/src/components/SwitchInput.tsx index a2291c2ec..e76b52770 100644 --- a/src/components/SwitchInput.tsx +++ b/src/components/SwitchInput.tsx @@ -1,11 +1,16 @@ import React from "react"; -import { Switch } from "./ui/switch"; -import { Label } from "./ui/label"; +import {Switch} from "./ui/switch"; +import {Label} from "./ui/label"; +import {Button} from "@/components/ui/button"; +import {Info} from "lucide-react"; +import {info} from "winston"; +import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover"; interface SwitchComponentProps { id: string; label?: string; description?: string; + info?: string; checked?: boolean; defaultChecked?: boolean; disabled?: boolean; @@ -13,14 +18,26 @@ interface SwitchComponentProps { } export function SwitchInput({ - id, - label, - description, - disabled, - checked, - defaultChecked = false, - onCheckedChange -}: SwitchComponentProps) { + id, + label, + description, + info, + disabled, + checked, + defaultChecked = false, + onCheckedChange + }: SwitchComponentProps) { + const defaultTrigger = ( + + ); + return (
@@ -32,6 +49,18 @@ export function SwitchInput({ disabled={disabled} /> {label && } + {info && + + {defaultTrigger} + + + {info && ( +

+ {info} +

+ )} +
+
}
{description && ( From 744305ab39ef90d7cd6f22ceba75aba37be9c90d Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Fri, 5 Dec 2025 00:02:13 +0100 Subject: [PATCH 0048/1422] =?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 8a5a74011..d1008a51e 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 0049/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8Faddress=20PR=20feed?= =?UTF-8?q?back?= 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 812b94918..2914573f5 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 0364e291c..d2c167b82 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 0050/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor=20rever?= =?UTF-8?q?se=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 ba3499b55..450c43fa7 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 bb5501a67..84fa31ace 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 0052/1422] =?UTF-8?q?=F0=9F=92=AC=20update=20texts=20to=20?= =?UTF-8?q?be=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 3f20f7e64..0d5e80ea7 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 f9c8c8c2c..8cee3da01 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 0053/1422] 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 f6b21ff64..bd633707a 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 a2432e3e1..811f4402f 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 d4d9678da..900a99acc 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 0054/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20domain?= =?UTF-8?q?=20picker=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 2d17e39f2..8fc6d5836 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 de3bf023d..9e43adf75 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 0055/1422] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20refactor=20prox?= =?UTF-8?q?y=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 fa9a6976c..6712f2011 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 c453b5771..f410b4c8b 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 0056/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20domain?= =?UTF-8?q?=20picker=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 2d17e39f2..8fc6d5836 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 2a19f3f97..73e80df8f 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 0057/1422] =?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 8fc6d5836..625b566ec 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 0058/1422] =?UTF-8?q?=F0=9F=8F=B7=EF=B8=8F=20fix=20typescr?= =?UTF-8?q?ipt=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 549dbffe6..e0867dc58 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 0059/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20set=20default=20?= =?UTF-8?q?value=20on=20domain=20picker=20modal=20in=20proxy=20resource=20?= =?UTF-8?q?page?= 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 fa9a6976c..7d4db07c5 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 0060/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20use=20react=20qu?= =?UTF-8?q?erty?= 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 27f4fd739..340870769 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 0065/1422] =?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 15c8e1970..8de6aa13b 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 a767a31c21595027a6285b4db446f5e60e7053b2 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 13 Dec 2025 12:28:44 -0500 Subject: [PATCH 0066/1422] 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 8060ccad2..825682167 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 0067/1422] 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 9993e117d..464186c22 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 97631c068cfe78f12e0ae08a04a920201c0e51a9 Mon Sep 17 00:00:00 2001 From: Owen Date: Sun, 14 Dec 2025 15:58:29 -0500 Subject: [PATCH 0068/1422] 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 ba3ab7ad8..488ef75b0 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 0069/1422] 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 64298029c..87b863219 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 0070/1422] 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 3b0627ccb..41d43bd9a 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 0071/1422] =?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 3601d6bd9..ee1a7c204 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 97dd4a03e..576589a12 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 0072/1422] =?UTF-8?q?=F0=9F=92=84=20put=20save=20org=20set?= =?UTF-8?q?tings=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 576589a12..24176dbbd 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 0073/1422] 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 c8a0b0771..32a5fb472 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 404a25014..cd37560d9 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 0074/1422] =?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 09e5d918f..ada2defed 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 0076/1422] =?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 24176dbbd..ff8a103de 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 0077/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20invalidate=20eve?= =?UTF-8?q?rything=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 8de6aa13b..7ace8450b 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 0078/1422] =?UTF-8?q?=F0=9F=90=9B=20use=20`/resource`=20in?= =?UTF-8?q?stead=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 541091f40..513e2e4fb 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 c44c1a551877a655e4e296970deb9fb4a89a89be Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 15 Dec 2025 21:02:04 -0500 Subject: [PATCH 0079/1422] 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 ff316a987..ee26c280b 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 71877f2f1..34562e7d4 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 6e17cac49..ab754bc97 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 9c4128015..2bf3e0e8d 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 e9ce8e04f..370037cb7 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 f6975cd24..4fc965331 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 92704adb4..8ecd8bf0d 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 49ccb97fe..1628e6899 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 3f65c762b..8ca1cc218 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 91ef26daf..6aad41231 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 f793d1472..cfa60463b 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 0080/1422] 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 6aad41231..adbb7f618 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 cfa60463b..b5c378f1a 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 3230f11b2..ee865eb61 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 0081/1422] 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 b7b91925c..653a25789 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 0082/1422] 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 8ecd8bf0d..fb2065c52 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 0083/1422] 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 fa2d71c03..c59490b63 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 9456effbd..2ee34da6e 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 0084/1422] 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 ee26c280b..148db379c 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 878d61fa4..6301bb6d6 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 6a000afcf..ab6a96d20 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 49a10215c..4862d780b 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 0085/1422] 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 34562e7d4..e80777542 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 ab754bc97..de8ad8d03 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 2bf3e0e8d..21c148ac8 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 370037cb7..f2e343cd7 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 4fc965331..7b2e0233c 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 fb2065c52..376d9c0ab 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 1628e6899..eacab1d23 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 8ca1cc218..758b6e129 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 adbb7f618..894315b8e 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 b5c378f1a..cfd4fbc12 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 0086/1422] =?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 ff8a103de..6bfb3013d 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 000000000..d8173e6e6 --- /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 0087/1422] 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 77e49a202..c7f2131e3 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 3852b00ed..b6dc81487 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 595b35ba8..88886cd15 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 4aa8edd7d..e164b257b 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 0088/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20pass=20the=20def?= =?UTF-8?q?ault=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 7d4db07c5..ed1080f30 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 625b566ec..5e883add7 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 0089/1422] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20pass=20default?= =?UTF-8?q?=20value=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 9b2c120ef..8d3ccab5a 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 5e883add7..36e76b2b1 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 0090/1422] 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 f2e343cd7..d2bb16651 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 376d9c0ab..17a4033e3 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 0091/1422] 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 d2bb16651..c103b09e9 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 17a4033e3..c3360e6f5 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 0092/1422] =?UTF-8?q?=F0=9F=9A=B8=20now=20the=20domain=20p?= =?UTF-8?q?icker=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 8d3ccab5a..e846ec6c3 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 36e76b2b1..ba29c029a 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 10a9764e9..bd5860a6f 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 80e13ff2d..54576c0cd 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 786b2d716..0a9eaa7d4 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 0123/1422] 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 1d705dae4..0cfd8f6f2 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 fa28dd059..7816924f6 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 5d5745c7c..88d98aa57 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 72093e0de..717a3c120 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 c30372505..f530ace10 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 0dc441478..e90f6eeab 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 0126/1422] 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 2ee34da6e..5b357d060 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 5a4aa542c..9cbc8d7be 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 f4b4444c6..cd1218ced 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 73f9fc431..602b44754 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 0127/1422] 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 c50a800b0..d2196e87d 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 10708443e..c03836160 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 0128/1422] 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 b71eb202f..e0728c940 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 e1a904ada..f2618bc24 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 000000000..dcd572fdf --- /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 000000000..6f49e036a --- /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 000000000..f69fa0610 --- /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 000000000..8320178d8 --- /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 000000000..406167580 --- /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 000000000..8ba7a2329 --- /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 71321bf84..e413207a2 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 0129/1422] 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 389f3978b..84ce34ebb 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 0134/1422] 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 e0728c940..c6028ec21 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 21c148ac8..87a0c3c6c 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 fe6106633..365bcb13a 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 f1d065665..e93af889f 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 771b0d992..cce46a017 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 36853e5c6..10a8b14e6 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 e64a4c706..ccb5c4976 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 000000000..cac8eb3f2 --- /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 000000000..78b831bc0 --- /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 000000000..51d84d362 --- /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 ca0512530..194d8b467 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 340995584..67837ec6d 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 c2ec1f5bc..e3af2d06a 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 fcb6a026d..c83b61baf 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 e5141bde7..99cc8ff48 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 122e01271..34c8c7efc 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 e983e1166afc5da7e992dce22a3aea0d28206ce5 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Sat, 20 Dec 2025 00:05:33 +0100 Subject: [PATCH 0138/1422] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20approval=20tabl?= =?UTF-8?q?es=20in=20DB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- messages/en-US.json | 1 + server/auth/actions.ts | 3 +- server/db/pg/schema/privateSchema.ts | 6 +- server/db/pg/schema/schema.ts | 8 +- server/db/sqlite/schema/schema.ts | 5 +- server/private/routers/approvals/index.ts | 14 ++ .../routers/approvals/listApprovals.ts | 134 ++++++++++++++++++ server/private/routers/external.ts | 10 ++ .../routers/loginPage/getLoginPageBranding.ts | 8 +- server/routers/role/listRoles.ts | 16 +-- .../settings/access/approvals/page.tsx | 5 + src/app/navigation.tsx | 43 +++--- src/components/DismissableBanner.tsx | 7 +- 13 files changed, 220 insertions(+), 40 deletions(-) create mode 100644 server/private/routers/approvals/index.ts create mode 100644 server/private/routers/approvals/listApprovals.ts create mode 100644 src/app/[orgId]/settings/access/approvals/page.tsx diff --git a/messages/en-US.json b/messages/en-US.json index e0728c940..ca5b53b38 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1184,6 +1184,7 @@ "sidebarOverview": "Overview", "sidebarHome": "Home", "sidebarSites": "Sites", + "sidebarApprovals": "Approval Requests", "sidebarResources": "Resources", "sidebarProxyResources": "Public", "sidebarClientResources": "Private", diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 71017f8d9..17ad7cda8 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -125,7 +125,8 @@ export enum ActionsEnum { getBlueprint = "getBlueprint", applyBlueprint = "applyBlueprint", viewLogs = "viewLogs", - exportLogs = "exportLogs" + exportLogs = "exportLogs", + listApprovals = "listApprovals" } export async function checkUserActionPermission( diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index 1f32f3288..7332ec732 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -307,7 +307,11 @@ export const approvals = pgTable("approvals", { .notNull(), olmId: varchar("olmId").references(() => olms.olmId, { onDelete: "cascade" - }), // olms reference user devices clients + }), // olms reference user devices clients (in this case) + userId: varchar("userId").references(() => users.userId, { + // optionally tied to a user and in this case delete when the user deletes + onDelete: "cascade" + }), decision: varchar("type") .$type<"approved" | "denied" | "pending">() .default("pending") diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index c689a35a9..c93483718 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -355,7 +355,8 @@ export const roles = pgTable("roles", { .notNull(), isAdmin: boolean("isAdmin"), name: varchar("name").notNull(), - description: varchar("description") + description: varchar("description"), + requireDeviceApproval: boolean("requireDeviceApproval").default(false) }); export const roleActions = pgTable("roleActions", { @@ -699,7 +700,10 @@ export const olms = pgTable("olms", { userId: text("userId").references(() => users.userId, { // optionally tied to a user and in this case delete when the user deletes onDelete: "cascade" - }) + }), + authorizationState: varchar("authorizationState") + .$type<"pending" | "authorized" | "denied">() + .default("authorized") }); export const olmSessions = pgTable("clientSession", { diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 848289ee1..c8d8c1143 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -503,7 +503,10 @@ export const roles = sqliteTable("roles", { .notNull(), isAdmin: integer("isAdmin", { mode: "boolean" }), name: text("name").notNull(), - description: text("description") + description: text("description"), + requireDeviceApproval: integer("requireDeviceApproval", { + mode: "boolean" + }).default(false) }); export const roleActions = sqliteTable("roleActions", { diff --git a/server/private/routers/approvals/index.ts b/server/private/routers/approvals/index.ts new file mode 100644 index 000000000..d115a054e --- /dev/null +++ b/server/private/routers/approvals/index.ts @@ -0,0 +1,14 @@ +/* + * 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 "./listApprovals"; diff --git a/server/private/routers/approvals/listApprovals.ts b/server/private/routers/approvals/listApprovals.ts new file mode 100644 index 000000000..05e3a2385 --- /dev/null +++ b/server/private/routers/approvals/listApprovals.ts @@ -0,0 +1,134 @@ +/* + * 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 logger from "@server/logger"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; + +import type { Request, Response, NextFunction } from "express"; +import { build } from "@server/build"; +import { getOrgTierData } from "@server/lib/billing"; +import { TierId } from "@server/lib/billing/tiers"; +import { approvals, db } from "@server/db"; +import { eq, sql } from "drizzle-orm"; +import response from "@server/lib/response"; + +const paramsSchema = z.strictObject({ + orgId: z.string() +}); + +const querySchema = z.strictObject({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.int().nonnegative()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.int().nonnegative()) +}); + +async function queryApprovals(orgId: string, limit: number, offset: number) { + const res = await db + .select() + .from(approvals) + .where(eq(approvals.orgId, orgId)) + .limit(limit) + .offset(offset); + return res; +} + +export type ListApprovalsResponse = { + approvals: NonNullable>>; + pagination: { total: number; limit: number; offset: number }; +}; + +export async function listApprovals( + req: Request, + res: Response, + next: NextFunction +) { + try { + const parsedParams = paramsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedQuery = querySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + const { limit, offset } = parsedQuery.data; + + 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 approvalsList = await queryApprovals( + orgId.toString(), + limit, + offset + ); + + const [{ count }] = await db + .select({ count: sql`count(*)` }) + .from(approvals); + + return response(res, { + data: { + approvals: approvalsList, + pagination: { + total: count, + limit, + offset + } + }, + success: true, + error: false, + message: "Approvals 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/external.ts b/server/private/routers/external.ts index d9608e211..9ecf57f36 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -24,6 +24,7 @@ import * as generateLicense from "./generatedLicense"; import * as logs from "#private/routers/auditLogs"; import * as misc from "#private/routers/misc"; import * as reKey from "#private/routers/re-key"; +import * as approval from "#private/routers/approvals"; import { verifyOrgAccess, @@ -311,6 +312,15 @@ authenticated.get( loginPage.getLoginPage ); +authenticated.get( + "/org/:orgId/approvals", + verifyValidLicense, + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.listApprovals), + logActionAudit(ActionsEnum.listApprovals), + approval.listApprovals +); + authenticated.get( "/org/:orgId/login-page-branding", verifyValidLicense, diff --git a/server/private/routers/loginPage/getLoginPageBranding.ts b/server/private/routers/loginPage/getLoginPageBranding.ts index 262e9ce82..8fd0772d5 100644 --- a/server/private/routers/loginPage/getLoginPageBranding.ts +++ b/server/private/routers/loginPage/getLoginPageBranding.ts @@ -29,11 +29,9 @@ 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() +}); export async function getLoginPageBranding( req: Request, diff --git a/server/routers/role/listRoles.ts b/server/routers/role/listRoles.ts index 288a540d1..cf6b90df1 100644 --- a/server/routers/role/listRoles.ts +++ b/server/routers/role/listRoles.ts @@ -1,15 +1,13 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { roles, orgs } from "@server/db"; +import { db, orgs, roles } from "@server/db"; import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import { sql, eq } from "drizzle-orm"; import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; -import stoi from "@server/lib/stoi"; import { OpenAPITags, registry } from "@server/openApi"; +import HttpCode from "@server/types/HttpCode"; +import { eq, sql } from "drizzle-orm"; +import { NextFunction, Request, Response } from "express"; +import createHttpError from "http-errors"; +import { z } from "zod"; +import { fromError } from "zod-validation-error"; const listRolesParamsSchema = z.strictObject({ orgId: z.string() diff --git a/src/app/[orgId]/settings/access/approvals/page.tsx b/src/app/[orgId]/settings/access/approvals/page.tsx new file mode 100644 index 000000000..5674a7076 --- /dev/null +++ b/src/app/[orgId]/settings/access/approvals/page.tsx @@ -0,0 +1,5 @@ +export interface ApprovalFeedPageProps {} + +export default function ApprovalFeedPage(props: ApprovalFeedPageProps) { + return <>; +} diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 54576c0cd..98bbe3078 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -1,27 +1,27 @@ import { SidebarNavItem } from "@app/components/SidebarNav"; import { build } from "@server/build"; import { - Settings, - Users, - Link as LinkIcon, - Waypoints, + ChartLine, Combine, + CreditCard, Fingerprint, + Globe, + GlobeLock, KeyRound, + Laptop, + Link as LinkIcon, + Logs, // Added from 'dev' branch + MonitorUp, + ReceiptText, + ScanEye, // Added from 'dev' branch + Server, + Settings, + SquareMousePointer, TicketCheck, User, - Globe, // Added from 'dev' branch - MonitorUp, // Added from 'dev' branch - Server, - ReceiptText, - CreditCard, - Logs, - SquareMousePointer, - ScanEye, - GlobeLock, - Smartphone, - Laptop, - ChartLine + UserCog, + Users, + Waypoints } from "lucide-react"; export type SidebarNavSection = { @@ -123,7 +123,7 @@ export const orgNavSections = (): SidebarNavSection[] => [ href: "/{orgId}/settings/access/roles", icon: }, - ...(build == "saas" + ...(build === "saas" ? [ { title: "sidebarIdentityProviders", @@ -133,6 +133,15 @@ export const orgNavSections = (): SidebarNavSection[] => [ } ] : []), + ...(build !== "oss" + ? [ + { + title: "sidebarApprovals", + href: "/{orgId}/settings/access/approvals", + icon: + } + ] + : []), { title: "sidebarShareableLinks", href: "/{orgId}/settings/share-links", diff --git a/src/components/DismissableBanner.tsx b/src/components/DismissableBanner.tsx index 6f49e036a..555fdaa47 100644 --- a/src/components/DismissableBanner.tsx +++ b/src/components/DismissableBanner.tsx @@ -64,10 +64,10 @@ export const DismissableBanner = ({ } return ( - +
{children && ( -
+
{children}
)} @@ -95,4 +95,3 @@ export const DismissableBanner = ({ }; export default DismissableBanner; - From afc19f192b112c94e89e266afc821d3c0dba38d1 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 19 Dec 2025 21:57:44 -0500 Subject: [PATCH 0139/1422] 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 49644ff6a..7c4da0bce 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 c578614d6..5d3609d57 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 042500ddd..b2939a90c 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 630d08005..e3346b4f7 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 3677f625d..bd862c117 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 08761eba4..6ec5f0b7e 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 75b97037931b1f6c0d081149edfe66d0d712c6f9 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 20 Dec 2025 11:41:23 -0500 Subject: [PATCH 0140/1422] Seperate config gen into functions --- server/routers/newt/handleGetConfigMessage.ts | 53 ++++--- server/routers/newt/handleNewtPingMessage.ts | 141 ++++++++++++++++++ .../routers/newt/handleNewtRegisterMessage.ts | 55 ++++--- server/routers/newt/index.ts | 1 + server/routers/olm/handleOlmPingMessage.ts | 11 +- .../routers/olm/handleOlmRegisterMessage.ts | 99 +++++++----- server/routers/ws/messageHandlers.ts | 4 +- server/routers/ws/types.ts | 6 +- server/routers/ws/ws.ts | 8 +- 9 files changed, 295 insertions(+), 83 deletions(-) create mode 100644 server/routers/newt/handleNewtPingMessage.ts diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index bfe14ec51..927fed503 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -7,7 +7,8 @@ import { ExitNode, exitNodes, siteResources, - clientSiteResourcesAssociationsCache + clientSiteResourcesAssociationsCache, + Site } from "@server/db"; import { clients, clientSitesAssociationsCache, Newt, sites } from "@server/db"; import { eq } from "drizzle-orm"; @@ -130,6 +131,38 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { } } + const { peers, targets } = await buildClientConfigurationForNewtClient( + site, + exitNode + ); + + // Build the configuration response + const configResponse = { + ipAddress: site.address, + peers, + targets + }; + + logger.debug("Sending config: ", configResponse); + + return { + message: { + type: "newt/wg/receive-config", + data: { + ...configResponse + } + }, + broadcast: false, + excludeSender: false, + }; +}; + +export async function buildClientConfigurationForNewtClient( + site: Site, + exitNode?: ExitNode +) { + const siteId = site.siteId; + // Get all clients connected to this site const clientsRes = await db .select() @@ -278,22 +311,8 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { targetsToSend.push(...resourceTargets); } - // Build the configuration response - const configResponse = { - ipAddress: site.address, + return { peers: validPeers, targets: targetsToSend }; - - logger.debug("Sending config: ", configResponse); - return { - message: { - type: "newt/wg/receive-config", - data: { - ...configResponse - } - }, - broadcast: false, - excludeSender: false - }; -}; +} diff --git a/server/routers/newt/handleNewtPingMessage.ts b/server/routers/newt/handleNewtPingMessage.ts new file mode 100644 index 000000000..e7dea7ce7 --- /dev/null +++ b/server/routers/newt/handleNewtPingMessage.ts @@ -0,0 +1,141 @@ +import { db } from "@server/db"; +import { disconnectClient } from "#dynamic/routers/ws"; +import { getClientConfigVersion, MessageHandler } from "@server/routers/ws"; +import { clients, Newt } from "@server/db"; +import { eq, lt, isNull, and, or } from "drizzle-orm"; +import logger from "@server/logger"; +import { validateSessionToken } from "@server/auth/sessions/app"; +import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; +import { sendTerminateClient } from "../client/terminate"; +import { encodeHexLowerCase } from "@oslojs/encoding"; +import { sha256 } from "@oslojs/crypto/sha2"; + +// Track if the offline checker interval is running +// let offlineCheckerInterval: NodeJS.Timeout | null = null; +// const OFFLINE_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds +// const OFFLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes + +/** + * Starts the background interval that checks for clients that haven't pinged recently + * and marks them as offline + */ +// export const startNewtOfflineChecker = (): void => { +// if (offlineCheckerInterval) { +// return; // Already running +// } + +// offlineCheckerInterval = setInterval(async () => { +// try { +// const twoMinutesAgo = Math.floor( +// (Date.now() - OFFLINE_THRESHOLD_MS) / 1000 +// ); + +// // TODO: WE NEED TO MAKE SURE THIS WORKS WITH DISTRIBUTED NODES ALL DOING THE SAME THING + +// // Find clients that haven't pinged in the last 2 minutes and mark them as offline +// const offlineClients = await db +// .update(clients) +// .set({ online: false }) +// .where( +// and( +// eq(clients.online, true), +// or( +// lt(clients.lastPing, twoMinutesAgo), +// isNull(clients.lastPing) +// ) +// ) +// ) +// .returning(); + +// for (const offlineClient of offlineClients) { +// logger.info( +// `Kicking offline newt client ${offlineClient.clientId} due to inactivity` +// ); + +// if (!offlineClient.newtId) { +// logger.warn( +// `Offline client ${offlineClient.clientId} has no newtId, cannot disconnect` +// ); +// continue; +// } + +// // Send a disconnect message to the client if connected +// try { +// await sendTerminateClient( +// offlineClient.clientId, +// offlineClient.newtId +// ); // terminate first +// // wait a moment to ensure the message is sent +// await new Promise((resolve) => setTimeout(resolve, 1000)); +// await disconnectClient(offlineClient.newtId); +// } catch (error) { +// logger.error( +// `Error sending disconnect to offline newt ${offlineClient.clientId}`, +// { error } +// ); +// } +// } +// } catch (error) { +// logger.error("Error in offline checker interval", { error }); +// } +// }, OFFLINE_CHECK_INTERVAL); + +// logger.debug("Started offline checker interval"); +// }; + +/** + * Stops the background interval that checks for offline clients + */ +// export const stopNewtOfflineChecker = (): void => { +// if (offlineCheckerInterval) { +// clearInterval(offlineCheckerInterval); +// offlineCheckerInterval = null; +// logger.info("Stopped offline checker interval"); +// } +// }; + +/** + * Handles ping messages from clients and responds with pong + */ +export const handleNewtPingMessage: MessageHandler = async (context) => { + const { message, client: c, sendToClient } = context; + const newt = c as Newt; + + if (!newt) { + logger.warn("Newt not found"); + return; + } + + // get the version + const configVersion = await getClientConfigVersion(newt.newtId); + + if (message.configVersion && configVersion != message.configVersion) { + logger.warn(`Newt ping with outdated config version: ${message.configVersion} (current: ${configVersion})`); + + // TODO: sync the client + } + + // try { + // // Update the client's last ping timestamp + // await db + // .update(clients) + // .set({ + // lastPing: Math.floor(Date.now() / 1000), + // online: true + // }) + // .where(eq(clients.clientId, newt.clientId)); + // } catch (error) { + // logger.error("Error handling ping message", { error }); + // } + + return { + message: { + type: "pong", + data: { + timestamp: new Date().toISOString() + } + }, + broadcast: false, + excludeSender: false + }; +}; diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index c7f2131e3..28f6e64a5 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -233,6 +233,35 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { .where(eq(newts.newtId, newt.newtId)); } + const { tcpTargets, udpTargets, validHealthCheckTargets } = + await buildTargetConfigurationForNewtClient(siteId); + + logger.debug( + `Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}` + ); + + return { + message: { + 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], + targets: { + udp: udpTargets, + tcp: tcpTargets + }, + healthCheckTargets: validHealthCheckTargets + } + }, + broadcast: false, // Send to all clients + excludeSender: false // Include sender in broadcast + }; +}; + +export async function buildTargetConfigurationForNewtClient(siteId: number) { // Get all enabled targets with their resource protocol information const allTargets = await db .select({ @@ -337,30 +366,12 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { (target) => target !== null ); - logger.debug( - `Sending health check targets to newt ${newt.newtId}: ${JSON.stringify(validHealthCheckTargets)}` - ); - return { - message: { - 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], - targets: { - udp: udpTargets, - tcp: tcpTargets - }, - healthCheckTargets: validHealthCheckTargets - } - }, - broadcast: false, // Send to all clients - excludeSender: false // Include sender in broadcast + validHealthCheckTargets, + tcpTargets, + udpTargets }; -}; +} async function getUniqueSubnetForSite( exitNode: ExitNode, diff --git a/server/routers/newt/index.ts b/server/routers/newt/index.ts index 6b17f3249..8ff1b61ae 100644 --- a/server/routers/newt/index.ts +++ b/server/routers/newt/index.ts @@ -6,3 +6,4 @@ export * from "./handleGetConfigMessage"; export * from "./handleSocketMessages"; export * from "./handleNewtPingRequestMessage"; export * from "./handleApplyBlueprintMessage"; +export * from "./handleNewtPingMessage"; diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts index 0fa490c82..46b071c99 100644 --- a/server/routers/olm/handleOlmPingMessage.ts +++ b/server/routers/olm/handleOlmPingMessage.ts @@ -1,6 +1,6 @@ import { db } from "@server/db"; import { disconnectClient } from "#dynamic/routers/ws"; -import { MessageHandler } from "@server/routers/ws"; +import { getClientConfigVersion, MessageHandler } from "@server/routers/ws"; import { clients, Olm } from "@server/db"; import { eq, lt, isNull, and, or } from "drizzle-orm"; import logger from "@server/logger"; @@ -108,6 +108,15 @@ export const handleOlmPingMessage: MessageHandler = async (context) => { return; } + // get the version + const configVersion = await getClientConfigVersion(olm.olmId); + + if (message.configVersion && configVersion != message.configVersion) { + logger.warn(`Olm ping with outdated config version: ${message.configVersion} (current: ${configVersion})`); + + // TODO: sync the client + } + if (olm.userId) { // we need to check a user token to make sure its still valid const { session: userSession, user } = diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 0f71ee8b3..a662383a5 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -1,4 +1,5 @@ import { + Client, clientSiteResourcesAssociationsCache, db, orgs, @@ -13,7 +14,7 @@ import { olms, sites } from "@server/db"; -import { and, eq, inArray, isNull } from "drizzle-orm"; +import { and, count, eq, inArray, isNull } from "drizzle-orm"; import { addPeer, deletePeer } from "../newt/peers"; import logger from "@server/logger"; import { generateAliasConfig } from "@server/lib/ip"; @@ -144,6 +145,64 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); } + // Get all sites data + const sitesCountResult = await db + .select({ count: count() }) + .from(sites) + .innerJoin( + clientSitesAssociationsCache, + eq(sites.siteId, clientSitesAssociationsCache.siteId) + ) + .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); + + // Extract the count value from the result array + const sitesCount = sitesCountResult.length > 0 ? sitesCountResult[0].count : 0; + + // Prepare an array to store site configurations + logger.debug( + `Found ${sitesCount} sites for client ${client.clientId}` + ); + + // this prevents us from accepting a register from an olm that has not hole punched yet. + // the olm will pump the register so we can keep checking + // TODO: I still think there is a better way to do this rather than locking it out here but ??? + if (now - (client.lastHolePunch || 0) > 5 && sitesCount > 0) { + logger.warn( + "Client last hole punch is too old and we have sites to send; skipping this register" + ); + return; + } + + const siteConfigurations = await buildSiteConfigurationForOlmClient(client, publicKey, relay); + + // REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES + // if (siteConfigurations.length === 0) { + // logger.warn("No valid site configurations found"); + // return; + // } + + // Return connect message with all site configurations + return { + message: { + type: "olm/wg/connect", + data: { + sites: siteConfigurations, + tunnelIP: client.subnet, + utilitySubnet: org.utilitySubnet + } + }, + broadcast: false, + excludeSender: false + }; +}; + +export async function buildSiteConfigurationForOlmClient( + client: Client, + publicKey: string, + relay: boolean +) { + const siteConfigurations = []; + // Get all sites data const sitesData = await db .select() @@ -154,22 +213,6 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { ) .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); - // Prepare an array to store site configurations - const siteConfigurations = []; - logger.debug( - `Found ${sitesData.length} sites for client ${client.clientId}` - ); - - // this prevents us from accepting a register from an olm that has not hole punched yet. - // the olm will pump the register so we can keep checking - // TODO: I still think there is a better way to do this rather than locking it out here but ??? - if (now - (client.lastHolePunch || 0) > 5 && sitesData.length > 0) { - logger.warn( - "Client last hole punch is too old and we have sites to send; skipping this register" - ); - return; - } - // Process each site for (const { sites: site, @@ -289,23 +332,5 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { }); } - // REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES - // if (siteConfigurations.length === 0) { - // logger.warn("No valid site configurations found"); - // return; - // } - - // Return connect message with all site configurations - return { - message: { - type: "olm/wg/connect", - data: { - sites: siteConfigurations, - tunnelIP: client.subnet, - utilitySubnet: org.utilitySubnet - } - }, - broadcast: false, - excludeSender: false - }; -}; + return siteConfigurations; +} diff --git a/server/routers/ws/messageHandlers.ts b/server/routers/ws/messageHandlers.ts index acd1aef00..bcf0b4dc5 100644 --- a/server/routers/ws/messageHandlers.ts +++ b/server/routers/ws/messageHandlers.ts @@ -5,7 +5,8 @@ import { handleDockerStatusMessage, handleDockerContainersMessage, handleNewtPingRequestMessage, - handleApplyBlueprintMessage + handleApplyBlueprintMessage, + handleNewtPingMessage } from "../newt"; import { handleOlmRegisterMessage, @@ -24,6 +25,7 @@ export const messageHandlers: Record = { "olm/wg/relay": handleOlmRelayMessage, "olm/wg/unrelay": handleOlmUnRelayMessage, "olm/ping": handleOlmPingMessage, + "newt/ping": handleNewtPingMessage, "newt/wg/register": handleNewtRegisterMessage, "newt/wg/get-config": handleGetConfigMessage, "newt/receive-bandwidth": handleReceiveBandwidthMessage, diff --git a/server/routers/ws/types.ts b/server/routers/ws/types.ts index 5cca3c091..81d3bd49c 100644 --- a/server/routers/ws/types.ts +++ b/server/routers/ws/types.ts @@ -52,7 +52,11 @@ export interface HandlerContext { senderWs: WebSocket; client: Newt | Olm | RemoteExitNode | undefined; clientType: ClientType; - sendToClient: (clientId: string, message: WSMessage, options?: SendMessageOptions) => Promise; + sendToClient: ( + clientId: string, + message: WSMessage, + options?: SendMessageOptions + ) => Promise; broadcastToAllExcept: ( message: WSMessage, excludeClientId?: string, diff --git a/server/routers/ws/ws.ts b/server/routers/ws/ws.ts index f707848c2..063202dbe 100644 --- a/server/routers/ws/ws.ts +++ b/server/routers/ws/ws.ts @@ -95,7 +95,7 @@ const sendToClientLocal = async ( if (!clients || clients.length === 0) { return false; } - + // Increment config version if requested if (options.incrementConfigVersion) { const currentVersion = clientConfigVersions.get(clientId) || 0; @@ -106,14 +106,14 @@ const sendToClientLocal = async ( client.configVersion = newVersion; }); } - + // Include config version in message const configVersion = clientConfigVersions.get(clientId) || 0; const messageWithVersion = { ...message, configVersion }; - + const messageString = JSON.stringify(messageWithVersion); clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { @@ -189,7 +189,7 @@ const hasActiveConnections = async (clientId: string): Promise => { }; // Get the current config version for a client -const getClientConfigVersion = (clientId: string): number => { +const getClientConfigVersion = async (clientId: string): Promise => { return clientConfigVersions.get(clientId) || 0; }; From 9ffa39141695d62c7efa8f59e7356b0e5c7e6ca8 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Sat, 20 Dec 2025 12:00:58 -0500 Subject: [PATCH 0141/1422] 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 7ef778076..bd6327fd2 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 e3af2d06a..b855683a2 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 048b3bdc2..02a8dde12 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 0142/1422] 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 b3a18c317..5206a52b4 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 267c78eb6..bc9d84ff5 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 0143/1422] 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 5206a52b4..e4a23ed4c 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 bc9d84ff5..c0c4caecc 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 0144/1422] 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 e4a23ed4c..8cdfda256 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 c0c4caecc..af6a8ddef 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 0145/1422] 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 000000000..f78577f5f --- /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 000000000..13951262e --- /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 000000000..18a39c464 --- /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 9874518e5..405db2d14 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 365bcb13a..da5678207 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 d7fe91902..0e3a3489c 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 3f86665b6..a516d14af 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 cae3f16e9..b443bd1c2 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 a58953e7a..003b7f0e8 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 4e7e29810..dbe47bd53 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 d4b62d102..e40ac5d3b 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 0146/1422] 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 0f0edded9..2cd487fbe 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 0147/1422] 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 3accc5007..751a1a0cb 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 0148/1422] 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 2cd487fbe..df6d7bb01 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), From 7e047d9e340bc284919253a88d77e994ecbc1101 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 20 Dec 2025 15:40:11 -0500 Subject: [PATCH 0149/1422] db schema for maintenance --- server/db/sqlite/schema/schema.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index e9f81b179..1725e5d39 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -147,10 +147,19 @@ 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" }) + proxyProtocol: integer("proxyProtocol", { mode: "boolean" }).notNull().default(false), + proxyProtocolVersion: integer("proxyProtocolVersion").default(1), + + maintenanceModeEnabled: integer("maintenanceModeEnabled", { mode: "boolean" }) .notNull() .default(false), - proxyProtocolVersion: integer("proxyProtocolVersion").default(1) + maintenanceModeType: text("maintenanceModeType", { + enum: ["forced", "automatic"] + }).default("forced"), // "forced" = always show, "automatic" = only when down + maintenanceTitle: text("maintenanceTitle"), + maintenanceMessage: text("maintenanceMessage"), + maintenanceEstimatedTime: text("maintenanceEstimatedTime"), + }); export const targets = sqliteTable("targets", { From d2fa55dd1125c0c919f48e9b86cfe2abe2452690 Mon Sep 17 00:00:00 2001 From: Owen Date: Sat, 20 Dec 2025 15:40:29 -0500 Subject: [PATCH 0150/1422] ui to enable down for maintenance screen --- .../resources/[niceId]/general/page.tsx | 720 ++++++++++++++++++ 1 file changed, 720 insertions(+) create mode 100644 src/app/[orgId]/settings/resources/[niceId]/general/page.tsx diff --git a/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx new file mode 100644 index 000000000..876921124 --- /dev/null +++ b/src/app/[orgId]/settings/resources/[niceId]/general/page.tsx @@ -0,0 +1,720 @@ +"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, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +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 { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import DomainPicker from "@app/components/DomainPicker"; +import { AlertCircle, Globe } from "lucide-react"; +import { build } from "@server/build"; +import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; +import { DomainRow } from "../../../../../../components/DomainsTable"; +import { toASCII, toUnicode } from "punycode"; +import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext"; +import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext"; +import { useUserContext } from "@app/hooks/useUserContext"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; +import { Separator } from "@app/components/ui/separator"; +import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; + +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(); + + const orgId = params.orgId; + + 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 || "")}` + ); + const [selectedDomain, setSelectedDomain] = useState<{ + domainId: string; + subdomain?: string; + fullDomain: string; + baseDomain: string; + } | null>(null); + + const GeneralFormSchema = z + .object({ + enabled: z.boolean(), + subdomain: z.string().optional(), + name: z.string().min(1).max(255), + niceId: z.string().min(1).max(255).optional(), + domainId: z.string().optional(), + proxyPort: z.number().int().min(1).max(65535).optional(), + // enableProxy: z.boolean().optional() + maintenanceModeEnabled: z.boolean().optional(), + maintenanceModeType: z.enum(["forced", "automatic"]).optional(), + maintenanceTitle: z.string().max(255).optional(), + maintenanceMessage: z.string().max(2000).optional(), + maintenanceEstimatedTime: z.string().max(100).optional(), + }) + .refine( + (data) => { + // For non-HTTP resources, proxyPort should be defined + if (!resource.http) { + return data.proxyPort !== undefined; + } + // For HTTP resources, proxyPort should be undefined + return data.proxyPort === undefined; + }, + { + message: !resource.http + ? "Port number is required for non-HTTP resources" + : "Port number should not be set for HTTP resources", + path: ["proxyPort"] + } + ); + + type GeneralFormValues = z.infer; + + const form = useForm({ + resolver: zodResolver(GeneralFormSchema), + defaultValues: { + enabled: resource.enabled, + name: resource.name, + niceId: resource.niceId, + subdomain: resource.subdomain ? resource.subdomain : undefined, + domainId: resource.domainId || undefined, + proxyPort: resource.proxyPort || undefined, + // enableProxy: resource.enableProxy || false + maintenanceModeEnabled: resource.maintenanceModeEnabled || false, + maintenanceModeType: resource.maintenanceModeType || "automatic", + maintenanceTitle: resource.maintenanceTitle || "We'll be back soon!", + maintenanceMessage: resource.maintenanceMessage || "We are currently performing scheduled maintenance. Please check back soon.", + maintenanceEstimatedTime: resource.maintenanceEstimatedTime || "", + }, + mode: "onChange" + }); + + const isMaintenanceEnabled = form.watch("maintenanceModeEnabled"); + const maintenanceModeType = form.watch("maintenanceModeType"); + + useEffect(() => { + const fetchSites = async () => { + const res = await api.get>( + `/org/${orgId}/sites/` + ); + setSites(res.data.data.sites); + }; + + 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") + ) + }); + }); + + 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 res = await api + .post>( + `resource/${resource?.resourceId}`, + { + enabled: data.enabled, + name: data.name, + niceId: data.niceId, + subdomain: data.subdomain ? toASCII(data.subdomain) : undefined, + domainId: data.domainId, + proxyPort: data.proxyPort, + // ...(!resource.http && { + // enableProxy: data.enableProxy + // }) + maintenanceModeEnabled: data.maintenanceModeEnabled, + maintenanceModeType: data.maintenanceModeType, + maintenanceTitle: data.maintenanceTitle || null, + maintenanceMessage: data.maintenanceMessage || null, + maintenanceEstimatedTime: data.maintenanceEstimatedTime || null, + } + ) + .catch((e) => { + toast({ + variant: "destructive", + title: t("resourceErrorUpdate"), + description: formatAxiosError( + e, + t("resourceErrorUpdateDescription") + ) + }); + }); + + if (res && res.status === 200) { + const updated = res.data.data; + + updateResource({ + enabled: data.enabled, + name: data.name, + niceId: data.niceId, + subdomain: data.subdomain, + fullDomain: resource.fullDomain, + proxyPort: data.proxyPort, + // ...(!resource.http && { + // enableProxy: data.enableProxy + // }) + maintenanceModeEnabled: data.maintenanceModeEnabled, + maintenanceModeType: data.maintenanceModeType, + maintenanceTitle: data.maintenanceTitle || null, + maintenanceMessage: data.maintenanceMessage || null, + maintenanceEstimatedTime: data.maintenanceEstimatedTime || null, + }); + + toast({ + title: t("resourceUpdated"), + description: t("resourceUpdatedDescription") + }); + + if (data.niceId && data.niceId !== resource?.niceId) { + router.replace(`/${updated.orgId}/settings/resources/${data.niceId}/general`); + } else { + router.refresh(); + } + + setSaveLoading(false); + } + + setSaveLoading(false); + } + + return ( + !loadingPage && ( + <> + + + + + {t("resourceGeneral")} + + + {t("resourceGeneralDescription")} + + + + + +
+ + ( + +
+ + + form.setValue( + "enabled", + val + ) + } + /> + +
+ +
+ )} + /> + + ( + + + {t("name")} + + + + + + + )} + /> + + ( + + {t("identifier")} + + + + + + )} + /> + + {!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} + + +
+
+ + + +
+
+

+ Maintenance Mode +

+

+ Display a maintenance page to visitors +

+
+ + ( + +
+ + + form.setValue( + "maintenanceModeEnabled", + val + ) + } + /> + +
+ + Show a maintenance page to visitors + + +
+ )} + /> + + {isMaintenanceEnabled && ( +
+ ( + + + Maintenance Mode Type + + + + + + + +
+ + Automatic (Recommended) + + + Show maintenance page only when all backend targets are down or unhealthy. + Your resource continues working normally as long as at least one target is healthy. + +
+
+ + + + +
+ + Forced + + + Always show the maintenance page regardless of backend health. + Use this for planned maintenance when you want to prevent all access. + +
+
+
+
+ +
+ )} + /> + + {maintenanceModeType === "forced" && ( + + + + Warning: All traffic will be directed to the maintenance page. + Your backend resources will not receive any requests. + + + )} + + ( + + Page Title + + + + + The main heading displayed on the maintenance page + + + + )} + /> + + ( + + Maintenance Message + +