From 0abc561bb843d07b19ef44456846525af672bb09 Mon Sep 17 00:00:00 2001 From: Fred KISSIE Date: Tue, 11 Nov 2025 02:22:26 +0100 Subject: [PATCH 01/46] =?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 02/46] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20add=20org=20auth?= =?UTF-8?q?=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 03/46] =?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 04/46] =?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 05/46] =?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 06/46] =?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 | 862 +++++++++--------- 23 files changed, 1377 insertions(+), 975 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(); - - 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); - - 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); +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(); + + // 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" + }); + + // 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 { + 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); } - }; - - 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 - }); + } catch (err) { + console.error("Failed to fetch domains:", err); } + }; - setHasUnsavedChanges(true); + if (org?.org.orgId) { + fetchDomains(); } - - // Clear auth page domain - function clearAuthPageDomain() { - form.setValue("authPageDomainId", ""); - form.setValue("authPageSubdomain", ""); - setLoginPage(null); - setHasUnsavedChanges(true); + }, []); + + // 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 + }); } - async function onSubmit(data: AuthPageFormValues) { - setLoadingSave(true); - - 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 { - // 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(true); + } - // Update existing auth page domain - const updateRes = await api.post( - `/org/${org?.org.orgId}/login-page/${loginPageId}`, - { - domainId: data.authPageDomainId, - subdomain: sanitizedSubdomain || null - } - ); + // Clear auth page domain + function clearAuthPageDomain() { + form.setValue("authPageDomainId", ""); + form.setValue("authPageSubdomain", ""); + setLoginPage(null); + setHasUnsavedChanges(true); + } - if (updateRes.status === 201) { - setLoginPage(updateRes.data.data); - setLoginPageExists(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 } - } - } - } 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; + if (updateRes.status === 201) { + setLoginPage(updateRes.data.data); + setLoginPageExists(true); + } } 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; - } + // 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 + } + ); - await api.delete( - `/org/${org?.org.orgId}/login-page/${loginPageId}` - ); - setLoginPage(null); - setLoginPageExists(false); + 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; + + 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") + return ( + <> + + + {t("authPage")} + + {t("authPageDescription")} + + + + {build === "saas" && !subscription?.subscribed ? ( + + + {t("orgAuthPageDisabled")}{" "} + {t("subscriptionRequiredToUse")} + + + ) : null} + + + + +
+ +
+ + + {loginPage && + !loginPage.domainId ? ( + -
- - {form.watch( - "authPageDomainId" - ) && ( - - )} -
-
- - {!form.watch( - "authPageDomainId" - ) && ( -
- {t( - "addDomainToEnableCustomAuthPages" - )} -
+ ); + 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") && ( + + )} +
+
- {env.flags - .usePangolinDns && - (build === "enterprise" || - (build === "saas" && - subscription?.subscribed)) && - loginPage?.domainId && - loginPage?.fullDomain && - !hasUnsavedChanges && ( - - )} + {!form.watch("authPageDomainId") && ( +
+ {t( + "addDomainToEnableCustomAuthPages" + )}
- - - )} -
-
-
- - {/* 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); - }} - /> - - - - - - - - - - - ); - } -); + )} + + {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 07/46] =?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 08/46] =?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 09/46] =?UTF-8?q?=E2=9C=A8=20save=20and=20update=20brandin?= =?UTF-8?q?g?= 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 10/46] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20do=20not=20manually?= =?UTF-8?q?=20track=20the=20loading=20state=20in=20`ConfirmDeleteDialog`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ConfirmDeleteDialog.tsx | 44 ++++---------------------- 1 file changed, 7 insertions(+), 37 deletions(-) diff --git a/src/components/ConfirmDeleteDialog.tsx b/src/components/ConfirmDeleteDialog.tsx index 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 11/46] =?UTF-8?q?=E2=9C=A8=20apply=20auth=20branding=20to?= =?UTF-8?q?=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 12/46] =?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 13/46] =?UTF-8?q?=F0=9F=97=83=EF=B8=8F=20Add=20`primaryCol?= =?UTF-8?q?or`=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 14/46] =?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 15/46] =?UTF-8?q?=E2=99=BB=EF=B8=8Fshow=20org=20page=20bra?= =?UTF-8?q?nding=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 16/46] =?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 17/46] =?UTF-8?q?=E2=99=BB=EF=B8=8Frename=20`title`=20&=20?= =?UTF-8?q?`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 18/46] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20only=20include=20org?= =?UTF-8?q?=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 19/46] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20generate=20build=20v?= =?UTF-8?q?ariable=20as=20fully=20typed=20to=20prevent=20typos=20(to=20che?= =?UTF-8?q?ck=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 20/46] =?UTF-8?q?=E2=9C=A8add=20primary=20color=20branding?= =?UTF-8?q?=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 21/46] =?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 22/46] =?UTF-8?q?=E2=9C=A8show=20toast=20message=20when=20?= =?UTF-8?q?updating=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 23/46] =?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 24/46] =?UTF-8?q?=F0=9F=90=9B=20fix=20`cols`=20and=20some?= =?UTF-8?q?=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 26/46] =?UTF-8?q?=E2=9C=A8apply=20branding=20to=20org=20au?= =?UTF-8?q?th=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; - - 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 { orgId } = parsedQuery.data; - 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 27/46] =?UTF-8?q?=E2=99=BB=EF=B8=8Fonly=20apply=20org=20br?= =?UTF-8?q?anding=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 28/46] =?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 29/46] =?UTF-8?q?=E2=8F=AA=20revert=20package.json=20chang?= =?UTF-8?q?es?= 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 30/46] =?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 31/46] =?UTF-8?q?=E2=99=BB=EF=B8=8Fremove=20unused=20impor?= =?UTF-8?q?ts?= 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 32/46] =?UTF-8?q?=F0=9F=92=A1remove=20useless=20comments?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AuthPageBrandingForm.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx index 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 33/46] =?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 34/46] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20correctly=20apply=20?= =?UTF-8?q?the=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 35/46] =?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 36/46] =?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 37/46] =?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 38/46] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20adapt=20zod=20schema?= =?UTF-8?q?=20to=20v4=20and=20move=20=20form=20description=20below=20the?= =?UTF-8?q?=20inptu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../loginPage/upsertLoginPageBranding.ts | 36 ++++----- src/components/AuthPageBrandingForm.tsx | 74 +++++++++---------- 2 files changed, 53 insertions(+), 57 deletions(-) diff --git a/server/private/routers/loginPage/upsertLoginPageBranding.ts b/server/private/routers/loginPage/upsertLoginPageBranding.ts index 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 39/46] =?UTF-8?q?=F0=9F=9A=9A=20rename=20`SecurityFeatures?= =?UTF-8?q?Alert`=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 40/46] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Also=20check=20for?= =?UTF-8?q?=20active=20subscription=20in=20paid=20status=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/usePaidStatus.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hooks/usePaidStatus.ts b/src/hooks/usePaidStatus.ts index 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 41/46] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20check=20for=20disabl?= =?UTF-8?q?ed=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 42/46] =?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 43/46] =?UTF-8?q?=F0=9F=9B=82=20=20check=20for=20subscript?= =?UTF-8?q?ion=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 44/46] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20load=20branding=20on?= =?UTF-8?q?ly=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 45/46] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20check=20for=20licenc?= =?UTF-8?q?e=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 46/46] =?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" }