From df31c1391256fbc5845a28f69cd1170ee95dd7da Mon Sep 17 00:00:00 2001 From: Adrian Astles <49412215+adrianeastles@users.noreply.github.com> Date: Fri, 25 Jul 2025 21:59:25 +0800 Subject: [PATCH 01/38] added real-time password validation to signup form. --- messages/en-US.json | 20 ++- src/app/auth/signup/SignupForm.tsx | 209 ++++++++++++++++++++++++++--- 2 files changed, 212 insertions(+), 17 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index ed004d99..cd31f7f9 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -834,6 +834,24 @@ "pincodeRequirementsLength": "PIN must be exactly 6 digits", "pincodeRequirementsChars": "PIN must only contain numbers", "passwordRequirementsLength": "Password must be at least 1 character long", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP must be at least 1 character long", "otpEmailSent": "OTP Sent", "otpEmailSentDescription": "An OTP has been sent to your email", @@ -1281,4 +1299,4 @@ "and": "and", "privacyPolicy": "privacy policy" } -} +} \ No newline at end of file diff --git a/src/app/auth/signup/SignupForm.tsx b/src/app/auth/signup/SignupForm.tsx index 5494ba10..d6d79eb7 100644 --- a/src/app/auth/signup/SignupForm.tsx +++ b/src/app/auth/signup/SignupForm.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; @@ -23,6 +23,7 @@ import { CardTitle } from "@/components/ui/card"; import { Alert, AlertDescription } from "@/components/ui/alert"; +import { Progress } from "@/components/ui/progress"; import { SignUpResponse } from "@server/routers/auth"; import { useRouter } from "next/navigation"; import { passwordSchema } from "@server/auth/passwordSchema"; @@ -35,6 +36,40 @@ import { cleanRedirect } from "@app/lib/cleanRedirect"; import { useTranslations } from "next-intl"; import BrandingLogo from "@app/components/BrandingLogo"; import { build } from "@server/build"; +import { Check, X } from "lucide-react"; +import { cn } from "@app/lib/cn"; + +// Password strength calculation +const calculatePasswordStrength = (password: string) => { + const requirements = { + length: password.length >= 8, + uppercase: /[A-Z]/.test(password), + lowercase: /[a-z]/.test(password), + number: /[0-9]/.test(password), + special: /[~!`@#$%^&*()_\-+={}[\]|\\:;"'<>,.\/?]/.test(password) + }; + + const score = Object.values(requirements).filter(Boolean).length; + let strength: "weak" | "medium" | "strong" = "weak"; + let color = "bg-red-500"; + let percentage = 0; + + if (score >= 5) { + strength = "strong"; + color = "bg-green-500"; + percentage = 100; + } else if (score >= 3) { + strength = "medium"; + color = "bg-yellow-500"; + percentage = 60; + } else if (score >= 1) { + strength = "weak"; + color = "bg-red-500"; + percentage = 30; + } + + return { requirements, strength, color, percentage, score }; +}; type SignupFormProps = { redirect?: string; @@ -71,14 +106,14 @@ export default function SignupForm({ inviteToken }: SignupFormProps) { const router = useRouter(); - - const { env } = useEnvContext(); - - const api = createApiClient({ env }); + const api = createApiClient(useEnvContext()); + const t = useTranslations(); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [termsAgreedAt, setTermsAgreedAt] = useState(null); + const [passwordValue, setPasswordValue] = useState(""); + const [confirmPasswordValue, setConfirmPasswordValue] = useState(""); const form = useForm>({ resolver: zodResolver(formSchema), @@ -87,10 +122,12 @@ export default function SignupForm({ password: "", confirmPassword: "", agreeToTerms: false - } + }, + mode: "onChange" // Enable real-time validation }); - const t = useTranslations(); + const passwordStrength = calculatePasswordStrength(passwordValue); + const doPasswordsMatch = passwordValue.length > 0 && confirmPasswordValue.length > 0 && passwordValue === confirmPasswordValue; async function onSubmit(values: z.infer) { const { email, password } = values; @@ -183,11 +220,128 @@ export default function SignupForm({ name="password" render={({ field }) => ( - {t("password")} +
+ {t("password")} + {passwordStrength.strength === "strong" && ( + + )} +
- +
+ { + field.onChange(e); + setPasswordValue(e.target.value); + }} + className={cn( + passwordStrength.strength === "strong" && "border-green-500 focus-visible:ring-green-500", + passwordStrength.strength === "medium" && "border-yellow-500 focus-visible:ring-yellow-500", + passwordStrength.strength === "weak" && passwordValue.length > 0 && "border-red-500 focus-visible:ring-red-500" + )} + autoComplete="new-password" + /> +
- + + {passwordValue.length > 0 && ( +
+ {/* Password Strength Meter */} +
+
+ {t("passwordStrength")} + + {t(`passwordStrength${passwordStrength.strength.charAt(0).toUpperCase() + passwordStrength.strength.slice(1)}`)} + +
+ +
+ + {/* Requirements Checklist */} +
+
{t("passwordRequirements")}
+
+
+ {passwordStrength.requirements.length ? ( + + ) : ( + + )} + + {t("passwordRequirementLengthText")} + +
+
+ {passwordStrength.requirements.uppercase ? ( + + ) : ( + + )} + + {t("passwordRequirementUppercaseText")} + +
+
+ {passwordStrength.requirements.lowercase ? ( + + ) : ( + + )} + + {t("passwordRequirementLowercaseText")} + +
+
+ {passwordStrength.requirements.number ? ( + + ) : ( + + )} + + {t("passwordRequirementNumberText")} + +
+
+ {passwordStrength.requirements.special ? ( + + ) : ( + + )} + + {t("passwordRequirementSpecialText")} + +
+
+
+
+ )} + + {/* Only show FormMessage when not showing our custom requirements */} + {passwordValue.length === 0 && }
)} /> @@ -196,13 +350,36 @@ export default function SignupForm({ name="confirmPassword" render={({ field }) => ( - - {t("confirmPassword")} - +
+ {t('confirmPassword')} + {doPasswordsMatch && ( + + )} +
- +
+ { + field.onChange(e); + setConfirmPasswordValue(e.target.value); + }} + className={cn( + doPasswordsMatch && "border-green-500 focus-visible:ring-green-500", + confirmPasswordValue.length > 0 && !doPasswordsMatch && "border-red-500 focus-visible:ring-red-500" + )} + autoComplete="new-password" + /> +
- + {confirmPasswordValue.length > 0 && !doPasswordsMatch && ( +

+ {t("passwordsDoNotMatch")} +

+ )} + {/* Only show FormMessage when field is empty */} + {confirmPasswordValue.length === 0 && }
)} /> @@ -269,4 +446,4 @@ export default function SignupForm({ ); -} +} \ No newline at end of file From 350485612ee15c7dc49dc0f62bc5e7f740c461ed Mon Sep 17 00:00:00 2001 From: Adrian Astles <49412215+adrianeastles@users.noreply.github.com> Date: Fri, 25 Jul 2025 22:46:40 +0800 Subject: [PATCH 02/38] This improves the user experience by automatically filling the email field and preventing users from changing the email they were invited with. - Update invite link generation to include email parameter in URL - Modify signup form to pre-fill and lock email field when provided via invite - Update invite page and status card to preserve email through redirect chain - Ensure existing invite URLs continue to work without breaking changes --- server/routers/user/inviteUser.ts | 4 ++-- src/app/auth/signup/SignupForm.tsx | 11 ++++++++--- src/app/auth/signup/page.tsx | 6 +++++- src/app/invite/InviteStatusCard.tsx | 12 ++++++++++-- src/app/invite/page.tsx | 13 ++++++++++--- 5 files changed, 35 insertions(+), 11 deletions(-) diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index 837ef179..174600fc 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -189,7 +189,7 @@ export async function inviteUser( ) ); - const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}`; + const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`; if (doEmail) { await sendEmail( @@ -241,7 +241,7 @@ export async function inviteUser( }); }); - const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}`; + const inviteLink = `${config.getRawConfig().app.dashboard_url}/invite?token=${inviteId}-${token}&email=${encodeURIComponent(email)}`; if (doEmail) { await sendEmail( diff --git a/src/app/auth/signup/SignupForm.tsx b/src/app/auth/signup/SignupForm.tsx index d6d79eb7..f4690683 100644 --- a/src/app/auth/signup/SignupForm.tsx +++ b/src/app/auth/signup/SignupForm.tsx @@ -75,6 +75,7 @@ type SignupFormProps = { redirect?: string; inviteId?: string; inviteToken?: string; + emailParam?: string; }; const formSchema = z @@ -103,7 +104,8 @@ const formSchema = z export default function SignupForm({ redirect, inviteId, - inviteToken + inviteToken, + emailParam }: SignupFormProps) { const router = useRouter(); const api = createApiClient(useEnvContext()); @@ -118,7 +120,7 @@ export default function SignupForm({ const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - email: "", + email: emailParam || "", password: "", confirmPassword: "", agreeToTerms: false @@ -209,7 +211,10 @@ export default function SignupForm({ {t("email")} - + diff --git a/src/app/auth/signup/page.tsx b/src/app/auth/signup/page.tsx index debd7c58..673e69bf 100644 --- a/src/app/auth/signup/page.tsx +++ b/src/app/auth/signup/page.tsx @@ -11,7 +11,10 @@ import { getTranslations } from "next-intl/server"; export const dynamic = "force-dynamic"; export default async function Page(props: { - searchParams: Promise<{ redirect: string | undefined }>; + searchParams: Promise<{ + redirect: string | undefined; + email: string | undefined; + }>; }) { const searchParams = await props.searchParams; const getUser = cache(verifySession); @@ -69,6 +72,7 @@ export default async function Page(props: { redirect={redirectUrl} inviteToken={inviteToken} inviteId={inviteId} + emailParam={searchParams.email} />

diff --git a/src/app/invite/InviteStatusCard.tsx b/src/app/invite/InviteStatusCard.tsx index 3ecf16f5..6d7db4dc 100644 --- a/src/app/invite/InviteStatusCard.tsx +++ b/src/app/invite/InviteStatusCard.tsx @@ -17,11 +17,13 @@ import { useTranslations } from "next-intl"; type InviteStatusCardProps = { type: "rejected" | "wrong_user" | "user_does_not_exist" | "not_logged_in"; token: string; + email?: string; }; export default function InviteStatusCard({ type, token, + email, }: InviteStatusCardProps) { const router = useRouter(); const api = createApiClient(useEnvContext()); @@ -29,12 +31,18 @@ export default function InviteStatusCard({ async function goToLogin() { await api.post("/auth/logout", {}); - router.push(`/auth/login?redirect=/invite?token=${token}`); + const redirectUrl = email + ? `/auth/login?redirect=/invite?token=${token}&email=${encodeURIComponent(email)}` + : `/auth/login?redirect=/invite?token=${token}`; + router.push(redirectUrl); } async function goToSignup() { await api.post("/auth/logout", {}); - router.push(`/auth/signup?redirect=/invite?token=${token}`); + const redirectUrl = email + ? `/auth/signup?redirect=/invite?token=${token}&email=${encodeURIComponent(email)}` + : `/auth/signup?redirect=/invite?token=${token}`; + router.push(redirectUrl); } function renderBody() { diff --git a/src/app/invite/page.tsx b/src/app/invite/page.tsx index 014fb45b..0df7b810 100644 --- a/src/app/invite/page.tsx +++ b/src/app/invite/page.tsx @@ -14,6 +14,7 @@ export default async function InvitePage(props: { const params = await props.searchParams; const tokenParam = params.token as string; + const emailParam = params.email as string; if (!tokenParam) { redirect("/"); @@ -70,16 +71,22 @@ export default async function InvitePage(props: { const type = cardType(); if (!user && type === "user_does_not_exist") { - redirect(`/auth/signup?redirect=/invite?token=${params.token}`); + const redirectUrl = emailParam + ? `/auth/signup?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}` + : `/auth/signup?redirect=/invite?token=${params.token}`; + redirect(redirectUrl); } if (!user && type === "not_logged_in") { - redirect(`/auth/login?redirect=/invite?token=${params.token}`); + const redirectUrl = emailParam + ? `/auth/login?redirect=/invite?token=${params.token}&email=${encodeURIComponent(emailParam)}` + : `/auth/login?redirect=/invite?token=${params.token}`; + redirect(redirectUrl); } return ( <> - + ); } From 69baa6785fba33e5466eddcffd88dc4bbb87aa57 Mon Sep 17 00:00:00 2001 From: Adrian Astles <49412215+adrianeastles@users.noreply.github.com> Date: Sun, 3 Aug 2025 21:17:18 +0800 Subject: [PATCH 03/38] feat: Add setup token security for initial server setup - Add setupTokens database table with proper schema - Implement setup token generation on first server startup - Add token validation endpoint and modify admin creation - Update initial setup page to require setup token - Add migration scripts for both SQLite and PostgreSQL - Add internationalization support for setup token fields - Implement proper error handling and logging - Add CLI command for resetting user security keys This prevents unauthorized access during initial server setup by requiring a token that is generated and displayed in the server console. --- messages/en-US.json | 3 + package-lock.json | 110 ++-------------------- server/db/pg/schema.ts | 9 ++ server/db/sqlite/schema.ts | 9 ++ server/routers/auth/index.ts | 1 + server/routers/auth/setServerAdmin.ts | 59 +++++++++--- server/routers/auth/validateSetupToken.ts | 84 +++++++++++++++++ server/routers/external.ts | 1 + server/setup/ensureSetupToken.ts | 73 ++++++++++++++ server/setup/index.ts | 2 + server/setup/migrationsPg.ts | 4 +- server/setup/migrationsSqlite.ts | 2 + server/setup/scriptsPg/1.9.0.ts | 25 +++++ server/setup/scriptsSqlite/1.9.0.ts | 35 +++++++ src/app/auth/initial-setup/page.tsx | 20 ++++ 15 files changed, 322 insertions(+), 115 deletions(-) create mode 100644 server/routers/auth/validateSetupToken.ts create mode 100644 server/setup/ensureSetupToken.ts create mode 100644 server/setup/scriptsPg/1.9.0.ts create mode 100644 server/setup/scriptsSqlite/1.9.0.ts diff --git a/messages/en-US.json b/messages/en-US.json index 9986c5fd..0389a0dc 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -967,6 +967,9 @@ "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", "actionListSites": "List Sites", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Update Site", "actionListSiteRoles": "List Allowed Site Roles", "actionCreateResource": "Create Resource", diff --git a/package-lock.json b/package-lock.json index baec0b2b..7eb58d2c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,7 +61,6 @@ "http-errors": "2.0.0", "i": "^0.3.7", "input-otp": "1.4.2", - "ioredis": "^5.6.1", "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", @@ -77,7 +76,6 @@ "oslo": "1.2.1", "pg": "^8.16.2", "qrcode.react": "4.2.0", - "rate-limit-redis": "^4.2.1", "react": "19.1.0", "react-dom": "19.1.0", "react-easy-sort": "^1.6.0", @@ -2010,12 +2008,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@ioredis/commands": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", - "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", - "license": "MIT" - }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -2058,6 +2050,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -6309,6 +6302,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -6455,15 +6449,6 @@ "node": ">=6" } }, - "node_modules/cluster-key-slot": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/cmdk": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", @@ -6947,15 +6932,6 @@ "node": ">=0.4.0" } }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8835,6 +8811,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -9122,30 +9099,6 @@ "tslib": "^2.8.0" } }, - "node_modules/ioredis": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", - "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", - "license": "MIT", - "dependencies": { - "@ioredis/commands": "^1.1.1", - "cluster-key-slot": "^1.1.0", - "debug": "^4.3.4", - "denque": "^2.1.0", - "lodash.defaults": "^4.2.0", - "lodash.isarguments": "^3.1.0", - "redis-errors": "^1.2.0", - "redis-parser": "^3.0.0", - "standard-as-callback": "^2.1.0" - }, - "engines": { - "node": ">=12.22.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/ioredis" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -9606,6 +9559,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, "license": "ISC", "engines": { "node": ">=16" @@ -10112,24 +10066,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "license": "MIT" - }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, - "node_modules/lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", - "license": "MIT" - }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -10481,6 +10423,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -10493,6 +10436,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" @@ -14470,18 +14414,6 @@ "node": ">= 0.6" } }, - "node_modules/rate-limit-redis": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.2.1.tgz", - "integrity": "sha512-JsUsVmRVI6G/XrlYtfGV1NMCbGS/CVYayHkxD5Ism5FaL8qpFHCXbFkUeIi5WJ/onJOKWCgtB/xtCLa6qSXb4g==", - "license": "MIT", - "engines": { - "node": ">= 16" - }, - "peerDependencies": { - "express-rate-limit": ">= 6" - } - }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", @@ -14828,27 +14760,6 @@ "node": ">=0.8.8" } }, - "node_modules/redis-errors": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", - "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/redis-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", - "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", - "license": "MIT", - "dependencies": { - "redis-errors": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -15628,12 +15539,6 @@ "node": "*" } }, - "node_modules/standard-as-callback": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", - "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", - "license": "MIT" - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -16021,6 +15926,7 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -16667,6 +16573,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -16972,6 +16879,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index be4e58e2..afc9d6e7 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -592,6 +592,14 @@ export const webauthnChallenge = pgTable("webauthnChallenge", { expiresAt: bigint("expiresAt", { mode: "number" }).notNull() // Unix timestamp }); +export const setupTokens = pgTable("setupTokens", { + tokenId: varchar("tokenId").primaryKey(), + token: varchar("token").notNull(), + used: boolean("used").notNull().default(false), + dateCreated: varchar("dateCreated").notNull(), + dateUsed: varchar("dateUsed") +}); + export type Org = InferSelectModel; export type User = InferSelectModel; export type Site = InferSelectModel; @@ -637,3 +645,4 @@ export type OlmSession = InferSelectModel; export type UserClient = InferSelectModel; export type RoleClient = InferSelectModel; export type OrgDomains = InferSelectModel; +export type SetupToken = InferSelectModel; diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 5773a5f3..cc0fb6d0 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -187,6 +187,14 @@ export const webauthnChallenge = sqliteTable("webauthnChallenge", { expiresAt: integer("expiresAt").notNull() // Unix timestamp }); +export const setupTokens = sqliteTable("setupTokens", { + tokenId: text("tokenId").primaryKey(), + token: text("token").notNull(), + used: integer("used", { mode: "boolean" }).notNull().default(false), + dateCreated: text("dateCreated").notNull(), + dateUsed: text("dateUsed") +}); + export const newts = sqliteTable("newt", { newtId: text("id").primaryKey(), secretHash: text("secretHash").notNull(), @@ -679,3 +687,4 @@ export type ApiKey = InferSelectModel; export type ApiKeyAction = InferSelectModel; export type ApiKeyOrg = InferSelectModel; export type OrgDomains = InferSelectModel; +export type SetupToken = InferSelectModel; diff --git a/server/routers/auth/index.ts b/server/routers/auth/index.ts index cc8fd630..505d12c2 100644 --- a/server/routers/auth/index.ts +++ b/server/routers/auth/index.ts @@ -10,6 +10,7 @@ export * from "./resetPassword"; export * from "./requestPasswordReset"; export * from "./setServerAdmin"; export * from "./initialSetupComplete"; +export * from "./validateSetupToken"; export * from "./changePassword"; export * from "./checkResourceSession"; export * from "./securityKey"; diff --git a/server/routers/auth/setServerAdmin.ts b/server/routers/auth/setServerAdmin.ts index 7c49753e..ebb95359 100644 --- a/server/routers/auth/setServerAdmin.ts +++ b/server/routers/auth/setServerAdmin.ts @@ -8,14 +8,15 @@ import logger from "@server/logger"; import { hashPassword } from "@server/auth/password"; import { passwordSchema } from "@server/auth/passwordSchema"; import { response } from "@server/lib"; -import { db, users } from "@server/db"; -import { eq } from "drizzle-orm"; +import { db, users, setupTokens } from "@server/db"; +import { eq, and } from "drizzle-orm"; import { UserType } from "@server/types/UserTypes"; import moment from "moment"; export const bodySchema = z.object({ email: z.string().toLowerCase().email(), - password: passwordSchema + password: passwordSchema, + setupToken: z.string().min(1, "Setup token is required") }); export type SetServerAdminBody = z.infer; @@ -39,7 +40,27 @@ export async function setServerAdmin( ); } - const { email, password } = parsedBody.data; + const { email, password, setupToken } = parsedBody.data; + + // Validate setup token + const [validToken] = await db + .select() + .from(setupTokens) + .where( + and( + eq(setupTokens.token, setupToken), + eq(setupTokens.used, false) + ) + ); + + if (!validToken) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Invalid or expired setup token" + ) + ); + } const [existing] = await db .select() @@ -58,15 +79,27 @@ export async function setServerAdmin( const passwordHash = await hashPassword(password); const userId = generateId(15); - await db.insert(users).values({ - userId: userId, - email: email, - type: UserType.Internal, - username: email, - passwordHash, - dateCreated: moment().toISOString(), - serverAdmin: true, - emailVerified: true + await db.transaction(async (trx) => { + // Mark the token as used + await trx + .update(setupTokens) + .set({ + used: true, + dateUsed: moment().toISOString() + }) + .where(eq(setupTokens.tokenId, validToken.tokenId)); + + // Create the server admin user + await trx.insert(users).values({ + userId: userId, + email: email, + type: UserType.Internal, + username: email, + passwordHash, + dateCreated: moment().toISOString(), + serverAdmin: true, + emailVerified: true + }); }); return response(res, { diff --git a/server/routers/auth/validateSetupToken.ts b/server/routers/auth/validateSetupToken.ts new file mode 100644 index 00000000..e3c29833 --- /dev/null +++ b/server/routers/auth/validateSetupToken.ts @@ -0,0 +1,84 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, setupTokens } 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"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; + +const validateSetupTokenSchema = z + .object({ + token: z.string().min(1, "Token is required") + }) + .strict(); + +export type ValidateSetupTokenResponse = { + valid: boolean; + message: string; +}; + +export async function validateSetupToken( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedBody = validateSetupTokenSchema.safeParse(req.body); + + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { token } = parsedBody.data; + + // Find the token in the database + const [setupToken] = await db + .select() + .from(setupTokens) + .where( + and( + eq(setupTokens.token, token), + eq(setupTokens.used, false) + ) + ); + + if (!setupToken) { + return response(res, { + data: { + valid: false, + message: "Invalid or expired setup token" + }, + success: true, + error: false, + message: "Token validation completed", + status: HttpCode.OK + }); + } + + return response(res, { + data: { + valid: true, + message: "Setup token is valid" + }, + success: true, + error: false, + message: "Token validation completed", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to validate setup token" + ) + ); + } +} \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index 5bae553e..f9ff7377 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -1033,6 +1033,7 @@ authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback); authRouter.put("/set-server-admin", auth.setServerAdmin); authRouter.get("/initial-setup-complete", auth.initialSetupComplete); +authRouter.post("/validate-setup-token", auth.validateSetupToken); // Security Key routes authRouter.post( diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts new file mode 100644 index 00000000..1734b5e6 --- /dev/null +++ b/server/setup/ensureSetupToken.ts @@ -0,0 +1,73 @@ +import { db, setupTokens, users } from "@server/db"; +import { eq } from "drizzle-orm"; +import { generateRandomString, RandomReader } from "@oslojs/crypto/random"; +import moment from "moment"; +import logger from "@server/logger"; + +const random: RandomReader = { + read(bytes: Uint8Array): void { + crypto.getRandomValues(bytes); + } +}; + +function generateToken(): string { + // Generate a 32-character alphanumeric token + const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; + return generateRandomString(random, alphabet, 32); +} + +function generateId(length: number): string { + const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; + return generateRandomString(random, alphabet, length); +} + +export async function ensureSetupToken() { + try { + // Check if a server admin already exists + const [existingAdmin] = await db + .select() + .from(users) + .where(eq(users.serverAdmin, true)); + + // If admin exists, no need for setup token + if (existingAdmin) { + logger.warn("Server admin exists. Setup token generation skipped."); + return; + } + + // Check if a setup token already exists + const existingTokens = await db + .select() + .from(setupTokens) + .where(eq(setupTokens.used, false)); + + // If unused token exists, display it instead of creating a new one + if (existingTokens.length > 0) { + console.log("=== SETUP TOKEN EXISTS ==="); + console.log("Token:", existingTokens[0].token); + console.log("Use this token on the initial setup page"); + console.log("================================"); + return; + } + + // Generate a new setup token + const token = generateToken(); + const tokenId = generateId(15); + + await db.insert(setupTokens).values({ + tokenId: tokenId, + token: token, + used: false, + dateCreated: moment().toISOString(), + dateUsed: null + }); + + console.log("=== SETUP TOKEN GENERATED ==="); + console.log("Token:", token); + console.log("Use this token on the initial setup page"); + console.log("================================"); + } catch (error) { + console.error("Failed to ensure setup token:", error); + throw error; + } +} \ No newline at end of file diff --git a/server/setup/index.ts b/server/setup/index.ts index d126869a..2dfb633e 100644 --- a/server/setup/index.ts +++ b/server/setup/index.ts @@ -1,9 +1,11 @@ import { ensureActions } from "./ensureActions"; import { copyInConfig } from "./copyInConfig"; import { clearStaleData } from "./clearStaleData"; +import { ensureSetupToken } from "./ensureSetupToken"; export async function runSetupFunctions() { await copyInConfig(); // copy in the config to the db as needed await ensureActions(); // make sure all of the actions are in the db and the roles await clearStaleData(); + await ensureSetupToken(); // ensure setup token exists for initial setup } diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index 07ece65b..6b3f20b9 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -8,6 +8,7 @@ import path from "path"; import m1 from "./scriptsPg/1.6.0"; import m2 from "./scriptsPg/1.7.0"; import m3 from "./scriptsPg/1.8.0"; +import m4 from "./scriptsPg/1.9.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -16,7 +17,8 @@ import m3 from "./scriptsPg/1.8.0"; const migrations = [ { version: "1.6.0", run: m1 }, { version: "1.7.0", run: m2 }, - { version: "1.8.0", run: m3 } + { version: "1.8.0", run: m3 }, + { version: "1.9.0", run: m4 } // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 15dd28d2..5b0850c8 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -25,6 +25,7 @@ import m20 from "./scriptsSqlite/1.5.0"; import m21 from "./scriptsSqlite/1.6.0"; import m22 from "./scriptsSqlite/1.7.0"; import m23 from "./scriptsSqlite/1.8.0"; +import m24 from "./scriptsSqlite/1.9.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -49,6 +50,7 @@ const migrations = [ { version: "1.6.0", run: m21 }, { version: "1.7.0", run: m22 }, { version: "1.8.0", run: m23 }, + { version: "1.9.0", run: m24 }, // Add new migrations here as they are created ] as const; diff --git a/server/setup/scriptsPg/1.9.0.ts b/server/setup/scriptsPg/1.9.0.ts new file mode 100644 index 00000000..22259cae --- /dev/null +++ b/server/setup/scriptsPg/1.9.0.ts @@ -0,0 +1,25 @@ +import { db } from "@server/db/pg/driver"; +import { sql } from "drizzle-orm"; + +const version = "1.9.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + try { + await db.execute(sql` + CREATE TABLE "setupTokens" ( + "tokenId" varchar PRIMARY KEY NOT NULL, + "token" varchar NOT NULL, + "used" boolean DEFAULT false NOT NULL, + "dateCreated" varchar NOT NULL, + "dateUsed" varchar + ); + `); + + console.log(`Added setupTokens table`); + } catch (e) { + console.log("Unable to add setupTokens table:", e); + throw e; + } +} \ No newline at end of file diff --git a/server/setup/scriptsSqlite/1.9.0.ts b/server/setup/scriptsSqlite/1.9.0.ts new file mode 100644 index 00000000..a4a20dda --- /dev/null +++ b/server/setup/scriptsSqlite/1.9.0.ts @@ -0,0 +1,35 @@ +import { APP_PATH } from "@server/lib/consts"; +import Database from "better-sqlite3"; +import path from "path"; + +const version = "1.9.0"; + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + const location = path.join(APP_PATH, "db", "db.sqlite"); + const db = new Database(location); + + try { + db.pragma("foreign_keys = OFF"); + + db.transaction(() => { + db.exec(` + CREATE TABLE 'setupTokens' ( + 'tokenId' text PRIMARY KEY NOT NULL, + 'token' text NOT NULL, + 'used' integer DEFAULT 0 NOT NULL, + 'dateCreated' text NOT NULL, + 'dateUsed' text + ); + `); + })(); + + db.pragma("foreign_keys = ON"); + + console.log(`Added setupTokens table`); + } catch (e) { + console.log("Unable to add setupTokens table:", e); + throw e; + } +} \ No newline at end of file diff --git a/src/app/auth/initial-setup/page.tsx b/src/app/auth/initial-setup/page.tsx index 17e6c2ec..518c5370 100644 --- a/src/app/auth/initial-setup/page.tsx +++ b/src/app/auth/initial-setup/page.tsx @@ -31,6 +31,7 @@ import { passwordSchema } from "@server/auth/passwordSchema"; const formSchema = z .object({ + setupToken: z.string().min(1, "Setup token is required"), email: z.string().email({ message: "Invalid email address" }), password: passwordSchema, confirmPassword: z.string() @@ -52,6 +53,7 @@ export default function InitialSetupPage() { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { + setupToken: "", email: "", password: "", confirmPassword: "" @@ -63,6 +65,7 @@ export default function InitialSetupPage() { setError(null); try { const res = await api.put("/auth/set-server-admin", { + setupToken: values.setupToken, email: values.email, password: values.password }); @@ -102,6 +105,23 @@ export default function InitialSetupPage() { onSubmit={form.handleSubmit(onSubmit)} className="space-y-4" > + ( + + {t("setupToken")} + + + + + + )} + /> Date: Sun, 3 Aug 2025 21:20:25 +0800 Subject: [PATCH 04/38] revert: package-lock.json to original state --- package-lock.json | 110 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 101 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7eb58d2c..baec0b2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "http-errors": "2.0.0", "i": "^0.3.7", "input-otp": "1.4.2", + "ioredis": "^5.6.1", "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", @@ -76,6 +77,7 @@ "oslo": "1.2.1", "pg": "^8.16.2", "qrcode.react": "4.2.0", + "rate-limit-redis": "^4.2.1", "react": "19.1.0", "react-dom": "19.1.0", "react-easy-sort": "^1.6.0", @@ -2008,6 +2010,12 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "license": "MIT" + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -2050,7 +2058,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -6302,7 +6309,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -6449,6 +6455,15 @@ "node": ">=6" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/cmdk": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", @@ -6932,6 +6947,15 @@ "node": ">=0.4.0" } }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -8811,7 +8835,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -9099,6 +9122,30 @@ "tslib": "^2.8.0" } }, + "node_modules/ioredis": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.6.1.tgz", + "integrity": "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -9559,7 +9606,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, "license": "ISC", "engines": { "node": ">=16" @@ -10066,12 +10112,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", "license": "MIT" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -10423,7 +10481,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -10436,7 +10493,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" @@ -14414,6 +14470,18 @@ "node": ">= 0.6" } }, + "node_modules/rate-limit-redis": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-4.2.1.tgz", + "integrity": "sha512-JsUsVmRVI6G/XrlYtfGV1NMCbGS/CVYayHkxD5Ism5FaL8qpFHCXbFkUeIi5WJ/onJOKWCgtB/xtCLa6qSXb4g==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "express-rate-limit": ">= 6" + } + }, "node_modules/raw-body": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", @@ -14760,6 +14828,27 @@ "node": ">=0.8.8" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -15539,6 +15628,12 @@ "node": "*" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -15926,7 +16021,6 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "dev": true, "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -16573,7 +16667,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -16879,7 +16972,6 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" From b2947193ec6e60ee1599d0a970d0b6c60c9de911 Mon Sep 17 00:00:00 2001 From: Adrian Astles <49412215+adrianeastles@users.noreply.github.com> Date: Tue, 5 Aug 2025 17:35:22 +0800 Subject: [PATCH 05/38] Integrate setup token into installer, this will now parse the container logs to extract setup token automatically. Displays token with clear instructions and URL for initial admin setup. --- install/main.go | 136 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 133 insertions(+), 3 deletions(-) diff --git a/install/main.go b/install/main.go index a6d9d686..ccf82e70 100644 --- a/install/main.go +++ b/install/main.go @@ -202,6 +202,28 @@ func main() { } } else { fmt.Println("Looks like you already installed, so I am going to do the setup...") + + // Read existing config to get DashboardDomain + traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml") + if err != nil { + fmt.Printf("Warning: Could not read existing config: %v\n", err) + fmt.Println("You may need to manually enter your domain information.") + config = collectUserInput(reader) + } else { + config.DashboardDomain = traefikConfig.DashboardDomain + config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail + config.BadgerVersion = traefikConfig.BadgerVersion + + // Show detected values and allow user to confirm or re-enter + fmt.Println("Detected existing configuration:") + fmt.Printf("Dashboard Domain: %s\n", config.DashboardDomain) + fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail) + fmt.Printf("Badger Version: %s\n", config.BadgerVersion) + + if !readBool(reader, "Are these values correct?", true) { + config = collectUserInput(reader) + } + } } if !checkIsCrowdsecInstalledInCompose() { @@ -239,6 +261,23 @@ func main() { } } + // Setup Token Section + fmt.Println("\n=== Setup Token ===") + + // Check if containers were started during this installation + containersStarted := false + if (isDockerInstalled() && chosenContainer == Docker) || + (isPodmanInstalled() && chosenContainer == Podman) { + // Try to fetch and display the token if containers are running + containersStarted = true + printSetupToken(chosenContainer, config.DashboardDomain) + } + + // If containers weren't started or token wasn't found, show instructions + if !containersStarted { + showSetupTokenInstructions(chosenContainer, config.DashboardDomain) + } + fmt.Println("Installation complete!") fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) } @@ -302,7 +341,13 @@ func collectUserInput(reader *bufio.Reader) Config { // Basic configuration fmt.Println("\n=== Basic Configuration ===") config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") - config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain) + + // Set default dashboard domain after base domain is collected + defaultDashboardDomain := "" + if config.BaseDomain != "" { + defaultDashboardDomain = "pangolin." + config.BaseDomain + } + config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) @@ -625,8 +670,8 @@ func pullContainers(containerType SupportedContainer) error { } if containerType == Docker { - if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil { - return fmt.Errorf("failed to pull the containers: %v", err) + if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil { + return fmt.Errorf("failed to pull the containers: %v", err) } return nil @@ -755,6 +800,91 @@ func waitForContainer(containerName string, containerType SupportedContainer) er return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds())) } +func printSetupToken(containerType SupportedContainer, dashboardDomain string) { + fmt.Println("Waiting for Pangolin to generate setup token...") + + // Wait for Pangolin to be healthy + if err := waitForContainer("pangolin", containerType); err != nil { + fmt.Println("Warning: Pangolin container did not become healthy in time.") + return + } + + // Give a moment for the setup token to be generated + time.Sleep(2 * time.Second) + + // Fetch logs + var cmd *exec.Cmd + if containerType == Docker { + cmd = exec.Command("docker", "logs", "pangolin") + } else { + cmd = exec.Command("podman", "logs", "pangolin") + } + output, err := cmd.Output() + if err != nil { + fmt.Println("Warning: Could not fetch Pangolin logs to find setup token.") + return + } + + // Parse for setup token + lines := strings.Split(string(output), "\n") + for i, line := range lines { + if strings.Contains(line, "=== SETUP TOKEN GENERATED ===") || strings.Contains(line, "=== SETUP TOKEN EXISTS ===") { + // Look for "Token: ..." in the next few lines + for j := i + 1; j < i+5 && j < len(lines); j++ { + trimmedLine := strings.TrimSpace(lines[j]) + if strings.Contains(trimmedLine, "Token:") { + // Extract token after "Token:" + tokenStart := strings.Index(trimmedLine, "Token:") + if tokenStart != -1 { + token := strings.TrimSpace(trimmedLine[tokenStart+6:]) + fmt.Printf("Setup token: %s\n", token) + fmt.Println("") + fmt.Println("This token is required to register the first admin account in the web UI at:") + fmt.Printf("https://%s/auth/initial-setup\n", dashboardDomain) + fmt.Println("") + fmt.Println("Save this token securely. It will be invalid after the first admin is created.") + return + } + } + } + } + } + fmt.Println("Warning: Could not find a setup token in Pangolin logs.") +} + +func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomain string) { + fmt.Println("\n=== Setup Token Instructions ===") + fmt.Println("To get your setup token, you need to:") + fmt.Println("") + fmt.Println("1. Start the containers:") + if containerType == Docker { + fmt.Println(" docker-compose up -d") + } else { + fmt.Println(" podman-compose up -d") + } + fmt.Println("") + fmt.Println("2. Wait for the Pangolin container to start and generate the token") + fmt.Println("") + fmt.Println("3. Check the container logs for the setup token:") + if containerType == Docker { + fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'") + } else { + fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'") + } + fmt.Println("") + fmt.Println("4. Look for output like:") + fmt.Println(" === SETUP TOKEN GENERATED ===") + fmt.Println(" Token: [your-token-here]") + fmt.Println(" Use this token on the initial setup page") + fmt.Println("") + fmt.Println("5. Use the token to complete initial setup at:") + fmt.Printf(" https://%s/auth/initial-setup\n", dashboardDomain) + fmt.Println("") + fmt.Println("The setup token is required to register the first admin account.") + fmt.Println("Save it securely - it will be invalid after the first admin is created.") + fmt.Println("================================") +} + func generateRandomSecretKey() string { const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" const length = 32 From a829eb949b58de8ab12e26d30d94fc8cea26ba7a Mon Sep 17 00:00:00 2001 From: Marvin <127591405+Lokowitz@users.noreply.github.com> Date: Sun, 10 Aug 2025 19:02:50 +0000 Subject: [PATCH 06/38] modified: package-lock.json modified: package.json modified: server/nextServer.ts --- package-lock.json | 1129 +++++++++--------------------------------- package.json | 4 +- server/nextServer.ts | 2 +- 3 files changed, 229 insertions(+), 906 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5ef1b28a..0733801d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "drizzle-orm": "0.44.4", "eslint": "9.32.0", "eslint-config-next": "15.4.6", - "express": "4.21.2", + "express": "5.1.0", "express-rate-limit": "7.5.1", "glob": "11.0.3", "helmet": "8.1.0", @@ -103,7 +103,7 @@ "@types/cookie-parser": "1.4.9", "@types/cors": "2.8.19", "@types/crypto-js": "^4.2.2", - "@types/express": "5.0.0", + "@types/express": "5.0.3", "@types/express-session": "^1.18.2", "@types/jmespath": "^0.15.2", "@types/js-yaml": "4.0.9", @@ -760,427 +760,6 @@ "url": "https://github.com/sponsors/nzakas" } }, -<<<<<<< HEAD -======= - "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", - "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", - "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", - "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", - "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", - "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", - "cpu": [ - "arm" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", - "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", - "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", - "cpu": [ - "ppc64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", - "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", - "cpu": [ - "s390x" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", - "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", - "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", - "cpu": [ - "arm64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", - "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", - "cpu": [ - "x64" - ], - "license": "LGPL-3.0-or-later", - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", - "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", - "cpu": [ - "arm" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", - "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", - "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", - "cpu": [ - "ppc64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", - "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", - "cpu": [ - "s390x" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.0" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", - "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", - "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", - "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.0" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", - "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", - "cpu": [ - "wasm32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.4.4" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", - "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", - "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", - "cpu": [ - "ia32" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.34.3", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", - "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0 AND LGPL-3.0-or-later", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, ->>>>>>> main "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -1292,66 +871,6 @@ "fast-glob": "3.3.1" } }, - "node_modules/@next/swc-darwin-arm64": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.6.tgz", - "integrity": "sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.6.tgz", - "integrity": "sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.6.tgz", - "integrity": "sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.6.tgz", - "integrity": "sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@next/swc-linux-x64-gnu": { "version": "15.4.6", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.6.tgz", @@ -1384,36 +903,6 @@ "node": ">= 10" } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.6.tgz", - "integrity": "sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.4.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.6.tgz", - "integrity": "sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@noble/ciphers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", @@ -3246,133 +2735,6 @@ "node": ">= 10" } }, -<<<<<<< HEAD -======= - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", - "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@emnapi/wasi-threads": "^1.0.2", - "@napi-rs/wasm-runtime": "^0.2.11", - "@tybys/wasm-util": "^0.9.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.4.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.0.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.4.3", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.11", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.9.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { - "version": "0.9.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/tslib": { - "version": "2.8.0", - "dev": true, - "inBundle": true, - "license": "0BSD", - "optional": true - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", - "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", - "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, ->>>>>>> main "node_modules/@tailwindcss/postcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", @@ -3485,15 +2847,13 @@ "license": "MIT" }, "node_modules/@types/express": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.0.tgz", - "integrity": "sha512-DvZriSMehGHL1ZNLzi6MidnsDhUZM/x2pRdDIKdwbUNqqwHxMlRdkxtn6/EPKyqKpHqTl/4nRZsRNLpZxZRpPQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, - "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", - "@types/qs": "*", "@types/serve-static": "*" } }, @@ -4021,6 +3381,7 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -4175,12 +3536,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, "node_modules/array-includes": { "version": "3.1.9", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", @@ -4505,44 +3860,24 @@ } }, "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" }, "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" + "node": ">=18" } }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4606,7 +3941,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -5005,10 +4339,9 @@ } }, "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", "dependencies": { "safe-buffer": "5.2.1" }, @@ -5020,7 +4353,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -5358,16 +4690,6 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-libc": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", @@ -5667,8 +4989,7 @@ "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -5686,7 +5007,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -6059,8 +5379,7 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -6482,7 +5801,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -6521,45 +5839,40 @@ } }, "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">= 0.10.0" + "node": ">= 18" }, "funding": { "type": "opencollective", @@ -6581,6 +5894,18 @@ "express": ">= 4.11" } }, + "node_modules/express/node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/express/node_modules/cookie": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", @@ -6590,20 +5915,40 @@ "node": ">= 0.6" } }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" + "node_modules/express/node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" } }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } }, "node_modules/exsolve": { "version": "1.0.7", @@ -6750,38 +6095,21 @@ } }, "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" }, "engines": { "node": ">= 0.8" } }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -6924,12 +6252,11 @@ } }, "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/fs-constants": { @@ -7381,12 +6708,11 @@ } }, "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" @@ -7783,6 +7109,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -8449,19 +7780,20 @@ } }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } @@ -8482,15 +7814,6 @@ "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -8516,18 +7839,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -8736,6 +8047,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -11583,7 +10895,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -11902,7 +11213,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -11948,10 +11258,12 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "engines": { + "node": ">=16" + } }, "node_modules/path-type": { "version": "4.0.0", @@ -12338,12 +11650,11 @@ } }, "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "dependencies": { - "side-channel": "^1.0.6" + "side-channel": "^1.1.0" }, "engines": { "node": ">=0.6" @@ -12386,20 +11697,18 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", - "iconv-lite": "0.4.24", + "iconv-lite": "0.6.3", "unpipe": "1.0.0" }, "engines": { @@ -12873,6 +12182,21 @@ "node": ">=0.10.0" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -12980,8 +12304,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/scheduler": { "version": "0.26.0", @@ -13014,66 +12337,57 @@ } }, "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" } }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "engines": { - "node": ">= 0.8" + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 18" } }, "node_modules/set-function-length": { @@ -14154,13 +13468,32 @@ } }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -14306,7 +13639,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -14426,15 +13758,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", diff --git a/package.json b/package.json index 14013ee8..bf35b6e8 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,7 @@ "drizzle-orm": "0.44.4", "eslint": "9.32.0", "eslint-config-next": "15.4.6", - "express": "4.21.2", + "express": "5.1.0", "express-rate-limit": "7.5.1", "glob": "11.0.3", "helmet": "8.1.0", @@ -121,7 +121,7 @@ "@types/cookie-parser": "1.4.9", "@types/cors": "2.8.19", "@types/crypto-js": "^4.2.2", - "@types/express": "5.0.0", + "@types/express": "5.0.3", "@types/express-session": "^1.18.2", "@types/jmespath": "^0.15.2", "@types/js-yaml": "4.0.9", diff --git a/server/nextServer.ts b/server/nextServer.ts index e12c06e6..4c96d04f 100644 --- a/server/nextServer.ts +++ b/server/nextServer.ts @@ -15,7 +15,7 @@ export async function createNextServer() { const nextServer = express(); - nextServer.all("*", (req, res) => { + nextServer.all("/{*splat}", (req, res) => { const parsedUrl = parse(req.url!, true); return handle(req, res, parsedUrl); }); From 14d7a138a5ad0b399edc90e0a678a70abd77c225 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Aug 2025 02:26:46 +0000 Subject: [PATCH 07/38] Bump the prod-minor-updates group with 2 updates Bumps the prod-minor-updates group with 2 updates: [eslint](https://github.com/eslint/eslint) and [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react). Updates `eslint` from 9.32.0 to 9.33.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v9.32.0...v9.33.0) Updates `lucide-react` from 0.536.0 to 0.539.0 - [Release notes](https://github.com/lucide-icons/lucide/releases) - [Commits](https://github.com/lucide-icons/lucide/commits/0.539.0/packages/lucide-react) --- updated-dependencies: - dependency-name: eslint dependency-version: 9.33.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates - dependency-name: lucide-react dependency-version: 0.539.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: prod-minor-updates ... Signed-off-by: dependabot[bot] --- package-lock.json | 50 +++++++++++++++++++++++------------------------ package.json | 4 ++-- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6458688..08c1833b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,7 +52,7 @@ "cors": "2.8.5", "crypto-js": "^4.2.0", "drizzle-orm": "0.44.4", - "eslint": "9.32.0", + "eslint": "9.33.0", "eslint-config-next": "15.4.6", "express": "4.21.2", "express-rate-limit": "7.5.1", @@ -64,7 +64,7 @@ "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", - "lucide-react": "0.536.0", + "lucide-react": "0.539.0", "moment": "2.30.1", "next": "15.4.6", "next-intl": "^4.3.4", @@ -1339,18 +1339,18 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", - "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", "license": "Apache-2.0", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", - "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" @@ -1383,9 +1383,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", - "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1404,12 +1404,12 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", - "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.1", + "@eslint/core": "^0.15.2", "levn": "^0.4.1" }, "engines": { @@ -7701,19 +7701,19 @@ } }, "node_modules/eslint": { - "version": "9.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", - "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.0", - "@eslint/core": "^0.15.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.32.0", - "@eslint/plugin-kit": "^0.3.4", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -10213,9 +10213,9 @@ } }, "node_modules/lucide-react": { - "version": "0.536.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.536.0.tgz", - "integrity": "sha512-2PgvNa9v+qz4Jt/ni8vPLt4jwoFybXHuubQT8fv4iCW5TjDxkbZjNZZHa485ad73NSEn/jdsEtU57eE1g+ma8A==", + "version": "0.539.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.539.0.tgz", + "integrity": "sha512-VVISr+VF2krO91FeuCrm1rSOLACQUYVy7NQkzrOty52Y8TlTPcXcMdQFj9bYzBgXbWCiywlwSZ3Z8u6a+6bMlg==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" diff --git a/package.json b/package.json index 14013ee8..b7c14617 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "cors": "2.8.5", "crypto-js": "^4.2.0", "drizzle-orm": "0.44.4", - "eslint": "9.32.0", + "eslint": "9.33.0", "eslint-config-next": "15.4.6", "express": "4.21.2", "express-rate-limit": "7.5.1", @@ -82,7 +82,7 @@ "jmespath": "^0.16.0", "js-yaml": "4.1.0", "jsonwebtoken": "^9.0.2", - "lucide-react": "0.536.0", + "lucide-react": "0.539.0", "moment": "2.30.1", "next": "15.4.6", "next-intl": "^4.3.4", From 03c8d82471269395d289d3f2f9c6b22d52868bc3 Mon Sep 17 00:00:00 2001 From: jack rosenberg <56937175+jackrosenberg@users.noreply.github.com> Date: Tue, 12 Aug 2025 17:34:40 +0200 Subject: [PATCH 08/38] fix: fixed api error message in createSite.ts --- server/routers/site/createSite.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index fb1170cd..fc441b5a 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -144,7 +144,7 @@ export async function createSite( return next( createHttpError( HttpCode.BAD_REQUEST, - "Invalid subnet format. Please provide a valid CIDR notation." + "Invalid address format. Please provide a valid IP notation." ) ); } From a4d460e8507bc98ced80aba048ddfee609caf1e1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Aug 2025 02:39:05 +0000 Subject: [PATCH 09/38] Bump the dev-patch-updates group across 1 directory with 6 updates Bumps the dev-patch-updates group with 6 updates in the / directory: | Package | From | To | | --- | --- | --- | | [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `24.2.0` | `24.2.1` | | [@types/pg](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pg) | `8.15.4` | `8.15.5` | | [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.1.9` | `19.1.10` | | [esbuild](https://github.com/evanw/esbuild) | `0.25.6` | `0.25.9` | | [tsx](https://github.com/privatenumber/tsx) | `4.20.3` | `4.20.4` | | [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.39.0` | `8.39.1` | Updates `@types/node` from 24.2.0 to 24.2.1 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Updates `@types/pg` from 8.15.4 to 8.15.5 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pg) Updates `@types/react` from 19.1.9 to 19.1.10 - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react) Updates `esbuild` from 0.25.6 to 0.25.9 - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md) - [Commits](https://github.com/evanw/esbuild/compare/v0.25.6...v0.25.9) Updates `tsx` from 4.20.3 to 4.20.4 - [Release notes](https://github.com/privatenumber/tsx/releases) - [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs) - [Commits](https://github.com/privatenumber/tsx/compare/v4.20.3...v4.20.4) Updates `typescript-eslint` from 8.39.0 to 8.39.1 - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.39.1/packages/typescript-eslint) --- updated-dependencies: - dependency-name: "@types/node" dependency-version: 24.2.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@types/pg" dependency-version: 8.15.5 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: "@types/react" dependency-version: 19.1.10 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: esbuild dependency-version: 0.25.9 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: tsx dependency-version: 4.20.4 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates - dependency-name: typescript-eslint dependency-version: 8.39.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: dev-patch-updates ... Signed-off-by: dependabot[bot] --- package-lock.json | 370 +++++++++++++++++++++++----------------------- package.json | 10 +- 2 files changed, 190 insertions(+), 190 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6458688..37f7ac97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -110,23 +110,23 @@ "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24", "@types/nodemailer": "6.4.17", - "@types/pg": "8.15.4", - "@types/react": "19.1.9", + "@types/pg": "8.15.5", + "@types/react": "19.1.10", "@types/react-dom": "19.1.7", "@types/semver": "^7.7.0", "@types/swagger-ui-express": "^4.1.8", "@types/ws": "8.18.1", "@types/yargs": "17.0.33", "drizzle-kit": "0.31.4", - "esbuild": "0.25.6", + "esbuild": "0.25.9", "esbuild-node-externals": "1.18.0", "postcss": "^8", "react-email": "4.2.8", "tailwindcss": "^4.1.4", "tsc-alias": "1.8.16", - "tsx": "4.20.3", + "tsx": "4.20.4", "typescript": "^5", - "typescript-eslint": "^8.39.0" + "typescript-eslint": "^8.39.1" } }, "node_modules/@alloc/quick-lru": { @@ -844,9 +844,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", - "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], @@ -861,9 +861,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", - "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -878,9 +878,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", - "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -895,9 +895,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", - "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -912,9 +912,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", - "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -929,9 +929,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", - "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -946,9 +946,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", - "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -963,9 +963,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", - "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -980,9 +980,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", - "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -997,9 +997,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", - "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -1014,9 +1014,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", - "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -1031,9 +1031,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", - "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -1048,9 +1048,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", - "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -1065,9 +1065,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", - "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -1082,9 +1082,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", - "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -1099,9 +1099,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", - "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -1116,9 +1116,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", - "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -1133,9 +1133,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", - "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -1150,9 +1150,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", - "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -1167,9 +1167,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", - "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -1184,9 +1184,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", - "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -1201,9 +1201,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", - "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "cpu": [ "arm64" ], @@ -1218,9 +1218,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", - "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -1235,9 +1235,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", - "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -1252,9 +1252,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", - "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -1269,9 +1269,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", - "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -4967,9 +4967,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.0.tgz", - "integrity": "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==", + "version": "24.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", + "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -4987,9 +4987,9 @@ } }, "node_modules/@types/pg": { - "version": "8.15.4", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.4.tgz", - "integrity": "sha512-I6UNVBAoYbvuWkkU3oosC8yxqH21f4/Jc4DK71JLG3dT2mdlGe1z+ep/LQGXaKaOgcvUrsQoPRqfgtMcvZiJhg==", + "version": "8.15.5", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.5.tgz", + "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5013,9 +5013,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz", - "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", + "version": "19.1.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.10.tgz", + "integrity": "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -5107,16 +5107,16 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", - "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.1.tgz", + "integrity": "sha512-yYegZ5n3Yr6eOcqgj2nJH8cH/ZZgF+l0YIdKILSDjYFRjgYQMgv/lRjV5Z7Up04b9VYUondt8EPMqg7kTWgJ2g==", "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/type-utils": "8.39.0", - "@typescript-eslint/utils": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/type-utils": "8.39.1", + "@typescript-eslint/utils": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "graphemer": "^1.4.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", @@ -5130,7 +5130,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/parser": "^8.39.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -5145,15 +5145,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", - "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.1.tgz", + "integrity": "sha512-pUXGCuHnnKw6PyYq93lLRiZm3vjuslIy7tus1lIQTYVK9bL8XBgJnCWm8a0KcTtHC84Yya1Q6rtll+duSMj0dg==", "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4" }, "engines": { @@ -5169,13 +5169,13 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", - "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.1.tgz", + "integrity": "sha512-8fZxek3ONTwBu9ptw5nCKqZOSkXshZB7uAxuFF0J/wTMkKydjXCzqqga7MlFMpHi9DoG4BadhmTkITBcg8Aybw==", "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.39.0", - "@typescript-eslint/types": "^8.39.0", + "@typescript-eslint/tsconfig-utils": "^8.39.1", + "@typescript-eslint/types": "^8.39.1", "debug": "^4.3.4" }, "engines": { @@ -5190,13 +5190,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", - "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.1.tgz", + "integrity": "sha512-RkBKGBrjgskFGWuyUGz/EtD8AF/GW49S21J8dvMzpJitOF1slLEbbHnNEtAHtnDAnx8qDEdRrULRnWVx27wGBw==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0" + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5207,9 +5207,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", - "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.1.tgz", + "integrity": "sha512-ePUPGVtTMR8XMU2Hee8kD0Pu4NDE1CN9Q1sxGSGd/mbOtGZDM7pnhXNJnzW63zk/q+Z54zVzj44HtwXln5CvHA==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5223,14 +5223,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", - "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.1.tgz", + "integrity": "sha512-gu9/ahyatyAdQbKeHnhT4R+y3YLtqqHyvkfDxaBYk97EcbfChSJXyaJnIL3ygUv7OuZatePHmQvuH5ru0lnVeA==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/utils": "8.39.1", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -5247,9 +5247,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", - "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.1.tgz", + "integrity": "sha512-7sPDKQQp+S11laqTrhHqeAbsCfMkwJMrV7oTDvtDds4mEofJYir414bYKUEb8YPUm9QL3U+8f6L6YExSoAGdQw==", "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5260,15 +5260,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", - "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.1.tgz", + "integrity": "sha512-EKkpcPuIux48dddVDXyQBlKdeTPMmALqBUbEk38McWv0qVEZwOpVJBi7ugK5qVNgeuYjGNQxrrnoM/5+TI/BPw==", "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.39.0", - "@typescript-eslint/tsconfig-utils": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/visitor-keys": "8.39.0", + "@typescript-eslint/project-service": "8.39.1", + "@typescript-eslint/tsconfig-utils": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/visitor-keys": "8.39.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -5340,15 +5340,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", - "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.1.tgz", + "integrity": "sha512-VF5tZ2XnUSTuiqZFXCZfZs1cgkdd3O/sSYmdo2EpSyDlC86UM/8YytTmKnehOW3TGAlivqTDT6bS87B/GQ/jyg==", "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.39.0", - "@typescript-eslint/types": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0" + "@typescript-eslint/scope-manager": "8.39.1", + "@typescript-eslint/types": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5363,12 +5363,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", - "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.1.tgz", + "integrity": "sha512-W8FQi6kEh2e8zVhQ0eeRnxdvIoOkAp/CPAahcNio6nO9dsIwb9b34z90KOlheoyuVf6LSOEdjlkxSkapNEc+4A==", "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/types": "8.39.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -7603,9 +7603,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.6", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", - "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7616,32 +7616,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.6", - "@esbuild/android-arm": "0.25.6", - "@esbuild/android-arm64": "0.25.6", - "@esbuild/android-x64": "0.25.6", - "@esbuild/darwin-arm64": "0.25.6", - "@esbuild/darwin-x64": "0.25.6", - "@esbuild/freebsd-arm64": "0.25.6", - "@esbuild/freebsd-x64": "0.25.6", - "@esbuild/linux-arm": "0.25.6", - "@esbuild/linux-arm64": "0.25.6", - "@esbuild/linux-ia32": "0.25.6", - "@esbuild/linux-loong64": "0.25.6", - "@esbuild/linux-mips64el": "0.25.6", - "@esbuild/linux-ppc64": "0.25.6", - "@esbuild/linux-riscv64": "0.25.6", - "@esbuild/linux-s390x": "0.25.6", - "@esbuild/linux-x64": "0.25.6", - "@esbuild/netbsd-arm64": "0.25.6", - "@esbuild/netbsd-x64": "0.25.6", - "@esbuild/openbsd-arm64": "0.25.6", - "@esbuild/openbsd-x64": "0.25.6", - "@esbuild/openharmony-arm64": "0.25.6", - "@esbuild/sunos-x64": "0.25.6", - "@esbuild/win32-arm64": "0.25.6", - "@esbuild/win32-ia32": "0.25.6", - "@esbuild/win32-x64": "0.25.6" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/esbuild-node-externals": { @@ -16208,9 +16208,9 @@ } }, "node_modules/tsx": { - "version": "4.20.3", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", - "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "version": "4.20.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.4.tgz", + "integrity": "sha512-yyxBKfORQ7LuRt/BQKBXrpcq59ZvSW0XxwfjAt3w2/8PmdxaFzijtMhTawprSHhpzeM5BgU2hXHG3lklIERZXg==", "dev": true, "license": "MIT", "dependencies": { @@ -16361,16 +16361,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.39.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.0.tgz", - "integrity": "sha512-lH8FvtdtzcHJCkMOKnN73LIn6SLTpoojgJqDAxPm1jCR14eWSGPX8ul/gggBdPMk/d5+u9V854vTYQ8T5jF/1Q==", + "version": "8.39.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.39.1.tgz", + "integrity": "sha512-GDUv6/NDYngUlNvwaHM1RamYftxf782IyEDbdj3SeaIHHv8fNQVRC++fITT7kUJV/5rIA/tkoRSSskt6osEfqg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.39.0", - "@typescript-eslint/parser": "8.39.0", - "@typescript-eslint/typescript-estree": "8.39.0", - "@typescript-eslint/utils": "8.39.0" + "@typescript-eslint/eslint-plugin": "8.39.1", + "@typescript-eslint/parser": "8.39.1", + "@typescript-eslint/typescript-estree": "8.39.1", + "@typescript-eslint/utils": "8.39.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/package.json b/package.json index 14013ee8..d7b3814d 100644 --- a/package.json +++ b/package.json @@ -128,23 +128,23 @@ "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24", "@types/nodemailer": "6.4.17", - "@types/pg": "8.15.4", - "@types/react": "19.1.9", + "@types/pg": "8.15.5", + "@types/react": "19.1.10", "@types/react-dom": "19.1.7", "@types/semver": "^7.7.0", "@types/swagger-ui-express": "^4.1.8", "@types/ws": "8.18.1", "@types/yargs": "17.0.33", "drizzle-kit": "0.31.4", - "esbuild": "0.25.6", + "esbuild": "0.25.9", "esbuild-node-externals": "1.18.0", "postcss": "^8", "react-email": "4.2.8", "tailwindcss": "^4.1.4", "tsc-alias": "1.8.16", - "tsx": "4.20.3", + "tsx": "4.20.4", "typescript": "^5", - "typescript-eslint": "^8.39.0" + "typescript-eslint": "^8.39.1" }, "overrides": { "emblor": { From af638d666cf490277493e000a224510b8758f944 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 12 Aug 2025 21:34:07 -0700 Subject: [PATCH 10/38] Dont look for port if not root; causes permission --- install/main.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/install/main.go b/install/main.go index d380591b..b08f0073 100644 --- a/install/main.go +++ b/install/main.go @@ -77,16 +77,16 @@ func main() { fmt.Println("Lets get started!") fmt.Println("") + if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS + for _, p := range []int{80, 443} { + if err := checkPortsAvailable(p); err != nil { + fmt.Fprintln(os.Stderr, err) - for _, p := range []int{80, 443} { - if err := checkPortsAvailable(p); err != nil { - fmt.Fprintln(os.Stderr, err) - - fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly") - os.Exit(1) - } - } - + fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly") + os.Exit(1) + } + } + } reader := bufio.NewReader(os.Stdin) inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker") From c6d78680fb8b5bc144461b26994107cfbef3d393 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:03 -0700 Subject: [PATCH 11/38] New translations en-us.json (Norwegian Bokmal) --- messages/nb-NO.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/nb-NO.json b/messages/nb-NO.json index 92b52d01..f2b0924b 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN må være nøyaktig 6 siffer", "pincodeRequirementsChars": "PIN må kun inneholde tall", "passwordRequirementsLength": "Passord må være minst 1 tegn langt", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP må være minst 1 tegn lang.", "otpEmailSent": "OTP sendt", "otpEmailSentDescription": "En OTP er sendt til din e-post", @@ -967,6 +985,9 @@ "actionDeleteSite": "Slett område", "actionGetSite": "Hent område", "actionListSites": "List opp områder", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Oppdater område", "actionListSiteRoles": "List opp tillatte områderoller", "actionCreateResource": "Opprett ressurs", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Aktiver offentlig proxy", "resourceEnableProxyDescription": "Aktiver offentlig proxying til denne ressursen. Dette gir tilgang til ressursen fra utsiden av nettverket gjennom skyen på en åpen port. Krever Traefik-konfigurasjon.", "externalProxyEnabled": "Ekstern proxy aktivert" -} +} \ No newline at end of file From cea7190453b2de11a5fa126ae2b03775f3df7833 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:04 -0700 Subject: [PATCH 12/38] New translations en-us.json (French) --- messages/fr-FR.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/fr-FR.json b/messages/fr-FR.json index bb1a4ac3..16e286d9 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "Le code PIN doit comporter exactement 6 chiffres", "pincodeRequirementsChars": "Le code PIN ne doit contenir que des chiffres", "passwordRequirementsLength": "Le mot de passe doit comporter au moins 1 caractère", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "L'OTP doit comporter au moins 1 caractère", "otpEmailSent": "OTP envoyé", "otpEmailSentDescription": "Un OTP a été envoyé à votre e-mail", @@ -967,6 +985,9 @@ "actionDeleteSite": "Supprimer un site", "actionGetSite": "Obtenir un site", "actionListSites": "Lister les sites", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Mettre à jour un site", "actionListSiteRoles": "Lister les rôles autorisés du site", "actionCreateResource": "Créer une ressource", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Activer le proxy public", "resourceEnableProxyDescription": "Activez le proxy public vers cette ressource. Cela permet d'accéder à la ressource depuis l'extérieur du réseau via le cloud sur un port ouvert. Nécessite la configuration de Traefik.", "externalProxyEnabled": "Proxy externe activé" -} +} \ No newline at end of file From cf12d3ee56b1931c722323dde1cea1c7f4967929 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:06 -0700 Subject: [PATCH 13/38] New translations en-us.json (Spanish) --- messages/es-ES.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/es-ES.json b/messages/es-ES.json index 7fabb18c..3f862cea 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "El PIN debe tener exactamente 6 dígitos", "pincodeRequirementsChars": "El PIN sólo debe contener números", "passwordRequirementsLength": "La contraseña debe tener al menos 1 carácter", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP debe tener al menos 1 carácter", "otpEmailSent": "OTP enviado", "otpEmailSentDescription": "Un OTP ha sido enviado a tu correo electrónico", @@ -967,6 +985,9 @@ "actionDeleteSite": "Eliminar sitio", "actionGetSite": "Obtener sitio", "actionListSites": "Listar sitios", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Actualizar sitio", "actionListSiteRoles": "Lista de roles permitidos del sitio", "actionCreateResource": "Crear Recurso", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Habilitar proxy público", "resourceEnableProxyDescription": "Habilite el proxy público para este recurso. Esto permite el acceso al recurso desde fuera de la red a través de la nube en un puerto abierto. Requiere configuración de Traefik.", "externalProxyEnabled": "Proxy externo habilitado" -} +} \ No newline at end of file From 0a13b04c55ed69774fd6065e20ee99b78cc0d4de Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:07 -0700 Subject: [PATCH 14/38] New translations en-us.json (Bulgarian) --- messages/bg-BG.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/bg-BG.json b/messages/bg-BG.json index 738fe3ed..1d982bc6 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN must be exactly 6 digits", "pincodeRequirementsChars": "PIN must only contain numbers", "passwordRequirementsLength": "Password must be at least 1 character long", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP must be at least 1 character long", "otpEmailSent": "OTP Sent", "otpEmailSentDescription": "An OTP has been sent to your email", @@ -967,6 +985,9 @@ "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", "actionListSites": "List Sites", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Update Site", "actionListSiteRoles": "List Allowed Site Roles", "actionCreateResource": "Create Resource", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" -} +} \ No newline at end of file From e9c2868998b6de1833dbac83dd310949b93f09cb Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:09 -0700 Subject: [PATCH 15/38] New translations en-us.json (Czech) --- messages/cs-CZ.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index 6fe79036..d21f37c2 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN must be exactly 6 digits", "pincodeRequirementsChars": "PIN must only contain numbers", "passwordRequirementsLength": "Password must be at least 1 character long", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP must be at least 1 character long", "otpEmailSent": "OTP Sent", "otpEmailSentDescription": "An OTP has been sent to your email", @@ -967,6 +985,9 @@ "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", "actionListSites": "List Sites", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Update Site", "actionListSiteRoles": "List Allowed Site Roles", "actionCreateResource": "Create Resource", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" -} +} \ No newline at end of file From 9dc73efa3a96f1ac77acf3539e110c5d59c847c8 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:10 -0700 Subject: [PATCH 16/38] New translations en-us.json (German) --- messages/de-DE.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/de-DE.json b/messages/de-DE.json index e82fb44a..fab7e28a 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN muss genau 6 Ziffern lang sein", "pincodeRequirementsChars": "PIN darf nur Zahlen enthalten", "passwordRequirementsLength": "Passwort muss mindestens 1 Zeichen lang sein", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP muss mindestens 1 Zeichen lang sein", "otpEmailSent": "OTP gesendet", "otpEmailSentDescription": "Ein OTP wurde an Ihre E-Mail gesendet", @@ -967,6 +985,9 @@ "actionDeleteSite": "Standort löschen", "actionGetSite": "Standort abrufen", "actionListSites": "Standorte auflisten", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Standorte aktualisieren", "actionListSiteRoles": "Erlaubte Standort-Rollen auflisten", "actionCreateResource": "Ressource erstellen", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Öffentlichen Proxy aktivieren", "resourceEnableProxyDescription": "Ermöglichen Sie öffentliches Proxieren zu dieser Ressource. Dies ermöglicht den Zugriff auf die Ressource von außerhalb des Netzwerks durch die Cloud über einen offenen Port. Erfordert Traefik-Config.", "externalProxyEnabled": "Externer Proxy aktiviert" -} +} \ No newline at end of file From 5f36b13408eb8a21d4468b5b0c20f08be08a3c9b Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:11 -0700 Subject: [PATCH 17/38] New translations en-us.json (Italian) --- messages/it-IT.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/it-IT.json b/messages/it-IT.json index 651259eb..82753fc7 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "Il PIN deve essere esattamente di 6 cifre", "pincodeRequirementsChars": "Il PIN deve contenere solo numeri", "passwordRequirementsLength": "La password deve essere lunga almeno 1 carattere", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "L'OTP deve essere lungo almeno 1 carattere", "otpEmailSent": "OTP Inviato", "otpEmailSentDescription": "Un OTP è stato inviato alla tua email", @@ -967,6 +985,9 @@ "actionDeleteSite": "Elimina Sito", "actionGetSite": "Ottieni Sito", "actionListSites": "Elenca Siti", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Aggiorna Sito", "actionListSiteRoles": "Elenca Ruoli Sito Consentiti", "actionCreateResource": "Crea Risorsa", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Abilita Proxy Pubblico", "resourceEnableProxyDescription": "Abilita il proxy pubblico a questa risorsa. Consente l'accesso alla risorsa dall'esterno della rete tramite il cloud su una porta aperta. Richiede la configurazione di Traefik.", "externalProxyEnabled": "Proxy Esterno Abilitato" -} +} \ No newline at end of file From c70eaa00968f1fa27772d89eada7690b83b0fedd Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:13 -0700 Subject: [PATCH 18/38] New translations en-us.json (Korean) --- messages/ko-KR.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 0c28db0f..4e6fb851 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN은 정확히 6자리여야 합니다", "pincodeRequirementsChars": "PIN은 숫자만 포함해야 합니다.", "passwordRequirementsLength": "비밀번호는 최소 1자 이상이어야 합니다", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP는 최소 1자 이상이어야 합니다", "otpEmailSent": "OTP 전송됨", "otpEmailSentDescription": "OTP가 귀하의 이메일로 전송되었습니다.", @@ -967,6 +985,9 @@ "actionDeleteSite": "사이트 삭제", "actionGetSite": "사이트 가져오기", "actionListSites": "사이트 목록", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "사이트 업데이트", "actionListSiteRoles": "허용된 사이트 역할 목록", "actionCreateResource": "리소스 생성", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" -} +} \ No newline at end of file From 168056d595d0ee41b526a4928f7f72ef07790cc5 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:14 -0700 Subject: [PATCH 19/38] New translations en-us.json (Dutch) --- messages/nl-NL.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/nl-NL.json b/messages/nl-NL.json index 68ccfeae..aa8859cf 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "Pincode moet precies 6 cijfers zijn", "pincodeRequirementsChars": "Pincode mag alleen cijfers bevatten", "passwordRequirementsLength": "Wachtwoord moet ten minste 1 teken lang zijn", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP moet minstens 1 teken lang zijn", "otpEmailSent": "OTP verzonden", "otpEmailSentDescription": "Een OTP is naar uw e-mail verzonden", @@ -967,6 +985,9 @@ "actionDeleteSite": "Site verwijderen", "actionGetSite": "Site ophalen", "actionListSites": "Sites weergeven", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Site bijwerken", "actionListSiteRoles": "Toon toegestane sitenollen", "actionCreateResource": "Bron maken", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Openbare proxy inschakelen", "resourceEnableProxyDescription": "Schakel publieke proxy in voor deze resource. Dit maakt toegang tot de resource mogelijk vanuit het netwerk via de cloud met een open poort. Vereist Traefik-configuratie.", "externalProxyEnabled": "Externe Proxy Ingeschakeld" -} +} \ No newline at end of file From 5f09f970322e392a6d579d6dffc04421341df064 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:15 -0700 Subject: [PATCH 20/38] New translations en-us.json (Polish) --- messages/pl-PL.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 0df783a5..edf39a6a 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN musi składać się dokładnie z 6 cyfr", "pincodeRequirementsChars": "PIN może zawierać tylko cyfry", "passwordRequirementsLength": "Hasło musi mieć co najmniej 1 znak", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "Kod jednorazowy musi mieć co najmniej 1 znak", "otpEmailSent": "Kod jednorazowy wysłany", "otpEmailSentDescription": "Kod jednorazowy został wysłany na Twój e-mail", @@ -967,6 +985,9 @@ "actionDeleteSite": "Usuń witrynę", "actionGetSite": "Pobierz witrynę", "actionListSites": "Lista witryn", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Aktualizuj witrynę", "actionListSiteRoles": "Lista dozwolonych ról witryny", "actionCreateResource": "Utwórz zasób", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Włącz publiczny proxy", "resourceEnableProxyDescription": "Włącz publiczne proxy dla tego zasobu. To umożliwia dostęp do zasobu spoza sieci przez chmurę na otwartym porcie. Wymaga konfiguracji Traefik.", "externalProxyEnabled": "Zewnętrzny Proxy Włączony" -} +} \ No newline at end of file From c8dda4f90dc87998561b27c5039cbc19e10fbc46 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:17 -0700 Subject: [PATCH 21/38] New translations en-us.json (Portuguese) --- messages/pt-PT.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/pt-PT.json b/messages/pt-PT.json index c126ba1c..ad32ce79 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "O PIN deve ter exatamente 6 dígitos", "pincodeRequirementsChars": "O PIN deve conter apenas números", "passwordRequirementsLength": "A palavra-passe deve ter pelo menos 1 caractere", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "O OTP deve ter pelo menos 1 caractere", "otpEmailSent": "OTP Enviado", "otpEmailSentDescription": "Um OTP foi enviado para o seu email", @@ -967,6 +985,9 @@ "actionDeleteSite": "Eliminar Site", "actionGetSite": "Obter Site", "actionListSites": "Listar Sites", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Atualizar Site", "actionListSiteRoles": "Listar Funções Permitidas do Site", "actionCreateResource": "Criar Recurso", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Ativar Proxy Público", "resourceEnableProxyDescription": "Permite proxy público para este recurso. Isso permite o acesso ao recurso de fora da rede através da nuvem em uma porta aberta. Requer configuração do Traefik.", "externalProxyEnabled": "Proxy Externo Habilitado" -} +} \ No newline at end of file From 40f520086c15d749326c047a75811088e4d0b8ca Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:18 -0700 Subject: [PATCH 22/38] New translations en-us.json (Russian) --- messages/ru-RU.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/ru-RU.json b/messages/ru-RU.json index 62360ecc..f9a49a3f 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN должен состоять ровно из 6 цифр", "pincodeRequirementsChars": "PIN должен содержать только цифры", "passwordRequirementsLength": "Пароль должен быть не менее 1 символа", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP должен быть не менее 1 символа", "otpEmailSent": "OTP отправлен", "otpEmailSentDescription": "OTP был отправлен на ваш email", @@ -967,6 +985,9 @@ "actionDeleteSite": "Удалить сайт", "actionGetSite": "Получить сайт", "actionListSites": "Список сайтов", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Обновить сайт", "actionListSiteRoles": "Список разрешенных ролей сайта", "actionCreateResource": "Создать ресурс", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled" -} +} \ No newline at end of file From 75f97c4a31af331e65c86a54403c942f56331ee2 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:20 -0700 Subject: [PATCH 23/38] New translations en-us.json (Turkish) --- messages/tr-TR.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/tr-TR.json b/messages/tr-TR.json index 8b9e2450..103a94a5 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN kesinlikle 6 haneli olmalıdır", "pincodeRequirementsChars": "PIN sadece numaralardan oluşmalıdır", "passwordRequirementsLength": "Şifre en az 1 karakter uzunluğunda olmalıdır", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP en az 1 karakter uzunluğunda olmalıdır", "otpEmailSent": "OTP Gönderildi", "otpEmailSentDescription": "E-posta adresinize bir OTP gönderildi", @@ -967,6 +985,9 @@ "actionDeleteSite": "Siteyi Sil", "actionGetSite": "Siteyi Al", "actionListSites": "Siteleri Listele", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Siteyi Güncelle", "actionListSiteRoles": "İzin Verilen Site Rolleri Listele", "actionCreateResource": "Kaynak Oluştur", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "Genel Proxy'i Etkinleştir", "resourceEnableProxyDescription": "Bu kaynağa genel proxy erişimini etkinleştirin. Bu sayede ağ dışından açık bir port üzerinden kaynağa bulut aracılığıyla erişim sağlanır. Traefik yapılandırması gereklidir.", "externalProxyEnabled": "Dış Proxy Etkinleştirildi" -} +} \ No newline at end of file From 297991ef5f1ecc2f6102ece1cb0054632fd814b6 Mon Sep 17 00:00:00 2001 From: Owen Schwartz Date: Tue, 12 Aug 2025 22:16:21 -0700 Subject: [PATCH 24/38] New translations en-us.json (Chinese Simplified) --- messages/zh-CN.json | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 6172738c..b7b29307 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -833,6 +833,24 @@ "pincodeRequirementsLength": "PIN码必须是6位数字", "pincodeRequirementsChars": "PIN 必须只包含数字", "passwordRequirementsLength": "密码必须至少 1 个字符长", + "passwordRequirementsTitle": "Password requirements:", + "passwordRequirementLength": "At least 8 characters long", + "passwordRequirementUppercase": "At least one uppercase letter", + "passwordRequirementLowercase": "At least one lowercase letter", + "passwordRequirementNumber": "At least one number", + "passwordRequirementSpecial": "At least one special character", + "passwordRequirementsMet": "✓ Password meets all requirements", + "passwordStrength": "Password strength", + "passwordStrengthWeak": "Weak", + "passwordStrengthMedium": "Medium", + "passwordStrengthStrong": "Strong", + "passwordRequirements": "Requirements:", + "passwordRequirementLengthText": "8+ characters", + "passwordRequirementUppercaseText": "Uppercase letter (A-Z)", + "passwordRequirementLowercaseText": "Lowercase letter (a-z)", + "passwordRequirementNumberText": "Number (0-9)", + "passwordRequirementSpecialText": "Special character (!@#$%...)", + "passwordsDoNotMatch": "Passwords do not match", "otpEmailRequirementsLength": "OTP 必须至少 1 个字符长", "otpEmailSent": "OTP 已发送", "otpEmailSentDescription": "OTP 已经发送到您的电子邮件", @@ -967,6 +985,9 @@ "actionDeleteSite": "删除站点", "actionGetSite": "获取站点", "actionListSites": "站点列表", + "setupToken": "Setup Token", + "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenRequired": "Setup token is required", "actionUpdateSite": "更新站点", "actionListSiteRoles": "允许站点角色列表", "actionCreateResource": "创建资源", @@ -1324,4 +1345,4 @@ "resourceEnableProxy": "启用公共代理", "resourceEnableProxyDescription": "启用到此资源的公共代理。这允许外部网络通过开放端口访问资源。需要 Traefik 配置。", "externalProxyEnabled": "外部代理已启用" -} +} \ No newline at end of file From 0f50981573831394c9c3ba431f120e46784ed092 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 11:15:06 -0700 Subject: [PATCH 25/38] Update lock --- package-lock.json | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/package-lock.json b/package-lock.json index 3c16f842..647db0bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1670,6 +1670,66 @@ "fast-glob": "3.3.1" } }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.4.6.tgz", + "integrity": "sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.4.6.tgz", + "integrity": "sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.4.6.tgz", + "integrity": "sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.4.6.tgz", + "integrity": "sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@next/swc-linux-x64-gnu": { "version": "15.4.6", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.4.6.tgz", @@ -1702,6 +1762,36 @@ "node": ">= 10" } }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.4.6.tgz", + "integrity": "sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.4.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.4.6.tgz", + "integrity": "sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@noble/ciphers": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", From c244dc9c0cc689d20929fdd3fdf5c5de71af01c0 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 11:15:14 -0700 Subject: [PATCH 26/38] Add accept clients to install --- messages/en-US.json | 7 +- .../[orgId]/settings/sites/create/page.tsx | 114 ++++++++++-------- 2 files changed, 68 insertions(+), 53 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index f8b3f8b9..6ac72d8a 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1344,5 +1344,10 @@ "remoteSubnetsDescription": "Add CIDR ranges that can be accessed from this site remotely using clients. Use format like 10.0.0.0/24. This ONLY applies to VPN client connectivity.", "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", - "externalProxyEnabled": "External Proxy Enabled" + "externalProxyEnabled": "External Proxy Enabled", + "siteConfiguration": "Configuration", + "siteAcceptClientConnections": "Accept client connections", + "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", + "siteAddress": "Site Address", + "siteAddressDescription": "Specify the IP address of the host for clients to connect to." } \ No newline at end of file diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index d6ab64d0..4d5171f1 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -206,8 +206,10 @@ PersistentKeepalive = 5`; acceptClients: boolean = false ) => { const acceptClientsFlag = acceptClients ? " --accept-clients" : ""; - const acceptClientsEnv = acceptClients ? "\n - ACCEPT_CLIENTS=true" : ""; - + const acceptClientsEnv = acceptClients + ? "\n - ACCEPT_CLIENTS=true" + : ""; + const commands = { mac: { "Apple Silicon (arm64)": [ @@ -388,7 +390,7 @@ WantedBy=default.target` case "freebsd": return ; case "nixos": - return ; + return ; default: return ; } @@ -566,6 +568,11 @@ WantedBy=default.target` load(); }, []); + // Sync form acceptClients value with local state + useEffect(() => { + form.setValue("acceptClients", acceptClients); + }, [acceptClients, form]); + return ( <>

@@ -626,7 +633,7 @@ WantedBy=default.target` render={({ field }) => ( - Site Address + {t("siteAddress")} - Specify the - IP address - of the host - for clients - to connect - to. + {t("siteAddressDescription")} )} /> )} - {form.watch("method") === - "newt" && ( - ( - -
- { - const value = checked as boolean; - field.onChange(value); - setAcceptClients(value); - // Re-hydrate commands with new acceptClients value - if (newtId && newtSecret) { - hydrateCommands( - newtId, - newtSecret, - env.app.dashboardUrl, - newtVersion, - value - ); - } - }} - /> - -
- - Allow other devices to connect through this newt instance as a gateway. - - -
- )} - /> - )} @@ -903,6 +863,56 @@ WantedBy=default.target` ) )}
+ +
+

+ {t("siteConfiguration")} +

+
+ { + const value = + checked as boolean; + setAcceptClients( + value + ); + form.setValue( + "acceptClients", + value + ); + // Re-hydrate commands with new acceptClients value + if ( + newtId && + newtSecret && + newtVersion + ) { + hydrateCommands( + newtId, + newtSecret, + env.app + .dashboardUrl, + newtVersion, + value + ); + } + }} + /> + +
+

+ {t("siteAcceptClientConnectionsDescription")} +

+
+

{t("commands")} From 50fc2fc74e80fed95fdb01cb33ee0a09e7a0516a Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 11:30:21 -0700 Subject: [PATCH 27/38] Add newt install command --- messages/en-US.json | 2 +- .../[orgId]/settings/sites/create/page.tsx | 108 ++++++++++-------- 2 files changed, 62 insertions(+), 48 deletions(-) diff --git a/messages/en-US.json b/messages/en-US.json index 6ac72d8a..1a3fdfa8 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1346,7 +1346,7 @@ "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled", "siteConfiguration": "Configuration", - "siteAcceptClientConnections": "Accept client connections", + "siteAcceptClientConnections": "Accept Client Connections", "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", "siteAddress": "Site Address", "siteAddressDescription": "Specify the IP address of the host for clients to connect to." diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index 4d5171f1..c1c97c44 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -43,7 +43,7 @@ import { FaWindows } from "react-icons/fa"; import { SiNixos } from "react-icons/si"; -import { Checkbox } from "@app/components/ui/checkbox"; +import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { generateKeypair } from "../[niceId]/wireguardConfig"; import { createApiClient, formatAxiosError } from "@app/lib/api"; @@ -72,6 +72,7 @@ interface TunnelTypeOption { type Commands = { mac: Record; linux: Record; + freebsd: Record; windows: Record; docker: Record; podman: Record; @@ -212,46 +213,46 @@ PersistentKeepalive = 5`; const commands = { mac: { - "Apple Silicon (arm64)": [ - `curl -L -o newt "https://github.com/fosrl/newt/releases/download/${version}/newt_darwin_arm64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ], - "Intel x64 (amd64)": [ - `curl -L -o newt "https://github.com/fosrl/newt/releases/download/${version}/newt_darwin_amd64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + All: [ + `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] + // "Intel x64 (amd64)": [ + // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ] }, linux: { - amd64: [ - `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_amd64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ], - arm64: [ - `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_arm64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ], - arm32: [ - `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_arm32" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ], - arm32v6: [ - `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_arm32v6" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ], - riscv64: [ - `wget -O newt "https://github.com/fosrl/newt/releases/download/${version}/newt_linux_riscv64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + All: [ + `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] + // arm64: [ + // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ], + // arm32: [ + // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ], + // arm32v6: [ + // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ], + // riscv64: [ + // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ] }, freebsd: { - amd64: [ - `fetch -o newt "https://github.com/fosrl/newt/releases/download/${version}/newt_freebsd_amd64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ], - arm64: [ - `fetch -o newt "https://github.com/fosrl/newt/releases/download/${version}/newt_freebsd_arm64" && chmod +x ./newt`, - `./newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + All: [ + `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ] + // arm64: [ + // `curl -fsSL https://digpangolin.com/get-newt.sh | bash`, + // `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ] }, windows: { x64: [ @@ -299,12 +300,12 @@ WantedBy=default.target` ] }, nixos: { - x86_64: [ + All: [ `nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` ], - aarch64: [ - `nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` - ] + // aarch64: [ + // `nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}` + // ] } }; setCommands(commands); @@ -313,9 +314,11 @@ WantedBy=default.target` const getArchitectures = () => { switch (platform) { case "linux": - return ["amd64", "arm64", "arm32", "arm32v6", "riscv64"]; + // return ["amd64", "arm64", "arm32", "arm32v6", "riscv64"]; + return ["All"]; case "mac": - return ["Apple Silicon (arm64)", "Intel x64 (amd64)"]; + // return ["Apple Silicon (arm64)", "Intel x64 (amd64)"]; + return ["All"]; case "windows": return ["x64"]; case "docker": @@ -323,9 +326,11 @@ WantedBy=default.target` case "podman": return ["Podman Quadlet", "Podman Run"]; case "freebsd": - return ["amd64", "arm64"]; + // return ["amd64", "arm64"]; + return ["All"]; case "nixos": - return ["x86_64", "aarch64"]; + // return ["x86_64", "aarch64"]; + return ["All"]; default: return ["x64"]; } @@ -633,7 +638,9 @@ WantedBy=default.target` render={({ field }) => ( - {t("siteAddress")} + {t( + "siteAddress" + )} - {t("siteAddressDescription")} + {t( + "siteAddressDescription" + )} )} @@ -869,7 +878,7 @@ WantedBy=default.target` {t("siteConfiguration")}

-

- {t("siteAcceptClientConnectionsDescription")} + {t( + "siteAcceptClientConnectionsDescription" + )}

From 16e876ab68946390a0437012524d3ebad3a24bf7 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 12:13:47 -0700 Subject: [PATCH 28/38] Clean up checkbox --- src/app/[orgId]/settings/sites/create/page.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/app/[orgId]/settings/sites/create/page.tsx b/src/app/[orgId]/settings/sites/create/page.tsx index c1c97c44..cdf596cf 100644 --- a/src/app/[orgId]/settings/sites/create/page.tsx +++ b/src/app/[orgId]/settings/sites/create/page.tsx @@ -880,6 +880,7 @@ WantedBy=default.target`
-
-

+

{t( "siteAcceptClientConnectionsDescription" )} From 9987b35b60d613df089cabbf5801be4e833352e9 Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 12:26:38 -0700 Subject: [PATCH 29/38] Update package lock again --- package-lock.json | 2287 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 2063 insertions(+), 224 deletions(-) diff --git a/package-lock.json b/package-lock.json index 647db0bc..e0d69052 100644 --- a/package-lock.json +++ b/package-lock.json @@ -281,9 +281,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.1.tgz", - "integrity": "sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -360,6 +360,37 @@ "@noble/ciphers": "^1.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild-kit/core-utils": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz", @@ -1386,31 +1417,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", - "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", - "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.3.tgz", + "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.2", + "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", - "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.5.tgz", + "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.7.2" + "@floating-ui/dom": "^1.7.3" }, "peerDependencies": { "react": ">=16.8.0", @@ -1559,6 +1590,424 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz", + "integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz", + "integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz", + "integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz", + "integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz", + "integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz", + "integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz", + "integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz", + "integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz", + "integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz", + "integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz", + "integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz", + "integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz", + "integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz", + "integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz", + "integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz", + "integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz", + "integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz", + "integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz", + "integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.4" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz", + "integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz", + "integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz", + "integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/balanced-match": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", @@ -1601,7 +2050,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", - "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -1611,9 +2059,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "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": { @@ -1632,16 +2080,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "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.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1655,6 +2103,18 @@ "integrity": "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==", "license": "MIT" }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@next/env": { "version": "15.4.6", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.4.6.tgz", @@ -1677,6 +2137,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1692,6 +2153,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1707,6 +2169,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1722,6 +2185,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "linux" @@ -1769,6 +2233,7 @@ "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -1784,6 +2249,7 @@ "cpu": [ "x64" ], + "license": "MIT", "optional": true, "os": [ "win32" @@ -1806,9 +2272,9 @@ } }, "node_modules/@noble/curves": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.2.tgz", - "integrity": "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==", + "version": "1.9.6", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.6.tgz", + "integrity": "sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA==", "dev": true, "license": "MIT", "dependencies": { @@ -1859,6 +2325,134 @@ "@node-rs/argon2-win32-x64-msvc": "2.0.2" } }, + "node_modules/@node-rs/argon2-android-arm-eabi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-2.0.2.tgz", + "integrity": "sha512-DV/H8p/jt40lrao5z5g6nM9dPNPGEHL+aK6Iy/og+dbL503Uj0AHLqj1Hk9aVUSCNnsDdUEKp4TVMi0YakDYKw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-android-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-2.0.2.tgz", + "integrity": "sha512-1LKwskau+8O1ktKx7TbK7jx1oMOMt4YEXZOdSNIar1TQKxm6isZ0cRXgHLibPHEcNHgYRsJWDE9zvDGBB17QDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-3TTNL/7wbcpNju5YcqUrCgXnXUSbD7ogeAKatzBVHsbpjZQbNb1NDxDjqqrWoTt6XL3z9mJUMGwbAk7zQltHtA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-2.0.2.tgz", + "integrity": "sha512-vNPfkLj5Ij5111UTiYuwgxMqE7DRbOS2y58O2DIySzSHbcnu+nipmRKg+P0doRq6eKIJStyBK8dQi5Ic8pFyDw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-freebsd-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-2.0.2.tgz", + "integrity": "sha512-M8vQZk01qojQfCqQU0/O1j1a4zPPrz93zc9fSINY7Q/6RhQRBCYwDw7ltDCZXg5JRGlSaeS8cUXWyhPGar3cGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm-gnueabihf": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-2.0.2.tgz", + "integrity": "sha512-7EmmEPHLzcu0G2GDh30L6G48CH38roFC2dqlQJmtRCxs6no3tTE/pvgBGatTp/o2n2oyOJcfmgndVFcUpwMnww==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm64-gnu": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-2.0.2.tgz", + "integrity": "sha512-6lsYh3Ftbk+HAIZ7wNuRF4SZDtxtFTfK+HYFAQQyW7Ig3LHqasqwfUKRXVSV5tJ+xTnxjqgKzvZSUJCAyIfHew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-linux-arm64-musl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-2.0.2.tgz", + "integrity": "sha512-p3YqVMNT/4DNR67tIHTYGbedYmXxW9QlFmF39SkXyEbGQwpgSf6pH457/fyXBIYznTU/smnG9EH+C1uzT5j4hA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@node-rs/argon2-linux-x64-gnu": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-2.0.2.tgz", @@ -1891,6 +2485,70 @@ "node": ">= 10" } }, + "node_modules/@node-rs/argon2-wasm32-wasi": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-2.0.2.tgz", + "integrity": "sha512-U3PzLYKSQYzTERstgtHLd4ZTkOF9co57zTXT77r0cVUsleGZOrd6ut7rHzeWwoJSiHOVxxa0OhG1JVQeB7lLoQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/argon2-win32-arm64-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-2.0.2.tgz", + "integrity": "sha512-Eisd7/NM0m23ijrGr6xI2iMocdOuyl6gO27gfMfya4C5BODbUSP7ljKJ7LrA0teqZMdYHesRDzx36Js++/vhiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-ia32-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-2.0.2.tgz", + "integrity": "sha512-GsE2ezwAYwh72f9gIjbGTZOf4HxEksb5M2eCaj+Y5rGYVwAdt7C12Q2e9H5LRYxWcFvLH4m4jiSZpQQ4upnPAQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/argon2-win32-x64-msvc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-2.0.2.tgz", + "integrity": "sha512-cJxWXanH4Ew9CfuZ4IAEiafpOBCe97bzoKowHCGk5lG/7kR4WF/eknnBlHW9m8q7t10mKq75kruPLtbSDqgRTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@node-rs/bcrypt": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.9.0.tgz", @@ -1920,6 +2578,134 @@ "@node-rs/bcrypt-win32-x64-msvc": "1.9.0" } }, + "node_modules/@node-rs/bcrypt-android-arm-eabi": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm-eabi/-/bcrypt-android-arm-eabi-1.9.0.tgz", + "integrity": "sha512-nOCFISGtnodGHNiLrG0WYLWr81qQzZKYfmwHc7muUeq+KY0sQXyHOwZk9OuNQAWv/lnntmtbwkwT0QNEmOyLvA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-android-arm64": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-android-arm64/-/bcrypt-android-arm64-1.9.0.tgz", + "integrity": "sha512-+ZrIAtigVmjYkqZQTThHVlz0+TG6D+GDHWhVKvR2DifjtqJ0i+mb9gjo++hN+fWEQdWNGxKCiBBjwgT4EcXd6A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-darwin-arm64": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-arm64/-/bcrypt-darwin-arm64-1.9.0.tgz", + "integrity": "sha512-CQiS+F9Pa0XozvkXR1g7uXE9QvBOPOplDg0iCCPRYTN9PqA5qYxhwe48G3o+v2UeQceNRrbnEtWuANm7JRqIhw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-darwin-x64": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-darwin-x64/-/bcrypt-darwin-x64-1.9.0.tgz", + "integrity": "sha512-4pTKGawYd7sNEjdJ7R/R67uwQH1VvwPZ0SSUMmeNHbxD5QlwAPXdDH11q22uzVXsvNFZ6nGQBg8No5OUGpx6Ug==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-freebsd-x64": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-freebsd-x64/-/bcrypt-freebsd-x64-1.9.0.tgz", + "integrity": "sha512-UmWzySX4BJhT/B8xmTru6iFif3h0Rpx3TqxRLCcbgmH43r7k5/9QuhpiyzpvKGpKHJCFNm4F3rC2wghvw5FCIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-arm-gnueabihf": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm-gnueabihf/-/bcrypt-linux-arm-gnueabihf-1.9.0.tgz", + "integrity": "sha512-8qoX4PgBND2cVwsbajoAWo3NwdfJPEXgpCsZQZURz42oMjbGyhhSYbovBCskGU3EBLoC8RA2B1jFWooeYVn5BA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-arm64-gnu": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-gnu/-/bcrypt-linux-arm64-gnu-1.9.0.tgz", + "integrity": "sha512-TuAC6kx0SbcIA4mSEWPi+OCcDjTQUMl213v5gMNlttF+D4ieIZx6pPDGTaMO6M2PDHTeCG0CBzZl0Lu+9b0c7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-linux-arm64-musl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-arm64-musl/-/bcrypt-linux-arm64-musl-1.9.0.tgz", + "integrity": "sha512-/sIvKDABOI8QOEnLD7hIj02BVaNOuCIWBKvxcJOt8+TuwJ6zmY1UI5kSv9d99WbiHjTp97wtAUbZQwauU4b9ew==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@node-rs/bcrypt-linux-x64-gnu": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-gnu/-/bcrypt-linux-x64-gnu-1.9.0.tgz", @@ -1952,6 +2738,103 @@ "node": ">= 10" } }, + "node_modules/@node-rs/bcrypt-wasm32-wasi": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-wasm32-wasi/-/bcrypt-wasm32-wasi-1.9.0.tgz", + "integrity": "sha512-ylaGmn9Wjwv/D5lxtawttx3H6Uu2WTTR7lWlRHGT6Ga/MB1Vj4OjSGUW8G8zIVnKuXpGbZ92pgHlt4HUpSLctw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^0.45.0", + "@emnapi/runtime": "^0.45.0", + "@tybys/wasm-util": "^0.8.1", + "memfs-browser": "^3.4.13000" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@emnapi/core": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", + "integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", + "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@node-rs/bcrypt-wasm32-wasi/node_modules/@tybys/wasm-util": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", + "integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@node-rs/bcrypt-win32-arm64-msvc": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-arm64-msvc/-/bcrypt-win32-arm64-msvc-1.9.0.tgz", + "integrity": "sha512-2h86gF7QFyEzODuDFml/Dp1MSJoZjxJ4yyT2Erf4NkwsiA5MqowUhUsorRwZhX6+2CtlGa7orbwi13AKMsYndw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-win32-ia32-msvc": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-ia32-msvc/-/bcrypt-win32-ia32-msvc-1.9.0.tgz", + "integrity": "sha512-kqxalCvhs4FkN0+gWWfa4Bdy2NQAkfiqq/CEf6mNXC13RSV673Ev9V8sRlQyNpCHCNkeXfOT9pgoBdJmMs9muA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@node-rs/bcrypt-win32-x64-msvc": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-win32-x64-msvc/-/bcrypt-win32-x64-msvc-1.9.0.tgz", + "integrity": "sha512-2y0Tuo6ZAT2Cz8V7DHulSlv1Bip3zbzeXyeur+uR25IRNYXKvI/P99Zl85Fbuu/zzYAZRLLlGTRe6/9IHofe/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2043,59 +2926,59 @@ "license": "MIT" }, "node_modules/@peculiar/asn1-android": { - "version": "2.3.16", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.3.16.tgz", - "integrity": "sha512-a1viIv3bIahXNssrOIkXZIlI2ePpZaNmR30d4aBL99mu2rO+mT9D6zBsp7H6eROWGtmwv0Ionp5olJurIo09dw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-android/-/asn1-android-2.4.0.tgz", + "integrity": "sha512-YFueREq97CLslZZBI8dKzis7jMfEHSLxM+nr0Zdx1POiXFLjqqwoY5s0F1UimdBiEw/iKlHey2m56MRDv7Jtyg==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.3.15", - "asn1js": "^3.0.5", + "@peculiar/asn1-schema": "^2.4.0", + "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-ecc": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.3.15.tgz", - "integrity": "sha512-/HtR91dvgog7z/WhCVdxZJ/jitJuIu8iTqiyWVgRE9Ac5imt2sT/E4obqIVGKQw7PIy+X6i8lVBoT6wC73XUgA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.4.0.tgz", + "integrity": "sha512-fJiYUBCJBDkjh347zZe5H81BdJ0+OGIg0X9z06v8xXUoql3MFeENUX0JsjCaVaU9A0L85PefLPGYkIoGpTnXLQ==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.3.15", - "@peculiar/asn1-x509": "^2.3.15", - "asn1js": "^3.0.5", + "@peculiar/asn1-schema": "^2.4.0", + "@peculiar/asn1-x509": "^2.4.0", + "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-rsa": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.3.15.tgz", - "integrity": "sha512-p6hsanvPhexRtYSOHihLvUUgrJ8y0FtOM97N5UEpC+VifFYyZa0iZ5cXjTkZoDwxJ/TTJ1IJo3HVTB2JJTpXvg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.4.0.tgz", + "integrity": "sha512-6PP75voaEnOSlWR9sD25iCQyLgFZHXbmxvUfnnDcfL6Zh5h2iHW38+bve4LfH7a60x7fkhZZNmiYqAlAff9Img==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.3.15", - "@peculiar/asn1-x509": "^2.3.15", - "asn1js": "^3.0.5", + "@peculiar/asn1-schema": "^2.4.0", + "@peculiar/asn1-x509": "^2.4.0", + "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-schema": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.15.tgz", - "integrity": "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.4.0.tgz", + "integrity": "sha512-umbembjIWOrPSOzEGG5vxFLkeM8kzIhLkgigtsOrfLKnuzxWxejAcUX+q/SoZCdemlODOcr5WiYa7+dIEzBXZQ==", "license": "MIT", "dependencies": { - "asn1js": "^3.0.5", + "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-x509": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.3.15.tgz", - "integrity": "sha512-0dK5xqTqSLaxv1FHXIcd4Q/BZNuopg+u1l23hT9rOmQ1g4dNtw0g/RnEi+TboB0gOwGtrWn269v27cMgchFIIg==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.4.0.tgz", + "integrity": "sha512-F7mIZY2Eao2TaoVqigGMLv+NDdpwuBKU1fucHPONfzaBS4JXXCNCmfO0Z3dsy7JzKGqtDcYC1mr9JjaZQZNiuw==", "license": "MIT", "dependencies": { - "@peculiar/asn1-schema": "^2.3.15", - "asn1js": "^3.0.5", + "@peculiar/asn1-schema": "^2.4.0", + "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } @@ -3590,6 +4473,125 @@ "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", @@ -3624,6 +4626,70 @@ "node": ">= 10" } }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@tailwindcss/postcss": { "version": "4.1.11", "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.11.tgz", @@ -3671,6 +4737,16 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/better-sqlite3": { "version": "7.6.12", "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.12.tgz", @@ -3740,6 +4816,7 @@ "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, + "license": "MIT", "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4240,6 +5317,175 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", @@ -4266,15 +5512,69 @@ "linux" ] }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" @@ -4752,6 +6052,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", @@ -4830,6 +6131,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -4891,9 +6193,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001727", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", - "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "version": "1.0.30001734", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz", + "integrity": "sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A==", "funding": [ { "type": "opencollective", @@ -4956,7 +6258,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -5119,6 +6420,20 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5231,6 +6546,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" }, @@ -5242,6 +6558,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -5878,7 +7195,8 @@ "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -5896,6 +7214,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -5940,6 +7259,20 @@ "node": ">=10.0.0" } }, + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/engine.io/node_modules/cookie": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", @@ -5968,6 +7301,39 @@ } } }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/engine.io/node_modules/ws": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", @@ -5991,9 +7357,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.2", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", - "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", "dev": true, "license": "MIT", "dependencies": { @@ -6268,7 +7634,8 @@ "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" }, "node_modules/escape-string-regexp": { "version": "4.0.0", @@ -6690,6 +8057,7 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -6731,6 +8099,7 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -6783,22 +8152,10 @@ "express": ">= 4.11" } }, - "node_modules/express/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express/node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -6808,37 +8165,11 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", "engines": { "node": ">=6.6.0" } }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/exsolve": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", @@ -6987,6 +8318,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", @@ -7041,9 +8373,9 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -7119,6 +8451,27 @@ "node": ">= 6" } }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -7144,6 +8497,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -7154,6 +8508,28 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs-monkey": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", + "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", + "license": "Unlicense", + "optional": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7422,7 +8798,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -7578,6 +8953,15 @@ "node": ">= 0.8" } }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -7600,6 +8984,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -8001,7 +9386,8 @@ "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" }, "node_modules/is-regex": { "version": "1.2.1", @@ -8174,7 +9560,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "dev": true, "license": "ISC", "engines": { "node": ">=16" @@ -8213,9 +9598,9 @@ } }, "node_modules/jiti": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "devOptional": true, "license": "MIT", "bin": { @@ -8456,6 +9841,132 @@ "lightningcss-win32-x64-msvc": "1.30.1" } }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lightningcss-linux-x64-gnu": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", @@ -8498,6 +10009,48 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -8672,14 +10225,39 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "license": "Unlicense", + "optional": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memfs-browser": { + "version": "3.5.10302", + "resolved": "https://registry.npmjs.org/memfs-browser/-/memfs-browser-3.5.10302.tgz", + "integrity": "sha512-JJTc/nh3ig05O0gBBGZjTCPOyydaTxNF0uHYBrcc1gHNnO+KIHIvo0Y1FKCJsaei6FCl8C6xfQomXqu+cuzkIw==", + "license": "Unlicense", + "optional": true, + "dependencies": { + "memfs": "3.5.3" + } + }, "node_modules/merge-descriptors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -8729,21 +10307,21 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -8827,7 +10405,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", - "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -8840,7 +10417,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" @@ -8912,9 +10488,9 @@ "license": "MIT" }, "node_modules/napi-postinstall": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.0.tgz", - "integrity": "sha512-M7NqKyhODKV1gRLdkwE7pDsZP2/SC2a2vHkOYh9MCpKMbWVfyVfUw5MaH83Fv6XMjxr5jryUp3IDDL9rlxsTeA==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", "license": "MIT", "bin": { "napi-postinstall": "lib/cli.js" @@ -8933,10 +10509,9 @@ "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { "node": ">= 0.6" @@ -9021,15 +10596,6 @@ } } }, - "node_modules/next-intl/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -11784,6 +13350,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -11885,9 +13452,9 @@ } }, "node_modules/ora/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz", + "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==", "dev": true, "license": "MIT", "engines": { @@ -11963,6 +13530,26 @@ "@node-rs/bcrypt": "1.9.0" } }, + "node_modules/oslo/node_modules/@emnapi/core": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-0.45.0.tgz", + "integrity": "sha512-DPWjcUDQkCeEM4VnljEOEcXdAD7pp8zSZsgOujk/LGIwCXWbXJngin+MO4zbH429lzeC3WbYLGjE2MaUOwzpyw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/oslo/node_modules/@emnapi/runtime": { + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz", + "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/oslo/node_modules/@node-rs/argon2": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@node-rs/argon2/-/argon2-1.7.0.tgz", @@ -11988,6 +13575,134 @@ "@node-rs/argon2-win32-x64-msvc": "1.7.0" } }, + "node_modules/oslo/node_modules/@node-rs/argon2-android-arm-eabi": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm-eabi/-/argon2-android-arm-eabi-1.7.0.tgz", + "integrity": "sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-android-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-android-arm64/-/argon2-android-arm64-1.7.0.tgz", + "integrity": "sha512-s9j/G30xKUx8WU50WIhF0fIl1EdhBGq0RQ06lEhZ0Gi0ap8lhqbE2Bn5h3/G2D1k0Dx+yjeVVNmt/xOQIRG38A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-darwin-arm64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-arm64/-/argon2-darwin-arm64-1.7.0.tgz", + "integrity": "sha512-ZIz4L6HGOB9U1kW23g+m7anGNuTZ0RuTw0vNp3o+2DWpb8u8rODq6A8tH4JRL79S+Co/Nq608m9uackN2pe0Rw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-darwin-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-darwin-x64/-/argon2-darwin-x64-1.7.0.tgz", + "integrity": "sha512-5oi/pxqVhODW/pj1+3zElMTn/YukQeywPHHYDbcAW3KsojFjKySfhcJMd1DjKTc+CHQI+4lOxZzSUzK7mI14Hw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-freebsd-x64": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-freebsd-x64/-/argon2-freebsd-x64-1.7.0.tgz", + "integrity": "sha512-Ify08683hA4QVXYoIm5SUWOY5DPIT/CMB0CQT+IdxQAg/F+qp342+lUkeAtD5bvStQuCx/dFO3bnnzoe2clMhA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm-gnueabihf": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm-gnueabihf/-/argon2-linux-arm-gnueabihf-1.7.0.tgz", + "integrity": "sha512-7DjDZ1h5AUHAtRNjD19RnQatbhL+uuxBASuuXIBu4/w6Dx8n7YPxwTP4MXfsvuRgKuMWiOb/Ub/HJ3kXVCXRkg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm64-gnu": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-gnu/-/argon2-linux-arm64-gnu-1.7.0.tgz", + "integrity": "sha512-nJDoMP4Y3YcqGswE4DvP080w6O24RmnFEDnL0emdI8Nou17kNYBzP2546Nasx9GCyLzRcYQwZOUjrtUuQ+od2g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-linux-arm64-musl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-arm64-musl/-/argon2-linux-arm64-musl-1.7.0.tgz", + "integrity": "sha512-BKWS8iVconhE3jrb9mj6t1J9vwUqQPpzCbUKxfTGJfc+kNL58F1SXHBoe2cDYGnHrFEHTY0YochzXoAfm4Dm/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-gnu": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-gnu/-/argon2-linux-x64-gnu-1.7.0.tgz", @@ -12020,6 +13735,83 @@ "node": ">= 10" } }, + "node_modules/oslo/node_modules/@node-rs/argon2-wasm32-wasi": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-wasm32-wasi/-/argon2-wasm32-wasi-1.7.0.tgz", + "integrity": "sha512-Evmk9VcxqnuwQftfAfYEr6YZYSPLzmKUsbFIMep5nTt9PT4XYRFAERj7wNYp+rOcBenF3X4xoB+LhwcOMTNE5w==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^0.45.0", + "@emnapi/runtime": "^0.45.0", + "@tybys/wasm-util": "^0.8.1", + "memfs-browser": "^3.4.13000" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-win32-arm64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-arm64-msvc/-/argon2-win32-arm64-msvc-1.7.0.tgz", + "integrity": "sha512-qgsU7T004COWWpSA0tppDqDxbPLgg8FaU09krIJ7FBl71Sz8SFO40h7fDIjfbTT5w7u6mcaINMQ5bSHu75PCaA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-win32-ia32-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-ia32-msvc/-/argon2-win32-ia32-msvc-1.7.0.tgz", + "integrity": "sha512-JGafwWYQ/HpZ3XSwP4adQ6W41pRvhcdXvpzIWtKvX+17+xEXAe2nmGWM6s27pVkg1iV2ZtoYLRDkOUoGqZkCcg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@node-rs/argon2-win32-x64-msvc": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@node-rs/argon2-win32-x64-msvc/-/argon2-win32-x64-msvc-1.7.0.tgz", + "integrity": "sha512-9oq4ShyFakw8AG3mRls0AoCpxBFcimYx7+jvXeAf2OqKNO+mSA6eZ9z7KQeVCi0+SOEUYxMGf5UiGiDb9R6+9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/oslo/node_modules/@tybys/wasm-util": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.8.3.tgz", + "integrity": "sha512-Z96T/L6dUFFxgFJ+pQtkPpne9q7i6kIPYCFnQBHSgSPV9idTsKfIhCss0h5iM9irweZCatkrdeP8yi5uM1eX6Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/own-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", @@ -12102,6 +13894,7 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -12150,6 +13943,7 @@ "version": "8.2.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", "engines": { "node": ">=16" } @@ -12542,6 +14336,7 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" }, @@ -12586,6 +14381,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -12594,6 +14390,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -12705,9 +14502,9 @@ } }, "node_modules/react-email/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.5.0.tgz", + "integrity": "sha512-1tm8DTaJhPBG3bIkVeZt1iZM9GfSX2lzOeDVZH9R9ffRHpmHvxZ/QhgQH/aDTkswQVt+YHdXAdS/In/30OjCbg==", "dev": true, "license": "MIT", "engines": { @@ -12727,6 +14524,16 @@ "node": ">=18" } }, + "node_modules/react-email/node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/react-email/node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -12740,29 +14547,6 @@ "node": ">=6" } }, - "node_modules/react-email/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/react-email/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/react-email/node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -13075,6 +14859,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", @@ -13193,7 +14978,8 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/scheduler": { "version": "0.26.0", @@ -13229,6 +15015,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", @@ -13246,29 +15033,11 @@ "node": ">= 18" } }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", @@ -13331,6 +15100,49 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz", + "integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.4", + "semver": "^7.7.2" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.3", + "@img/sharp-darwin-x64": "0.34.3", + "@img/sharp-libvips-darwin-arm64": "1.2.0", + "@img/sharp-libvips-darwin-x64": "1.2.0", + "@img/sharp-libvips-linux-arm": "1.2.0", + "@img/sharp-libvips-linux-arm64": "1.2.0", + "@img/sharp-libvips-linux-ppc64": "1.2.0", + "@img/sharp-libvips-linux-s390x": "1.2.0", + "@img/sharp-libvips-linux-x64": "1.2.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", + "@img/sharp-libvips-linuxmusl-x64": "1.2.0", + "@img/sharp-linux-arm": "0.34.3", + "@img/sharp-linux-arm64": "0.34.3", + "@img/sharp-linux-ppc64": "0.34.3", + "@img/sharp-linux-s390x": "0.34.3", + "@img/sharp-linux-x64": "0.34.3", + "@img/sharp-linuxmusl-arm64": "0.34.3", + "@img/sharp-linuxmusl-x64": "0.34.3", + "@img/sharp-wasm32": "0.34.3", + "@img/sharp-win32-arm64": "0.34.3", + "@img/sharp-win32-ia32": "0.34.3", + "@img/sharp-win32-x64": "0.34.3" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13604,6 +15416,20 @@ } } }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/socket.io/node_modules/debug": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", @@ -13622,6 +15448,39 @@ } } }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -13677,9 +15536,9 @@ } }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "license": "MIT", "engines": { "node": ">= 0.8" @@ -14002,9 +15861,9 @@ } }, "node_modules/swagger-ui-dist": { - "version": "5.26.2", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.26.2.tgz", - "integrity": "sha512-WmMS9iMlHQejNm/Uw5ZTo4e3M2QMmEavRz7WLWVsq7Mlx4PSHJbY+VCrLsAz9wLxyHVgrJdt7N8+SdQLa52Ykg==", + "version": "5.27.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.27.1.tgz", + "integrity": "sha512-oGtpYO3lnoaqyGtlJalvryl7TwzgRuxpOVWqEHx8af0YXI+Kt+4jMpLdgMtMcmWmuQ0QTCHLKExwrBFMSxvAUA==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -14055,7 +15914,6 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", - "dev": true, "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -14360,6 +16218,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", @@ -14369,25 +16228,6 @@ "node": ">= 0.6" } }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -14528,6 +16368,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -14711,7 +16552,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -15017,16 +16857,15 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", - "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } }, "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", "license": "ISC", "bin": { "yaml": "bin.mjs" From e7df29104ea6d9378c1f782a35b2af6ba171a14e Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 12:28:45 -0700 Subject: [PATCH 30/38] Fix backwards compat path --- server/routers/olm/handleOlmRegisterMessage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 64443e07..5128e9e1 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -67,7 +67,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { // THIS IS FOR BACKWARDS COMPATIBILITY await sendToClient(olm.olmId, { - type: "olm/wg/holepunch/all", + type: "olm/wg/holepunch", data: { serverPubKey: allExitNodes[0].publicKey, endpoint: allExitNodes[0].endpoint From 1f4a7a7f6f82f0a6edb4c4656550c796951ae6ab Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 12:35:09 -0700 Subject: [PATCH 31/38] Add olm version --- server/db/pg/schema.ts | 1 + server/db/sqlite/schema.ts | 1 + .../routers/olm/handleOlmRegisterMessage.ts | 30 +++++++++++++------ 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index c7a1eebf..657e22eb 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -536,6 +536,7 @@ export const olms = pgTable("olms", { olmId: varchar("id").primaryKey(), secretHash: varchar("secretHash").notNull(), dateCreated: varchar("dateCreated").notNull(), + version: text("version").notNull(), clientId: integer("clientId").references(() => clients.clientId, { onDelete: "cascade" }) diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 4268cd9f..730ae4ea 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -243,6 +243,7 @@ export const olms = sqliteTable("olms", { olmId: text("id").primaryKey(), secretHash: text("secretHash").notNull(), dateCreated: text("dateCreated").notNull(), + version: text("version").notNull(), clientId: integer("clientId").references(() => clients.clientId, { onDelete: "cascade" }) diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 5128e9e1..7eb3d978 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -21,7 +21,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } const clientId = olm.clientId; - const { publicKey, relay } = message.data; + const { publicKey, relay, olmVersion } = message.data; logger.debug( `Olm client ID: ${clientId}, Public Key: ${publicKey}, Relay: ${relay}` @@ -65,14 +65,26 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { } }); - // THIS IS FOR BACKWARDS COMPATIBILITY - await sendToClient(olm.olmId, { - type: "olm/wg/holepunch", - data: { - serverPubKey: allExitNodes[0].publicKey, - endpoint: allExitNodes[0].endpoint - } - }); + if (!olmVersion) { + // THIS IS FOR BACKWARDS COMPATIBILITY + // THE OLDER CLIENTS DID NOT SEND THE VERSION + await sendToClient(olm.olmId, { + type: "olm/wg/holepunch", + data: { + serverPubKey: allExitNodes[0].publicKey, + endpoint: allExitNodes[0].endpoint + } + }); + } + } + + if (olmVersion) { + await db + .update(olms) + .set({ + version: olmVersion + }) + .where(eq(olms.olmId, olm.olmId)); } if (now - (client.lastHolePunch || 0) > 6) { From 4c463de45f31fdd0f4c54e3cf5a72d9f67c7152f Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 14:47:03 -0700 Subject: [PATCH 32/38] Version does not need to be notNull --- server/db/pg/schema.ts | 2 +- server/db/sqlite/schema.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 657e22eb..477636f7 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -536,7 +536,7 @@ export const olms = pgTable("olms", { olmId: varchar("id").primaryKey(), secretHash: varchar("secretHash").notNull(), dateCreated: varchar("dateCreated").notNull(), - version: text("version").notNull(), + version: text("version"), clientId: integer("clientId").references(() => clients.clientId, { onDelete: "cascade" }) diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 730ae4ea..460081a9 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -243,7 +243,7 @@ export const olms = sqliteTable("olms", { olmId: text("id").primaryKey(), secretHash: text("secretHash").notNull(), dateCreated: text("dateCreated").notNull(), - version: text("version").notNull(), + version: text("version"), clientId: integer("clientId").references(() => clients.clientId, { onDelete: "cascade" }) From eeb1d4954da72ab89617a6d1a3d66155f6d8997a Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 13 Aug 2025 20:26:50 -0700 Subject: [PATCH 33/38] Use an epoch number for the clients online to fix query --- server/db/pg/schema.ts | 2 +- server/db/sqlite/schema.ts | 2 +- server/routers/olm/handleOlmPingMessage.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 477636f7..2ba10e3e 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -513,7 +513,7 @@ export const clients = pgTable("clients", { megabytesIn: real("bytesIn"), megabytesOut: real("bytesOut"), lastBandwidthUpdate: varchar("lastBandwidthUpdate"), - lastPing: varchar("lastPing"), + lastPing: integer("lastPing"), type: varchar("type").notNull(), // "olm" online: boolean("online").notNull().default(false), // endpoint: varchar("endpoint"), diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 460081a9..db1d8090 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -221,7 +221,7 @@ export const clients = sqliteTable("clients", { megabytesIn: integer("bytesIn"), megabytesOut: integer("bytesOut"), lastBandwidthUpdate: text("lastBandwidthUpdate"), - lastPing: text("lastPing"), + lastPing: integer("lastPing"), type: text("type").notNull(), // "olm" online: integer("online", { mode: "boolean" }).notNull().default(false), // endpoint: text("endpoint"), diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts index c95f36af..04659bb3 100644 --- a/server/routers/olm/handleOlmPingMessage.ts +++ b/server/routers/olm/handleOlmPingMessage.ts @@ -28,7 +28,7 @@ export const startOfflineChecker = (): void => { .set({ online: false }) .where( eq(clients.online, true) && - (lt(clients.lastPing, twoMinutesAgo.toISOString()) || isNull(clients.lastPing)) + (lt(clients.lastPing, twoMinutesAgo.getTime() / 1000) || isNull(clients.lastPing)) ); } catch (error) { @@ -72,7 +72,7 @@ export const handleOlmPingMessage: MessageHandler = async (context) => { await db .update(clients) .set({ - lastPing: new Date().toISOString(), + lastPing: new Date().getTime() / 1000, online: true, }) .where(eq(clients.clientId, olm.clientId)); From 74d2527af56d87f2b4ed5fa72b2a066c144fd6a6 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Wed, 13 Aug 2025 21:59:32 -0700 Subject: [PATCH 34/38] make email lower case in pangctl reset password closes #1210 --- cli/commands/resetUserSecurityKeys.ts | 25 +++++++++++++++---------- cli/commands/setAdminCredentials.ts | 3 ++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/cli/commands/resetUserSecurityKeys.ts b/cli/commands/resetUserSecurityKeys.ts index 84af7cec..fdae0ebd 100644 --- a/cli/commands/resetUserSecurityKeys.ts +++ b/cli/commands/resetUserSecurityKeys.ts @@ -6,16 +6,19 @@ type ResetUserSecurityKeysArgs = { email: string; }; -export const resetUserSecurityKeys: CommandModule<{}, ResetUserSecurityKeysArgs> = { +export const resetUserSecurityKeys: CommandModule< + {}, + ResetUserSecurityKeysArgs +> = { command: "reset-user-security-keys", - describe: "Reset a user's security keys (passkeys) by deleting all their webauthn credentials", + describe: + "Reset a user's security keys (passkeys) by deleting all their webauthn credentials", builder: (yargs) => { - return yargs - .option("email", { - type: "string", - demandOption: true, - describe: "User email address" - }); + return yargs.option("email", { + type: "string", + demandOption: true, + describe: "User email address" + }); }, handler: async (argv: { email: string }) => { try { @@ -48,7 +51,9 @@ export const resetUserSecurityKeys: CommandModule<{}, ResetUserSecurityKeysArgs> process.exit(0); } - console.log(`Found ${userSecurityKeys.length} security key(s) for user '${email}'`); + console.log( + `Found ${userSecurityKeys.length} security key(s) for user '${email}'` + ); // Delete all security keys for the user await db @@ -64,4 +69,4 @@ export const resetUserSecurityKeys: CommandModule<{}, ResetUserSecurityKeysArgs> process.exit(1); } } -}; \ No newline at end of file +}; diff --git a/cli/commands/setAdminCredentials.ts b/cli/commands/setAdminCredentials.ts index 72ff8bff..c45da602 100644 --- a/cli/commands/setAdminCredentials.ts +++ b/cli/commands/setAdminCredentials.ts @@ -32,7 +32,8 @@ export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = { }, handler: async (argv: { email: string; password: string }) => { try { - const { email, password } = argv; + let { email, password } = argv; + email = email.trim().toLowerCase(); const parsed = passwordSchema.safeParse(password); From 67ba225003f33f443b67e300ac03fcf763e30865 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 14 Aug 2025 17:06:07 -0700 Subject: [PATCH 35/38] add statistics --- package-lock.json | 28 ++ package.json | 7 +- server/db/sqlite/schema.ts | 1 + server/index.ts | 6 + .../{setup/setHostMeta.ts => lib/hostMeta.ts} | 13 +- server/lib/readConfigFile.ts | 16 +- server/lib/telemetry.ts | 295 ++++++++++++++++++ server/license/license.ts | 2 +- server/logger.ts | 1 + server/setup/migrationsPg.ts | 2 +- server/setup/migrationsSqlite.ts | 2 +- 11 files changed, 362 insertions(+), 11 deletions(-) rename server/{setup/setHostMeta.ts => lib/hostMeta.ts} (57%) create mode 100644 server/lib/telemetry.ts diff --git a/package-lock.json b/package-lock.json index e0d69052..b0dd35e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,6 +75,7 @@ "npm": "^11.5.2", "oslo": "1.2.1", "pg": "^8.16.2", + "posthog-node": "^5.7.0", "qrcode.react": "4.2.0", "react": "19.1.1", "react-dom": "19.1.1", @@ -2050,6 +2051,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, "license": "ISC", "dependencies": { "minipass": "^7.0.4" @@ -2983,6 +2985,12 @@ "tslib": "^2.8.1" } }, + "node_modules/@posthog/core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.0.0.tgz", + "integrity": "sha512-gquQld+duT9DdzLIFoHZkUMW0DZOTSLCtSjuuC/zKFz65Qecbz9p37DHBJMkw0dCuB8Mgh2GtH8Ag3PznJrP3g==", + "license": "MIT" + }, "node_modules/@radix-ui/number": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", @@ -6258,6 +6266,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" @@ -8798,6 +8807,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -9560,6 +9570,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, "license": "ISC", "engines": { "node": ">=16" @@ -10405,6 +10416,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, "license": "MIT", "dependencies": { "minipass": "^7.1.2" @@ -10417,6 +10429,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" @@ -14183,6 +14196,18 @@ "node": ">=0.10.0" } }, + "node_modules/posthog-node": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.7.0.tgz", + "integrity": "sha512-6J1AIZWtbr2lEbZOO2AzO/h1FPJjUZM4KWcdaL2UQw7FY8J7VNaH3NiaRockASFmglpID7zEY25gV/YwCtuXjg==", + "license": "MIT", + "dependencies": { + "@posthog/core": "1.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/prebuild-install": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", @@ -15914,6 +15939,7 @@ "version": "7.4.3", "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", @@ -16552,6 +16578,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, "license": "ISC", "dependencies": { "isexe": "^3.1.1" @@ -16857,6 +16884,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, "license": "BlueOak-1.0.0", "engines": { "node": ">=18" diff --git a/package.json b/package.json index 937f76fa..7b3464a8 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,9 @@ "@radix-ui/react-tooltip": "^1.2.7", "@react-email/components": "0.5.0", "@react-email/render": "^1.2.0", + "@react-email/tailwind": "1.2.2", "@simplewebauthn/browser": "^13.1.0", "@simplewebauthn/server": "^9.0.3", - "@react-email/tailwind": "1.2.2", "@tailwindcss/forms": "^0.5.10", "@tanstack/react-table": "8.21.3", "arctic": "^3.7.0", @@ -93,6 +93,7 @@ "npm": "^11.5.2", "oslo": "1.2.1", "pg": "^8.16.2", + "posthog-node": "^5.7.0", "qrcode.react": "4.2.0", "react": "19.1.1", "react-dom": "19.1.1", @@ -109,9 +110,9 @@ "winston": "3.17.0", "winston-daily-rotate-file": "5.0.0", "ws": "8.18.3", + "yargs": "18.0.0", "zod": "3.25.76", - "zod-validation-error": "3.5.2", - "yargs": "18.0.0" + "zod-validation-error": "3.5.2" }, "devDependencies": { "@dotenvx/dotenvx": "1.48.4", diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index db1d8090..5bd81d6a 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -690,3 +690,4 @@ export type ApiKeyAction = InferSelectModel; export type ApiKeyOrg = InferSelectModel; export type OrgDomains = InferSelectModel; export type SetupToken = InferSelectModel; +export type HostMeta = InferSelectModel; diff --git a/server/index.ts b/server/index.ts index d3f90281..8724cb41 100644 --- a/server/index.ts +++ b/server/index.ts @@ -8,11 +8,17 @@ import { createInternalServer } from "./internalServer"; import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "@server/db"; import { createIntegrationApiServer } from "./integrationApiServer"; import config from "@server/lib/config"; +import { setHostMeta } from "@server/lib/hostMeta"; +import { initTelemetryClient } from "./lib/telemetry.js"; async function startServers() { + await setHostMeta(); + await config.initServer(); await runSetupFunctions(); + initTelemetryClient(); + // Start all servers const apiServer = createApiServer(); const internalServer = createInternalServer(); diff --git a/server/setup/setHostMeta.ts b/server/lib/hostMeta.ts similarity index 57% rename from server/setup/setHostMeta.ts rename to server/lib/hostMeta.ts index 2223d11b..2f2c7ed7 100644 --- a/server/setup/setHostMeta.ts +++ b/server/lib/hostMeta.ts @@ -1,7 +1,9 @@ -import { db } from "@server/db"; +import { db, HostMeta } from "@server/db"; import { hostMeta } from "@server/db"; import { v4 as uuidv4 } from "uuid"; +let gotHostMeta: HostMeta | undefined; + export async function setHostMeta() { const [existing] = await db.select().from(hostMeta).limit(1); @@ -15,3 +17,12 @@ export async function setHostMeta() { .insert(hostMeta) .values({ hostMetaId: id, createdAt: new Date().getTime() }); } + +export async function getHostMeta() { + if (gotHostMeta) { + return gotHostMeta; + } + const [meta] = await db.select().from(hostMeta).limit(1); + gotHostMeta = meta; + return meta; +} diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 1bc119fa..c349e50e 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -3,7 +3,6 @@ import yaml from "js-yaml"; import { configFilePath1, configFilePath2 } from "./consts"; import { z } from "zod"; import stoi from "./stoi"; -import { build } from "@server/build"; const portSchema = z.number().positive().gt(0).lte(65535); @@ -25,7 +24,13 @@ export const configSchema = z .optional() .default("info"), save_logs: z.boolean().optional().default(false), - log_failed_attempts: z.boolean().optional().default(false) + log_failed_attempts: z.boolean().optional().default(false), + telmetry: z + .object({ + anonymous_usage: z.boolean().optional().default(true) + }) + .optional() + .default({}) }), domains: z .record( @@ -213,7 +218,10 @@ export const configSchema = z smtp_host: z.string().optional(), smtp_port: portSchema.optional(), smtp_user: z.string().optional(), - smtp_pass: z.string().optional().transform(getEnvOrYaml("EMAIL_SMTP_PASS")), + smtp_pass: z + .string() + .optional() + .transform(getEnvOrYaml("EMAIL_SMTP_PASS")), smtp_secure: z.boolean().optional(), smtp_tls_reject_unauthorized: z.boolean().optional(), no_reply: z.string().email().optional() @@ -229,7 +237,7 @@ export const configSchema = z disable_local_sites: z.boolean().optional(), disable_basic_wireguard_sites: z.boolean().optional(), disable_config_managed_domains: z.boolean().optional(), - enable_clients: z.boolean().optional().default(true), + enable_clients: z.boolean().optional().default(true) }) .optional(), dns: z diff --git a/server/lib/telemetry.ts b/server/lib/telemetry.ts new file mode 100644 index 00000000..8475fb34 --- /dev/null +++ b/server/lib/telemetry.ts @@ -0,0 +1,295 @@ +import { PostHog } from "posthog-node"; +import config from "./config"; +import { getHostMeta } from "./hostMeta"; +import logger from "@server/logger"; +import { apiKeys, db, roles } from "@server/db"; +import { sites, users, orgs, resources, clients, idp } from "@server/db"; +import { eq, count, notInArray } from "drizzle-orm"; +import { APP_VERSION } from "./consts"; +import crypto from "crypto"; +import { UserType } from "@server/types/UserTypes"; + +class TelemetryClient { + private client: PostHog | null = null; + private enabled: boolean; + private intervalId: NodeJS.Timeout | null = null; + + constructor() { + const enabled = config.getRawConfig().app.telmetry.anonymous_usage; + this.enabled = enabled; + const dev = process.env.ENVIRONMENT !== "prod"; + + if (this.enabled && !dev) { + this.client = new PostHog( + "phc_QYuATSSZt6onzssWcYJbXLzQwnunIpdGGDTYhzK3VjX", + { + host: "https://digpangolin.com/relay-O7yI" + } + ); + + process.on("exit", () => { + this.client?.shutdown(); + }); + + this.sendStartupEvents().catch((err) => { + logger.error("Failed to send startup telemetry:", err); + }); + + this.startAnalyticsInterval(); + + logger.info( + "Pangolin now gathers anonymous usage data to help us better understand how the software is used and guide future improvements and feature development. You can find more details, including instructions for opting out of this anonymous data collection, at: https://docs.digpangolin.com/telemetry" + ); + } else if (!this.enabled && !dev) { + logger.info( + "Analytics usage statistics collection is disabled. If you enable this, you can help us make Pangolin better for everyone. Learn more at: https://docs.digpangolin.com/telemetry" + ); + } + } + + private startAnalyticsInterval() { + this.intervalId = setInterval( + () => { + this.collectAndSendAnalytics().catch((err) => { + logger.error("Failed to collect analytics:", err); + }); + }, + 6 * 60 * 60 * 1000 + ); + + this.collectAndSendAnalytics().catch((err) => { + logger.error("Failed to collect initial analytics:", err); + }); + } + + private anon(value: string): string { + return crypto + .createHash("sha256") + .update(value.toLowerCase()) + .digest("hex"); + } + + private async getSystemStats() { + try { + const [sitesCount] = await db + .select({ count: count() }) + .from(sites); + const [usersCount] = await db + .select({ count: count() }) + .from(users); + const [usersInternalCount] = await db + .select({ count: count() }) + .from(users) + .where(eq(users.type, UserType.Internal)); + const [usersOidcCount] = await db + .select({ count: count() }) + .from(users) + .where(eq(users.type, UserType.OIDC)); + const [orgsCount] = await db.select({ count: count() }).from(orgs); + const [resourcesCount] = await db + .select({ count: count() }) + .from(resources); + const [clientsCount] = await db + .select({ count: count() }) + .from(clients); + const [idpCount] = await db.select({ count: count() }).from(idp); + const [onlineSitesCount] = await db + .select({ count: count() }) + .from(sites) + .where(eq(sites.online, true)); + const [numApiKeys] = await db + .select({ count: count() }) + .from(apiKeys); + const [customRoles] = await db + .select({ count: count() }) + .from(roles) + .where(notInArray(roles.name, ["Admin", "Member"])); + + const adminUsers = await db + .select({ email: users.email }) + .from(users) + .where(eq(users.serverAdmin, true)); + + const resourceDetails = await db + .select({ + name: resources.name, + sso: resources.sso, + protocol: resources.protocol, + http: resources.http + }) + .from(resources); + + const siteDetails = await db + .select({ + siteName: sites.name, + megabytesIn: sites.megabytesIn, + megabytesOut: sites.megabytesOut, + type: sites.type, + online: sites.online + }) + .from(sites); + + const supporterKey = config.getSupporterData(); + + return { + numSites: sitesCount.count, + numUsers: usersCount.count, + numUsersInternal: usersInternalCount.count, + numUsersOidc: usersOidcCount.count, + numOrganizations: orgsCount.count, + numResources: resourcesCount.count, + numClients: clientsCount.count, + numIdentityProviders: idpCount.count, + numSitesOnline: onlineSitesCount.count, + resources: resourceDetails, + adminUsers: adminUsers.map((u) => u.email), + sites: siteDetails, + appVersion: APP_VERSION, + numApiKeys: numApiKeys.count, + numCustomRoles: customRoles.count, + supporterStatus: { + valid: supporterKey?.valid || false, + tier: supporterKey?.tier || "None", + githubUsername: supporterKey?.githubUsername || null + } + }; + } catch (error) { + logger.error("Failed to collect system stats:", error); + throw error; + } + } + + private async sendStartupEvents() { + if (!this.enabled || !this.client) return; + + const hostMeta = await getHostMeta(); + if (!hostMeta) return; + + const stats = await this.getSystemStats(); + + this.client.capture({ + distinctId: hostMeta.hostMetaId, + event: "supporter_status", + properties: { + valid: stats.supporterStatus.valid, + tier: stats.supporterStatus.tier, + github_username: stats.supporterStatus.githubUsername + ? this.anon(stats.supporterStatus.githubUsername) + : "None" + } + }); + + this.client.capture({ + distinctId: hostMeta.hostMetaId, + event: "host_startup", + properties: { + host_id: hostMeta.hostMetaId, + app_version: stats.appVersion, + install_timestamp: hostMeta.createdAt + } + }); + + for (const email of stats.adminUsers) { + // There should only be on admin user, but just in case + if (email) { + this.client.capture({ + distinctId: this.anon(email), + event: "admin_user", + properties: { + host_id: hostMeta.hostMetaId, + app_version: stats.appVersion, + hashed_email: this.anon(email) + } + }); + } + } + } + + private async collectAndSendAnalytics() { + if (!this.enabled || !this.client) return; + + try { + const hostMeta = await getHostMeta(); + if (!hostMeta) { + logger.warn( + "Telemetry: Host meta not found, skipping analytics" + ); + return; + } + + const stats = await this.getSystemStats(); + + this.client.capture({ + distinctId: hostMeta.hostMetaId, + event: "system_analytics", + properties: { + app_version: stats.appVersion, + num_sites: stats.numSites, + num_users: stats.numUsers, + num_users_internal: stats.numUsersInternal, + num_users_oidc: stats.numUsersOidc, + num_organizations: stats.numOrganizations, + num_resources: stats.numResources, + num_clients: stats.numClients, + num_identity_providers: stats.numIdentityProviders, + num_sites_online: stats.numSitesOnline, + resources: stats.resources.map((r) => ({ + name: this.anon(r.name), + sso_enabled: r.sso, + protocol: r.protocol, + http_enabled: r.http + })), + sites: stats.sites.map((s) => ({ + site_name: this.anon(s.siteName), + megabytes_in: s.megabytesIn, + megabytes_out: s.megabytesOut, + type: s.type, + online: s.online + })), + num_api_keys: stats.numApiKeys, + num_custom_roles: stats.numCustomRoles + } + }); + } catch (error) { + logger.error("Failed to send analytics:", error); + } + } + + async sendTelemetry(eventName: string, properties: Record) { + if (!this.enabled || !this.client) return; + + const hostMeta = await getHostMeta(); + if (!hostMeta) { + logger.warn("Telemetry: Host meta not found, skipping telemetry"); + return; + } + + this.client.groupIdentify({ + groupType: "host_id", + groupKey: hostMeta.hostMetaId, + properties + }); + } + + shutdown() { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + + if (this.enabled && this.client) { + this.client.shutdown(); + } + } +} + +let telemetryClient!: TelemetryClient; + +export function initTelemetryClient() { + if (!telemetryClient) { + telemetryClient = new TelemetryClient(); + } + return telemetryClient; +} + +export default telemetryClient; diff --git a/server/license/license.ts b/server/license/license.ts index 0adc54fd..aeb628df 100644 --- a/server/license/license.ts +++ b/server/license/license.ts @@ -5,7 +5,7 @@ import NodeCache from "node-cache"; import { validateJWT } from "./licenseJwt"; import { count, eq } from "drizzle-orm"; import moment from "moment"; -import { setHostMeta } from "@server/setup/setHostMeta"; +import { setHostMeta } from "@server/lib/hostMeta"; import { encrypt, decrypt } from "@server/lib/crypto"; const keyTypes = ["HOST", "SITES"] as const; diff --git a/server/logger.ts b/server/logger.ts index cd12d735..15dd6e3f 100644 --- a/server/logger.ts +++ b/server/logger.ts @@ -3,6 +3,7 @@ import config from "@server/lib/config"; import * as winston from "winston"; import path from "path"; import { APP_PATH } from "./lib/consts"; +import telemetryClient from "./lib/telemetry"; const hformat = winston.format.printf( ({ level, label, message, timestamp, stack, ...metadata }) => { diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index 6b3f20b9..fd9a7c21 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -18,7 +18,7 @@ const migrations = [ { version: "1.6.0", run: m1 }, { version: "1.7.0", run: m2 }, { version: "1.8.0", run: m3 }, - { version: "1.9.0", run: m4 } + // { version: "1.9.0", run: m4 } // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 5b0850c8..5411261f 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -50,7 +50,7 @@ const migrations = [ { version: "1.6.0", run: m21 }, { version: "1.7.0", run: m22 }, { version: "1.8.0", run: m23 }, - { version: "1.9.0", run: m24 }, + // { version: "1.9.0", run: m24 }, // Add new migrations here as they are created ] as const; From 5c04b1e14ab3d33ea18d8974619c3a4cf40cd734 Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Thu, 14 Aug 2025 18:24:21 -0700 Subject: [PATCH 36/38] add site targets, client resources, and auto login --- config/config.example.yml | 50 +- messages/en-US.json | 108 +- server/auth/actions.ts | 5 + server/db/pg/schema.ts | 30 +- server/db/sqlite/schema.ts | 40 +- server/middlewares/index.ts | 1 + .../middlewares/verifySiteResourceAccess.ts | 62 ++ server/routers/client/targets.ts | 39 + server/routers/external.ts | 84 +- server/routers/integration.ts | 7 - server/routers/newt/handleGetConfigMessage.ts | 93 +- .../routers/newt/handleNewtRegisterMessage.ts | 97 +- server/routers/newt/targets.ts | 33 +- server/routers/resource/createResource.ts | 26 +- server/routers/resource/deleteResource.ts | 76 +- server/routers/resource/getResource.ts | 11 +- .../routers/resource/getResourceAuthInfo.ts | 4 +- server/routers/resource/getUserResources.ts | 88 +- server/routers/resource/index.ts | 3 +- server/routers/resource/listResources.ts | 176 ++-- server/routers/resource/transferResource.ts | 214 ---- server/routers/resource/updateResource.ts | 8 +- server/routers/role/addRoleSite.ts | 24 +- server/routers/role/index.ts | 4 +- server/routers/role/removeRoleSite.ts | 32 +- .../siteResource/createSiteResource.ts | 171 ++++ .../siteResource/deleteSiteResource.ts | 124 +++ .../routers/siteResource/getSiteResource.ts | 83 ++ server/routers/siteResource/index.ts | 6 + .../siteResource/listAllSiteResourcesByOrg.ts | 111 +++ .../routers/siteResource/listSiteResources.ts | 118 +++ .../siteResource/updateSiteResource.ts | 196 ++++ server/routers/target/createTarget.ts | 36 +- server/routers/target/deleteTarget.ts | 64 +- server/routers/target/getTarget.ts | 6 +- server/routers/target/helpers.ts | 56 +- server/routers/target/index.ts | 2 +- server/routers/target/listTargets.ts | 9 +- server/routers/target/updateTarget.ts | 41 +- server/routers/traefik/getTraefikConfig.ts | 171 ++-- server/routers/user/addUserSite.ts | 22 +- server/routers/user/removeUserSite.ts | 32 +- server/routers/ws/messageHandlers.ts | 2 +- server/setup/scriptsPg/1.9.0.ts | 2 +- server/setup/scriptsSqlite/1.9.0.ts | 2 +- .../settings/resources/ResourcesDataTable.tsx | 36 - .../resources/ResourcesSplashCard.tsx | 70 -- .../settings/resources/ResourcesTable.tsx | 662 ++++++++++++- .../[resourceId]/ResourceInfoBox.tsx | 29 +- .../[resourceId]/authentication/page.tsx | 141 ++- .../resources/[resourceId]/general/page.tsx | 238 +---- .../resources/[resourceId]/layout.tsx | 15 - .../resources/[resourceId]/proxy/page.tsx | 923 +++++++++++------ .../settings/resources/create/page.tsx | 925 +++++++++++++++--- src/app/[orgId]/settings/resources/page.tsx | 60 +- .../share-links/CreateShareLinkForm.tsx | 11 +- .../settings/share-links/ShareLinksTable.tsx | 4 +- .../settings/sites/[niceId]/SiteInfoCard.tsx | 4 +- .../settings/sites/[niceId]/general/page.tsx | 128 ++- .../[orgId]/settings/sites/create/page.tsx | 2 +- src/app/auth/initial-setup/page.tsx | 10 +- .../[resourceId]/AutoLoginHandler.tsx | 100 ++ src/app/auth/resource/[resourceId]/page.tsx | 21 +- src/components/ContainersSelector.tsx | 76 +- .../CreateInternalResourceDialog.tsx | 422 ++++++++ src/components/DomainPicker.tsx | 903 ++++++++++------- src/components/EditInternalResourceDialog.tsx | 276 ++++++ src/components/LayoutSidebar.tsx | 2 +- src/components/OrgSelector.tsx | 2 +- src/components/SidebarNav.tsx | 2 +- src/components/ui/alert.tsx | 2 +- src/components/ui/data-table.tsx | 82 +- src/components/ui/input-otp.tsx | 2 +- src/components/ui/input.tsx | 4 +- src/components/ui/select.tsx | 2 +- src/components/ui/tabs.tsx | 2 +- src/contexts/resourceContext.ts | 2 - src/hooks/useDockerSocket.ts | 168 ---- src/lib/docker.ts | 136 +++ src/providers/ResourceProvider.tsx | 5 +- 80 files changed, 5651 insertions(+), 2385 deletions(-) create mode 100644 server/middlewares/verifySiteResourceAccess.ts create mode 100644 server/routers/client/targets.ts delete mode 100644 server/routers/resource/transferResource.ts create mode 100644 server/routers/siteResource/createSiteResource.ts create mode 100644 server/routers/siteResource/deleteSiteResource.ts create mode 100644 server/routers/siteResource/getSiteResource.ts create mode 100644 server/routers/siteResource/index.ts create mode 100644 server/routers/siteResource/listAllSiteResourcesByOrg.ts create mode 100644 server/routers/siteResource/listSiteResources.ts create mode 100644 server/routers/siteResource/updateSiteResource.ts delete mode 100644 src/app/[orgId]/settings/resources/ResourcesDataTable.tsx delete mode 100644 src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx create mode 100644 src/app/auth/resource/[resourceId]/AutoLoginHandler.tsx create mode 100644 src/components/CreateInternalResourceDialog.tsx create mode 100644 src/components/EditInternalResourceDialog.tsx delete mode 100644 src/hooks/useDockerSocket.ts create mode 100644 src/lib/docker.ts diff --git a/config/config.example.yml b/config/config.example.yml index c5f70641..fcb7edde 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -2,47 +2,27 @@ # https://docs.digpangolin.com/self-host/advanced/config-file app: - dashboard_url: "http://localhost:3002" - log_level: "info" - save_logs: false + dashboard_url: http://localhost:3002 + log_level: debug domains: - domain1: - base_domain: "example.com" - cert_resolver: "letsencrypt" + domain1: + base_domain: example.com server: - external_port: 3000 - internal_port: 3001 - next_port: 3002 - internal_hostname: "pangolin" - session_cookie_name: "p_session_token" - resource_access_token_param: "p_token" - secret: "your_secret_key_here" - resource_access_token_headers: - id: "P-Access-Token-Id" - token: "P-Access-Token" - resource_session_request_param: "p_session_request" - -traefik: - http_entrypoint: "web" - https_entrypoint: "websecure" + secret: my_secret_key gerbil: - start_port: 51820 - base_endpoint: "localhost" - block_size: 24 - site_block_size: 30 - subnet_group: 100.89.137.0/20 - use_subdomain: true + base_endpoint: example.com -rate_limits: - global: - window_minutes: 1 - max_requests: 500 +orgs: + block_size: 24 + subnet_group: 100.90.137.0/20 flags: - require_email_verification: false - disable_signup_without_invite: true - disable_user_create_org: true - allow_raw_resources: true + require_email_verification: false + disable_signup_without_invite: true + disable_user_create_org: true + allow_raw_resources: true + enable_integration_api: true + enable_clients: true diff --git a/messages/en-US.json b/messages/en-US.json index 1a3fdfa8..6f80cbe9 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -166,7 +166,7 @@ "siteSelect": "Select site", "siteSearch": "Search site", "siteNotFound": "No site found.", - "siteSelectionDescription": "This site will provide connectivity to the resource.", + "siteSelectionDescription": "This site will provide connectivity to the target.", "resourceType": "Resource Type", "resourceTypeDescription": "Determine how you want to access your resource", "resourceHTTPSSettings": "HTTPS Settings", @@ -197,6 +197,7 @@ "general": "General", "generalSettings": "General Settings", "proxy": "Proxy", + "internal": "Internal", "rules": "Rules", "resourceSettingDescription": "Configure the settings on your resource", "resourceSetting": "{resourceName} Settings", @@ -490,7 +491,7 @@ "targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.", "targetTlsSubmit": "Save Settings", "targets": "Targets Configuration", - "targetsDescription": "Set up targets to route traffic to your services", + "targetsDescription": "Set up targets to route traffic to your backend services", "targetStickySessions": "Enable Sticky Sessions", "targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.", "methodSelect": "Select method", @@ -986,7 +987,7 @@ "actionGetSite": "Get Site", "actionListSites": "List Sites", "setupToken": "Setup Token", - "setupTokenPlaceholder": "Enter the setup token from the server console", + "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Update Site", "actionListSiteRoles": "List Allowed Site Roles", @@ -1345,9 +1346,106 @@ "resourceEnableProxy": "Enable Public Proxy", "resourceEnableProxyDescription": "Enable public proxying to this resource. This allows access to the resource from outside the network through the cloud on an open port. Requires Traefik config.", "externalProxyEnabled": "External Proxy Enabled", + "addNewTarget": "Add New Target", + "targetsList": "Targets List", + "targetErrorDuplicateTargetFound": "Duplicate target found", + "httpMethod": "HTTP Method", + "selectHttpMethod": "Select HTTP method", + "domainPickerSubdomainLabel": "Subdomain", + "domainPickerBaseDomainLabel": "Base Domain", + "domainPickerSearchDomains": "Search domains...", + "domainPickerNoDomainsFound": "No domains found", + "domainPickerLoadingDomains": "Loading domains...", + "domainPickerSelectBaseDomain": "Select base domain...", + "domainPickerNotAvailableForCname": "Not available for CNAME domains", + "domainPickerEnterSubdomainOrLeaveBlank": "Enter subdomain or leave blank to use base domain.", + "domainPickerEnterSubdomainToSearch": "Enter a subdomain to search and select from available free domains.", + "domainPickerFreeDomains": "Free Domains", + "domainPickerSearchForAvailableDomains": "Search for available domains", + "resourceDomain": "Domain", + "resourceEditDomain": "Edit Domain", + "siteName": "Site Name", + "proxyPort": "Port", + "resourcesTableProxyResources": "Proxy Resources", + "resourcesTableClientResources": "Client Resources", + "resourcesTableNoProxyResourcesFound": "No proxy resources found.", + "resourcesTableNoInternalResourcesFound": "No internal resources found.", + "resourcesTableDestination": "Destination", + "resourcesTableTheseResourcesForUseWith": "These resources are for use with", + "resourcesTableClients": "Clients", + "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", + "editInternalResourceDialogEditClientResource": "Edit Client Resource", + "editInternalResourceDialogUpdateResourceProperties": "Update the resource properties and target configuration for {resourceName}.", + "editInternalResourceDialogResourceProperties": "Resource Properties", + "editInternalResourceDialogName": "Name", + "editInternalResourceDialogProtocol": "Protocol", + "editInternalResourceDialogSitePort": "Site Port", + "editInternalResourceDialogTargetConfiguration": "Target Configuration", + "editInternalResourceDialogDestinationIP": "Destination IP", + "editInternalResourceDialogDestinationPort": "Destination Port", + "editInternalResourceDialogCancel": "Cancel", + "editInternalResourceDialogSaveResource": "Save Resource", + "editInternalResourceDialogSuccess": "Success", + "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", + "editInternalResourceDialogError": "Error", + "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", + "editInternalResourceDialogNameRequired": "Name is required", + "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "editInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "editInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "editInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "editInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", + "createInternalResourceDialogNoSitesAvailable": "No Sites Available", + "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", + "createInternalResourceDialogClose": "Close", + "createInternalResourceDialogCreateClientResource": "Create Client Resource", + "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will be accessible to clients connected to the selected site.", + "createInternalResourceDialogResourceProperties": "Resource Properties", + "createInternalResourceDialogName": "Name", + "createInternalResourceDialogSite": "Site", + "createInternalResourceDialogSelectSite": "Select site...", + "createInternalResourceDialogSearchSites": "Search sites...", + "createInternalResourceDialogNoSitesFound": "No sites found.", + "createInternalResourceDialogProtocol": "Protocol", + "createInternalResourceDialogTcp": "TCP", + "createInternalResourceDialogUdp": "UDP", + "createInternalResourceDialogSitePort": "Site Port", + "createInternalResourceDialogSitePortDescription": "Use this port to access the resource on the site when connected with a client.", + "createInternalResourceDialogTargetConfiguration": "Target Configuration", + "createInternalResourceDialogDestinationIP": "Destination IP", + "createInternalResourceDialogDestinationIPDescription": "The IP address of the resource on the site's network.", + "createInternalResourceDialogDestinationPort": "Destination Port", + "createInternalResourceDialogDestinationPortDescription": "The port on the destination IP where the resource is accessible.", + "createInternalResourceDialogCancel": "Cancel", + "createInternalResourceDialogCreateResource": "Create Resource", + "createInternalResourceDialogSuccess": "Success", + "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", + "createInternalResourceDialogError": "Error", + "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", + "createInternalResourceDialogNameRequired": "Name is required", + "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", + "createInternalResourceDialogPleaseSelectSite": "Please select a site", + "createInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", + "createInternalResourceDialogProxyPortMax": "Proxy port must be less than 65536", + "createInternalResourceDialogInvalidIPAddressFormat": "Invalid IP address format", + "createInternalResourceDialogDestinationPortMin": "Destination port must be at least 1", + "createInternalResourceDialogDestinationPortMax": "Destination port must be less than 65536", "siteConfiguration": "Configuration", "siteAcceptClientConnections": "Accept Client Connections", "siteAcceptClientConnectionsDescription": "Allow other devices to connect through this Newt instance as a gateway using clients.", "siteAddress": "Site Address", - "siteAddressDescription": "Specify the IP address of the host for clients to connect to." -} \ No newline at end of file + "siteAddressDescription": "Specify the IP address of the host for clients to connect to.", + "autoLoginExternalIdp": "Auto Login with External IDP", + "autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.", + "selectIdp": "Select IDP", + "selectIdpPlaceholder": "Choose an IDP...", + "selectIdpRequired": "Please select an IDP when auto login is enabled.", + "autoLoginTitle": "Redirecting", + "autoLoginDescription": "Redirecting you to the external identity provider for authentication.", + "autoLoginProcessing": "Preparing authentication...", + "autoLoginRedirecting": "Redirecting to login...", + "autoLoginError": "Auto Login Error", + "autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.", + "autoLoginErrorGeneratingUrl": "Failed to generate authentication URL." +} diff --git a/server/auth/actions.ts b/server/auth/actions.ts index ee2c5dac..a3ad60ab 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -69,6 +69,11 @@ export enum ActionsEnum { deleteResourceRule = "deleteResourceRule", listResourceRules = "listResourceRules", updateResourceRule = "updateResourceRule", + createSiteResource = "createSiteResource", + deleteSiteResource = "deleteSiteResource", + getSiteResource = "getSiteResource", + listSiteResources = "listSiteResources", + updateSiteResource = "updateSiteResource", createClient = "createClient", deleteClient = "deleteClient", updateClient = "updateClient", diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 2ba10e3e..a2ec521e 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -66,11 +66,6 @@ export const sites = pgTable("sites", { export const resources = pgTable("resources", { resourceId: serial("resourceId").primaryKey(), - siteId: integer("siteId") - .references(() => sites.siteId, { - onDelete: "cascade" - }) - .notNull(), orgId: varchar("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -97,6 +92,9 @@ export const resources = pgTable("resources", { tlsServerName: varchar("tlsServerName"), setHostHeader: varchar("setHostHeader"), enableProxy: boolean("enableProxy").default(true), + skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { + onDelete: "cascade" + }), }); export const targets = pgTable("targets", { @@ -106,6 +104,11 @@ export const targets = pgTable("targets", { onDelete: "cascade" }) .notNull(), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), ip: varchar("ip").notNull(), method: varchar("method"), port: integer("port").notNull(), @@ -124,6 +127,22 @@ export const exitNodes = pgTable("exitNodes", { maxConnections: integer("maxConnections") }); +export const siteResources = pgTable("siteResources", { // this is for the clients + siteResourceId: serial("siteResourceId").primaryKey(), + siteId: integer("siteId") + .notNull() + .references(() => sites.siteId, { onDelete: "cascade" }), + orgId: varchar("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + name: varchar("name").notNull(), + protocol: varchar("protocol").notNull(), + proxyPort: integer("proxyPort").notNull(), + destinationPort: integer("destinationPort").notNull(), + destinationIp: varchar("destinationIp").notNull(), + enabled: boolean("enabled").notNull().default(true), +}); + export const users = pgTable("user", { userId: varchar("id").primaryKey(), email: varchar("email"), @@ -647,4 +666,5 @@ export type OlmSession = InferSelectModel; export type UserClient = InferSelectModel; export type RoleClient = InferSelectModel; export type OrgDomains = InferSelectModel; +export type SiteResource = InferSelectModel; export type SetupToken = InferSelectModel; diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 5bd81d6a..3dde2dd7 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -67,16 +67,11 @@ export const sites = sqliteTable("sites", { dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" }) .notNull() .default(true), - remoteSubnets: text("remoteSubnets"), // comma-separated list of subnets that this site can access + remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access }); export const resources = sqliteTable("resources", { resourceId: integer("resourceId").primaryKey({ autoIncrement: true }), - siteId: integer("siteId") - .references(() => sites.siteId, { - onDelete: "cascade" - }) - .notNull(), orgId: text("orgId") .references(() => orgs.orgId, { onDelete: "cascade" @@ -109,6 +104,9 @@ export const resources = sqliteTable("resources", { tlsServerName: text("tlsServerName"), setHostHeader: text("setHostHeader"), enableProxy: integer("enableProxy", { mode: "boolean" }).default(true), + skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, { + onDelete: "cascade" + }), }); export const targets = sqliteTable("targets", { @@ -118,6 +116,11 @@ export const targets = sqliteTable("targets", { onDelete: "cascade" }) .notNull(), + siteId: integer("siteId") + .references(() => sites.siteId, { + onDelete: "cascade" + }) + .notNull(), ip: text("ip").notNull(), method: text("method"), port: integer("port").notNull(), @@ -136,6 +139,22 @@ export const exitNodes = sqliteTable("exitNodes", { maxConnections: integer("maxConnections") }); +export const siteResources = sqliteTable("siteResources", { // this is for the clients + siteResourceId: integer("siteResourceId").primaryKey({ autoIncrement: true }), + siteId: integer("siteId") + .notNull() + .references(() => sites.siteId, { onDelete: "cascade" }), + orgId: text("orgId") + .notNull() + .references(() => orgs.orgId, { onDelete: "cascade" }), + name: text("name").notNull(), + protocol: text("protocol").notNull(), + proxyPort: integer("proxyPort").notNull(), + destinationPort: integer("destinationPort").notNull(), + destinationIp: text("destinationIp").notNull(), + enabled: integer("enabled", { mode: "boolean" }).notNull().default(true), +}); + export const users = sqliteTable("user", { userId: text("id").primaryKey(), email: text("email"), @@ -166,9 +185,11 @@ export const users = sqliteTable("user", { export const securityKeys = sqliteTable("webauthnCredentials", { credentialId: text("credentialId").primaryKey(), - userId: text("userId").notNull().references(() => users.userId, { - onDelete: "cascade" - }), + userId: text("userId") + .notNull() + .references(() => users.userId, { + onDelete: "cascade" + }), publicKey: text("publicKey").notNull(), signCount: integer("signCount").notNull(), transports: text("transports"), @@ -688,6 +709,7 @@ export type Idp = InferSelectModel; export type ApiKey = InferSelectModel; export type ApiKeyAction = InferSelectModel; export type ApiKeyOrg = InferSelectModel; +export type SiteResource = InferSelectModel; export type OrgDomains = InferSelectModel; export type SetupToken = InferSelectModel; export type HostMeta = InferSelectModel; diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index b1180995..28a73afd 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -27,3 +27,4 @@ export * from "./verifyApiKeyAccess"; export * from "./verifyDomainAccess"; export * from "./verifyClientsEnabled"; export * from "./verifyUserIsOrgOwner"; +export * from "./verifySiteResourceAccess"; diff --git a/server/middlewares/verifySiteResourceAccess.ts b/server/middlewares/verifySiteResourceAccess.ts new file mode 100644 index 00000000..e7fefd24 --- /dev/null +++ b/server/middlewares/verifySiteResourceAccess.ts @@ -0,0 +1,62 @@ +import { Request, Response, NextFunction } from "express"; +import { db } from "@server/db"; +import { siteResources } from "@server/db"; +import { eq, and } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; +import logger from "@server/logger"; + +export async function verifySiteResourceAccess( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const siteResourceId = parseInt(req.params.siteResourceId); + const siteId = parseInt(req.params.siteId); + const orgId = req.params.orgId; + + if (!siteResourceId || !siteId || !orgId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Missing required parameters" + ) + ); + } + + // Check if the site resource exists and belongs to the specified site and org + const [siteResource] = await db + .select() + .from(siteResources) + .where(and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + )) + .limit(1); + + if (!siteResource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Site resource not found" + ) + ); + } + + // Attach the siteResource to the request for use in the next middleware/route + // @ts-ignore - Extending Request type + req.siteResource = siteResource; + + next(); + } catch (error) { + logger.error("Error verifying site resource access:", error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying site resource access" + ) + ); + } +} diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts new file mode 100644 index 00000000..8d13d8cf --- /dev/null +++ b/server/routers/client/targets.ts @@ -0,0 +1,39 @@ +import { sendToClient } from "../ws"; + +export async function addTargets( + newtId: string, + destinationIp: string, + destinationPort: number, + protocol: string, + port: number | null = null +) { + const target = `${port ? port + ":" : ""}${ + destinationIp + }:${destinationPort}`; + + await sendToClient(newtId, { + type: `newt/wg/${protocol}/add`, + data: { + targets: [target] // We can only use one target for WireGuard right now + } + }); +} + +export async function removeTargets( + newtId: string, + destinationIp: string, + destinationPort: number, + protocol: string, + port: number | null = null +) { + const target = `${port ? port + ":" : ""}${ + destinationIp + }:${destinationPort}`; + + await sendToClient(newtId, { + type: `newt/wg/${protocol}/remove`, + data: { + targets: [target] // We can only use one target for WireGuard right now + } + }); +} diff --git a/server/routers/external.ts b/server/routers/external.ts index f9ff7377..65dc6108 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -9,6 +9,7 @@ import * as user from "./user"; import * as auth from "./auth"; import * as role from "./role"; import * as client from "./client"; +import * as siteResource from "./siteResource"; import * as supporterKey from "./supporterKey"; import * as accessToken from "./accessToken"; import * as idp from "./idp"; @@ -34,7 +35,8 @@ import { verifyDomainAccess, verifyClientsEnabled, verifyUserHasAction, - verifyUserIsOrgOwner + verifyUserIsOrgOwner, + verifySiteResourceAccess } from "@server/middlewares"; import { createStore } from "@server/lib/rateLimitStore"; import { ActionsEnum } from "@server/auth/actions"; @@ -213,9 +215,60 @@ authenticated.get( site.listContainers ); +// Site Resource endpoints authenticated.put( "/org/:orgId/site/:siteId/resource", verifyOrgAccess, + verifySiteAccess, + verifyUserHasAction(ActionsEnum.createSiteResource), + siteResource.createSiteResource +); + +authenticated.get( + "/org/:orgId/site/:siteId/resources", + verifyOrgAccess, + verifySiteAccess, + verifyUserHasAction(ActionsEnum.listSiteResources), + siteResource.listSiteResources +); + +authenticated.get( + "/org/:orgId/site-resources", + verifyOrgAccess, + verifyUserHasAction(ActionsEnum.listSiteResources), + siteResource.listAllSiteResourcesByOrg +); + +authenticated.get( + "/org/:orgId/site/:siteId/resource/:siteResourceId", + verifyOrgAccess, + verifySiteAccess, + verifySiteResourceAccess, + verifyUserHasAction(ActionsEnum.getSiteResource), + siteResource.getSiteResource +); + +authenticated.post( + "/org/:orgId/site/:siteId/resource/:siteResourceId", + verifyOrgAccess, + verifySiteAccess, + verifySiteResourceAccess, + verifyUserHasAction(ActionsEnum.updateSiteResource), + siteResource.updateSiteResource +); + +authenticated.delete( + "/org/:orgId/site/:siteId/resource/:siteResourceId", + verifyOrgAccess, + verifySiteAccess, + verifySiteResourceAccess, + verifyUserHasAction(ActionsEnum.deleteSiteResource), + siteResource.deleteSiteResource +); + +authenticated.put( + "/org/:orgId/resource", + verifyOrgAccess, verifyUserHasAction(ActionsEnum.createResource), resource.createResource ); @@ -397,28 +450,6 @@ authenticated.post( user.addUserRole ); -// authenticated.put( -// "/role/:roleId/site", -// verifyRoleAccess, -// verifyUserInRole, -// verifyUserHasAction(ActionsEnum.addRoleSite), -// role.addRoleSite -// ); -// authenticated.delete( -// "/role/:roleId/site", -// verifyRoleAccess, -// verifyUserInRole, -// verifyUserHasAction(ActionsEnum.removeRoleSite), -// role.removeRoleSite -// ); -// authenticated.get( -// "/role/:roleId/sites", -// verifyRoleAccess, -// verifyUserInRole, -// verifyUserHasAction(ActionsEnum.listRoleSites), -// role.listRoleSites -// ); - authenticated.post( "/resource/:resourceId/roles", verifyResourceAccess, @@ -463,13 +494,6 @@ authenticated.get( resource.getResourceWhitelist ); -authenticated.post( - `/resource/:resourceId/transfer`, - verifyResourceAccess, - verifyUserHasAction(ActionsEnum.updateResource), - resource.transferResource -); - authenticated.post( `/resource/:resourceId/access-token`, verifyResourceAccess, diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 39939e1c..ee707333 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -341,13 +341,6 @@ authenticated.get( resource.getResourceWhitelist ); -authenticated.post( - `/resource/:resourceId/transfer`, - verifyApiKeyResourceAccess, - verifyApiKeyHasAction(ActionsEnum.updateResource), - resource.transferResource -); - authenticated.post( `/resource/:resourceId/access-token`, verifyApiKeyResourceAccess, diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index b2594a71..7d6b3567 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -220,78 +220,37 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { // Filter out any null values from peers that didn't have an olm const validPeers = peers.filter((peer) => peer !== null); - // Improved version - const allResources = await db.transaction(async (tx) => { - // First get all resources for the site - const resourcesList = await tx - .select({ - resourceId: resources.resourceId, - subdomain: resources.subdomain, - fullDomain: resources.fullDomain, - ssl: resources.ssl, - blockAccess: resources.blockAccess, - sso: resources.sso, - emailWhitelistEnabled: resources.emailWhitelistEnabled, - http: resources.http, - proxyPort: resources.proxyPort, - protocol: resources.protocol - }) - .from(resources) - .where(and(eq(resources.siteId, siteId), eq(resources.http, false))); + // Get all enabled targets with their resource protocol information + const allTargets = await db + .select({ + resourceId: targets.resourceId, + targetId: targets.targetId, + ip: targets.ip, + method: targets.method, + port: targets.port, + internalPort: targets.internalPort, + enabled: targets.enabled, + protocol: resources.protocol + }) + .from(targets) + .innerJoin(resources, eq(targets.resourceId, resources.resourceId)) + .where(and(eq(targets.siteId, siteId), eq(targets.enabled, true))); - // Get all enabled targets for these resources in a single query - const resourceIds = resourcesList.map((r) => r.resourceId); - const allTargets = - resourceIds.length > 0 - ? await tx - .select({ - resourceId: targets.resourceId, - targetId: targets.targetId, - ip: targets.ip, - method: targets.method, - port: targets.port, - internalPort: targets.internalPort, - enabled: targets.enabled, - }) - .from(targets) - .where( - and( - inArray(targets.resourceId, resourceIds), - eq(targets.enabled, true) - ) - ) - : []; + const { tcpTargets, udpTargets } = allTargets.reduce( + (acc, target) => { + // Filter out invalid targets + if (!target.internalPort || !target.ip || !target.port) { + return acc; + } - // Combine the data in JS instead of using SQL for the JSON - return resourcesList.map((resource) => ({ - ...resource, - targets: allTargets.filter( - (target) => target.resourceId === resource.resourceId - ) - })); - }); - - const { tcpTargets, udpTargets } = allResources.reduce( - (acc, resource) => { - // Skip resources with no targets - if (!resource.targets?.length) return acc; - - // Format valid targets into strings - const formattedTargets = resource.targets - .filter( - (target: Target) => - resource.proxyPort && target?.ip && target?.port - ) - .map( - (target: Target) => - `${resource.proxyPort}:${target.ip}:${target.port}` - ); + // Format target into string + const formattedTarget = `${target.internalPort}:${target.ip}:${target.port}`; // Add to the appropriate protocol array - if (resource.protocol === "tcp") { - acc.tcpTargets.push(...formattedTargets); + if (target.protocol === "tcp") { + acc.tcpTargets.push(formattedTarget); } else { - acc.udpTargets.push(...formattedTargets); + acc.udpTargets.push(formattedTarget); } return acc; diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index 71a6fd5c..0255e97c 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -105,7 +105,9 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { .limit(1); const blockSize = config.getRawConfig().gerbil.site_block_size; - const subnets = sitesQuery.map((site) => site.subnet).filter((subnet) => subnet !== null); + const subnets = sitesQuery + .map((site) => site.subnet) + .filter((subnet) => subnet !== null); subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`)); const newSubnet = findNextAvailableCidr( subnets, @@ -160,78 +162,37 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { allowedIps: [siteSubnet] }); - // Improved version - const allResources = await db.transaction(async (tx) => { - // First get all resources for the site - const resourcesList = await tx - .select({ - resourceId: resources.resourceId, - subdomain: resources.subdomain, - fullDomain: resources.fullDomain, - ssl: resources.ssl, - blockAccess: resources.blockAccess, - sso: resources.sso, - emailWhitelistEnabled: resources.emailWhitelistEnabled, - http: resources.http, - proxyPort: resources.proxyPort, - protocol: resources.protocol - }) - .from(resources) - .where(eq(resources.siteId, siteId)); + // Get all enabled targets with their resource protocol information + const allTargets = await db + .select({ + resourceId: targets.resourceId, + targetId: targets.targetId, + ip: targets.ip, + method: targets.method, + port: targets.port, + internalPort: targets.internalPort, + enabled: targets.enabled, + protocol: resources.protocol + }) + .from(targets) + .innerJoin(resources, eq(targets.resourceId, resources.resourceId)) + .where(and(eq(targets.siteId, siteId), eq(targets.enabled, true))); - // Get all enabled targets for these resources in a single query - const resourceIds = resourcesList.map((r) => r.resourceId); - const allTargets = - resourceIds.length > 0 - ? await tx - .select({ - resourceId: targets.resourceId, - targetId: targets.targetId, - ip: targets.ip, - method: targets.method, - port: targets.port, - internalPort: targets.internalPort, - enabled: targets.enabled - }) - .from(targets) - .where( - and( - inArray(targets.resourceId, resourceIds), - eq(targets.enabled, true) - ) - ) - : []; + const { tcpTargets, udpTargets } = allTargets.reduce( + (acc, target) => { + // Filter out invalid targets + if (!target.internalPort || !target.ip || !target.port) { + return acc; + } - // Combine the data in JS instead of using SQL for the JSON - return resourcesList.map((resource) => ({ - ...resource, - targets: allTargets.filter( - (target) => target.resourceId === resource.resourceId - ) - })); - }); - - const { tcpTargets, udpTargets } = allResources.reduce( - (acc, resource) => { - // Skip resources with no targets - if (!resource.targets?.length) return acc; - - // Format valid targets into strings - const formattedTargets = resource.targets - .filter( - (target: Target) => - target?.internalPort && target?.ip && target?.port - ) - .map( - (target: Target) => - `${target.internalPort}:${target.ip}:${target.port}` - ); + // Format target into string + const formattedTarget = `${target.internalPort}:${target.ip}:${target.port}`; // Add to the appropriate protocol array - if (resource.protocol === "tcp") { - acc.tcpTargets.push(...formattedTargets); + if (target.protocol === "tcp") { + acc.tcpTargets.push(formattedTarget); } else { - acc.udpTargets.push(...formattedTargets); + acc.udpTargets.push(formattedTarget); } return acc; diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts index 642fc2df..91a0ac3f 100644 --- a/server/routers/newt/targets.ts +++ b/server/routers/newt/targets.ts @@ -1,7 +1,8 @@ import { Target } from "@server/db"; import { sendToClient } from "../ws"; +import logger from "@server/logger"; -export function addTargets( +export async function addTargets( newtId: string, targets: Target[], protocol: string, @@ -20,22 +21,9 @@ export function addTargets( targets: payloadTargets } }); - - const payloadTargetsResources = targets.map((target) => { - return `${port ? port + ":" : ""}${ - target.ip - }:${target.port}`; - }); - - sendToClient(newtId, { - type: `newt/wg/${protocol}/add`, - data: { - targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now - } - }); } -export function removeTargets( +export async function removeTargets( newtId: string, targets: Target[], protocol: string, @@ -48,23 +36,10 @@ export function removeTargets( }:${target.port}`; }); - sendToClient(newtId, { + await sendToClient(newtId, { type: `newt/${protocol}/remove`, data: { targets: payloadTargets } }); - - const payloadTargetsResources = targets.map((target) => { - return `${port ? port + ":" : ""}${ - target.ip - }:${target.port}`; - }); - - sendToClient(newtId, { - type: `newt/wg/${protocol}/remove`, - data: { - targets: [payloadTargetsResources[0]] // We can only use one target for WireGuard right now - } - }); } diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index 8c80c90c..e3e431ec 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -15,7 +15,6 @@ import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import { eq, and } from "drizzle-orm"; -import stoi from "@server/lib/stoi"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { subdomainSchema } from "@server/lib/schemas"; @@ -25,7 +24,6 @@ import { build } from "@server/build"; const createResourceParamsSchema = z .object({ - siteId: z.string().transform(stoi).pipe(z.number().int().positive()), orgId: z.string() }) .strict(); @@ -34,7 +32,6 @@ const createHttpResourceSchema = z .object({ name: z.string().min(1).max(255), subdomain: z.string().nullable().optional(), - siteId: z.number(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), domainId: z.string() @@ -53,11 +50,10 @@ const createHttpResourceSchema = z const createRawResourceSchema = z .object({ name: z.string().min(1).max(255), - siteId: z.number(), http: z.boolean(), protocol: z.enum(["tcp", "udp"]), proxyPort: z.number().int().min(1).max(65535), - enableProxy: z.boolean().default(true) + // enableProxy: z.boolean().default(true) // always true now }) .strict() .refine( @@ -78,7 +74,7 @@ export type CreateResourceResponse = Resource; registry.registerPath({ method: "put", - path: "/org/{orgId}/site/{siteId}/resource", + path: "/org/{orgId}/resource", description: "Create a resource.", tags: [OpenAPITags.Org, OpenAPITags.Resource], request: { @@ -111,7 +107,7 @@ export async function createResource( ); } - const { siteId, orgId } = parsedParams.data; + const { orgId } = parsedParams.data; if (req.user && !req.userOrgRoleId) { return next( @@ -146,7 +142,7 @@ export async function createResource( if (http) { return await createHttpResource( { req, res, next }, - { siteId, orgId } + { orgId } ); } else { if ( @@ -162,7 +158,7 @@ export async function createResource( } return await createRawResource( { req, res, next }, - { siteId, orgId } + { orgId } ); } } catch (error) { @@ -180,12 +176,11 @@ async function createHttpResource( next: NextFunction; }, meta: { - siteId: number; orgId: string; } ) { const { req, res, next } = route; - const { siteId, orgId } = meta; + const { orgId } = meta; const parsedBody = createHttpResourceSchema.safeParse(req.body); if (!parsedBody.success) { @@ -292,7 +287,6 @@ async function createHttpResource( const newResource = await trx .insert(resources) .values({ - siteId, fullDomain, domainId, orgId, @@ -357,12 +351,11 @@ async function createRawResource( next: NextFunction; }, meta: { - siteId: number; orgId: string; } ) { const { req, res, next } = route; - const { siteId, orgId } = meta; + const { orgId } = meta; const parsedBody = createRawResourceSchema.safeParse(req.body); if (!parsedBody.success) { @@ -374,7 +367,7 @@ async function createRawResource( ); } - const { name, http, protocol, proxyPort, enableProxy } = parsedBody.data; + const { name, http, protocol, proxyPort } = parsedBody.data; // if http is false check to see if there is already a resource with the same port and protocol const existingResource = await db @@ -402,13 +395,12 @@ async function createRawResource( const newResource = await trx .insert(resources) .values({ - siteId, orgId, name, http, protocol, proxyPort, - enableProxy + // enableProxy }) .returning(); diff --git a/server/routers/resource/deleteResource.ts b/server/routers/resource/deleteResource.ts index 99adc5f7..3b0e9df4 100644 --- a/server/routers/resource/deleteResource.ts +++ b/server/routers/resource/deleteResource.ts @@ -71,44 +71,44 @@ export async function deleteResource( ); } - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, deletedResource.siteId!)) - .limit(1); - - if (!site) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Site with ID ${deletedResource.siteId} not found` - ) - ); - } - - if (site.pubKey) { - if (site.type == "wireguard") { - await addPeer(site.exitNodeId!, { - publicKey: site.pubKey, - allowedIps: await getAllowedIps(site.siteId) - }); - } else if (site.type == "newt") { - // get the newt on the site by querying the newt table for siteId - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); - - removeTargets( - newt.newtId, - targetsToBeRemoved, - deletedResource.protocol, - deletedResource.proxyPort - ); - } - } - + // const [site] = await db + // .select() + // .from(sites) + // .where(eq(sites.siteId, deletedResource.siteId!)) + // .limit(1); + // + // if (!site) { + // return next( + // createHttpError( + // HttpCode.NOT_FOUND, + // `Site with ID ${deletedResource.siteId} not found` + // ) + // ); + // } + // + // if (site.pubKey) { + // if (site.type == "wireguard") { + // await addPeer(site.exitNodeId!, { + // publicKey: site.pubKey, + // allowedIps: await getAllowedIps(site.siteId) + // }); + // } else if (site.type == "newt") { + // // get the newt on the site by querying the newt table for siteId + // const [newt] = await db + // .select() + // .from(newts) + // .where(eq(newts.siteId, site.siteId)) + // .limit(1); + // + // removeTargets( + // newt.newtId, + // targetsToBeRemoved, + // deletedResource.protocol, + // deletedResource.proxyPort + // ); + // } + // } + // return response(res, { data: null, success: true, diff --git a/server/routers/resource/getResource.ts b/server/routers/resource/getResource.ts index 0cffb1cf..a2c1c0d1 100644 --- a/server/routers/resource/getResource.ts +++ b/server/routers/resource/getResource.ts @@ -19,9 +19,7 @@ const getResourceSchema = z }) .strict(); -export type GetResourceResponse = Resource & { - siteName: string; -}; +export type GetResourceResponse = Resource; registry.registerPath({ method: "get", @@ -56,11 +54,9 @@ export async function getResource( .select() .from(resources) .where(eq(resources.resourceId, resourceId)) - .leftJoin(sites, eq(sites.siteId, resources.siteId)) .limit(1); - const resource = resp.resources; - const site = resp.sites; + const resource = resp; if (!resource) { return next( @@ -73,8 +69,7 @@ export async function getResource( return response(res, { data: { - ...resource, - siteName: site?.name + ...resource }, success: true, error: false, diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts index 64fade89..191221f1 100644 --- a/server/routers/resource/getResourceAuthInfo.ts +++ b/server/routers/resource/getResourceAuthInfo.ts @@ -31,6 +31,7 @@ export type GetResourceAuthInfoResponse = { blockAccess: boolean; url: string; whitelist: boolean; + skipToIdpId: number | null; }; export async function getResourceAuthInfo( @@ -86,7 +87,8 @@ export async function getResourceAuthInfo( sso: resource.sso, blockAccess: resource.blockAccess, url, - whitelist: resource.emailWhitelistEnabled + whitelist: resource.emailWhitelistEnabled, + skipToIdpId: resource.skipToIdpId }, success: true, error: false, diff --git a/server/routers/resource/getUserResources.ts b/server/routers/resource/getUserResources.ts index 681ec4d0..3d28da6f 100644 --- a/server/routers/resource/getUserResources.ts +++ b/server/routers/resource/getUserResources.ts @@ -1,16 +1,14 @@ import { Request, Response, NextFunction } from "express"; import { db } from "@server/db"; import { and, eq, or, inArray } from "drizzle-orm"; -import { - resources, - userResources, - roleResources, - userOrgs, - roles, +import { + resources, + userResources, + roleResources, + userOrgs, resourcePassword, resourcePincode, - resourceWhitelist, - sites + resourceWhitelist } from "@server/db"; import createHttpError from "http-errors"; import HttpCode from "@server/types/HttpCode"; @@ -37,12 +35,7 @@ export async function getUserResources( roleId: userOrgs.roleId }) .from(userOrgs) - .where( - and( - eq(userOrgs.userId, userId), - eq(userOrgs.orgId, orgId) - ) - ) + .where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId))) .limit(1); if (userOrgResult.length === 0) { @@ -71,8 +64,8 @@ export async function getUserResources( // Combine all accessible resource IDs const accessibleResourceIds = [ - ...directResources.map(r => r.resourceId), - ...roleResourceResults.map(r => r.resourceId) + ...directResources.map((r) => r.resourceId), + ...roleResourceResults.map((r) => r.resourceId) ]; if (accessibleResourceIds.length === 0) { @@ -95,11 +88,9 @@ export async function getUserResources( enabled: resources.enabled, sso: resources.sso, protocol: resources.protocol, - emailWhitelistEnabled: resources.emailWhitelistEnabled, - siteName: sites.name + emailWhitelistEnabled: resources.emailWhitelistEnabled }) .from(resources) - .leftJoin(sites, eq(sites.siteId, resources.siteId)) .where( and( inArray(resources.resourceId, accessibleResourceIds), @@ -111,28 +102,61 @@ export async function getUserResources( // Check for password, pincode, and whitelist protection for each resource const resourcesWithAuth = await Promise.all( resourcesData.map(async (resource) => { - const [passwordCheck, pincodeCheck, whitelistCheck] = await Promise.all([ - db.select().from(resourcePassword).where(eq(resourcePassword.resourceId, resource.resourceId)).limit(1), - db.select().from(resourcePincode).where(eq(resourcePincode.resourceId, resource.resourceId)).limit(1), - db.select().from(resourceWhitelist).where(eq(resourceWhitelist.resourceId, resource.resourceId)).limit(1) - ]); + const [passwordCheck, pincodeCheck, whitelistCheck] = + await Promise.all([ + db + .select() + .from(resourcePassword) + .where( + eq( + resourcePassword.resourceId, + resource.resourceId + ) + ) + .limit(1), + db + .select() + .from(resourcePincode) + .where( + eq( + resourcePincode.resourceId, + resource.resourceId + ) + ) + .limit(1), + db + .select() + .from(resourceWhitelist) + .where( + eq( + resourceWhitelist.resourceId, + resource.resourceId + ) + ) + .limit(1) + ]); const hasPassword = passwordCheck.length > 0; const hasPincode = pincodeCheck.length > 0; - const hasWhitelist = whitelistCheck.length > 0 || resource.emailWhitelistEnabled; + const hasWhitelist = + whitelistCheck.length > 0 || resource.emailWhitelistEnabled; return { resourceId: resource.resourceId, name: resource.name, domain: `${resource.ssl ? "https://" : "http://"}${resource.fullDomain}`, enabled: resource.enabled, - protected: !!(resource.sso || hasPassword || hasPincode || hasWhitelist), + protected: !!( + resource.sso || + hasPassword || + hasPincode || + hasWhitelist + ), protocol: resource.protocol, sso: resource.sso, password: hasPassword, pincode: hasPincode, - whitelist: hasWhitelist, - siteName: resource.siteName + whitelist: hasWhitelist }; }) ); @@ -144,11 +168,13 @@ export async function getUserResources( message: "User resources retrieved successfully", status: HttpCode.OK }); - } catch (error) { console.error("Error fetching user resources:", error); return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Internal server error") + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Internal server error" + ) ); } } @@ -165,4 +191,4 @@ export type GetUserResourcesResponse = { protocol: string; }>; }; -}; \ No newline at end of file +}; diff --git a/server/routers/resource/index.ts b/server/routers/resource/index.ts index f97fcdf4..1a2e5c2d 100644 --- a/server/routers/resource/index.ts +++ b/server/routers/resource/index.ts @@ -16,10 +16,9 @@ export * from "./setResourceWhitelist"; export * from "./getResourceWhitelist"; export * from "./authWithWhitelist"; export * from "./authWithAccessToken"; -export * from "./transferResource"; export * from "./getExchangeToken"; export * from "./createResourceRule"; export * from "./deleteResourceRule"; export * from "./listResourceRules"; export * from "./updateResourceRule"; -export * from "./getUserResources"; \ No newline at end of file +export * from "./getUserResources"; diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index 6df56001..43757b27 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -3,7 +3,6 @@ import { z } from "zod"; import { db } from "@server/db"; import { resources, - sites, userResources, roleResources, resourcePassword, @@ -20,17 +19,9 @@ import { OpenAPITags, registry } from "@server/openApi"; const listResourcesParamsSchema = z .object({ - siteId: z - .string() - .optional() - .transform(stoi) - .pipe(z.number().int().positive().optional()), - orgId: z.string().optional() + orgId: z.string() }) - .strict() - .refine((data) => !!data.siteId !== !!data.orgId, { - message: "Either siteId or orgId must be provided, but not both" - }); + .strict(); const listResourcesSchema = z.object({ limit: z @@ -48,82 +39,38 @@ const listResourcesSchema = z.object({ .pipe(z.number().int().nonnegative()) }); -function queryResources( - accessibleResourceIds: number[], - siteId?: number, - orgId?: string -) { - if (siteId) { - return db - .select({ - resourceId: resources.resourceId, - name: resources.name, - fullDomain: resources.fullDomain, - ssl: resources.ssl, - siteName: sites.name, - siteId: sites.niceId, - passwordId: resourcePassword.passwordId, - pincodeId: resourcePincode.pincodeId, - sso: resources.sso, - whitelist: resources.emailWhitelistEnabled, - http: resources.http, - protocol: resources.protocol, - proxyPort: resources.proxyPort, - enabled: resources.enabled, - domainId: resources.domainId - }) - .from(resources) - .leftJoin(sites, eq(resources.siteId, sites.siteId)) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) +function queryResources(accessibleResourceIds: number[], orgId: string) { + return db + .select({ + resourceId: resources.resourceId, + name: resources.name, + ssl: resources.ssl, + fullDomain: resources.fullDomain, + passwordId: resourcePassword.passwordId, + sso: resources.sso, + pincodeId: resourcePincode.pincodeId, + whitelist: resources.emailWhitelistEnabled, + http: resources.http, + protocol: resources.protocol, + proxyPort: resources.proxyPort, + enabled: resources.enabled, + domainId: resources.domainId + }) + .from(resources) + .leftJoin( + resourcePassword, + eq(resourcePassword.resourceId, resources.resourceId) + ) + .leftJoin( + resourcePincode, + eq(resourcePincode.resourceId, resources.resourceId) + ) + .where( + and( + inArray(resources.resourceId, accessibleResourceIds), + eq(resources.orgId, orgId) ) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .where( - and( - inArray(resources.resourceId, accessibleResourceIds), - eq(resources.siteId, siteId) - ) - ); - } else if (orgId) { - return db - .select({ - resourceId: resources.resourceId, - name: resources.name, - ssl: resources.ssl, - fullDomain: resources.fullDomain, - siteName: sites.name, - siteId: sites.niceId, - passwordId: resourcePassword.passwordId, - sso: resources.sso, - pincodeId: resourcePincode.pincodeId, - whitelist: resources.emailWhitelistEnabled, - http: resources.http, - protocol: resources.protocol, - proxyPort: resources.proxyPort, - enabled: resources.enabled, - domainId: resources.domainId - }) - .from(resources) - .leftJoin(sites, eq(resources.siteId, sites.siteId)) - .leftJoin( - resourcePassword, - eq(resourcePassword.resourceId, resources.resourceId) - ) - .leftJoin( - resourcePincode, - eq(resourcePincode.resourceId, resources.resourceId) - ) - .where( - and( - inArray(resources.resourceId, accessibleResourceIds), - eq(resources.orgId, orgId) - ) - ); - } + ); } export type ListResourcesResponse = { @@ -131,20 +78,6 @@ export type ListResourcesResponse = { pagination: { total: number; limit: number; offset: number }; }; -registry.registerPath({ - method: "get", - path: "/site/{siteId}/resources", - description: "List resources for a site.", - tags: [OpenAPITags.Site, OpenAPITags.Resource], - request: { - params: z.object({ - siteId: z.number() - }), - query: listResourcesSchema - }, - responses: {} -}); - registry.registerPath({ method: "get", path: "/org/{orgId}/resources", @@ -185,9 +118,11 @@ export async function listResources( ) ); } - const { siteId } = parsedParams.data; - const orgId = parsedParams.data.orgId || req.userOrg?.orgId || req.apiKeyOrg?.orgId; + const orgId = + parsedParams.data.orgId || + req.userOrg?.orgId || + req.apiKeyOrg?.orgId; if (!orgId) { return next( @@ -207,24 +142,27 @@ export async function listResources( let accessibleResources; if (req.user) { accessibleResources = await db - .select({ - resourceId: sql`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})` - }) - .from(userResources) - .fullJoin( - roleResources, - eq(userResources.resourceId, roleResources.resourceId) - ) - .where( - or( - eq(userResources.userId, req.user!.userId), - eq(roleResources.roleId, req.userOrgRoleId!) + .select({ + resourceId: sql`COALESCE(${userResources.resourceId}, ${roleResources.resourceId})` + }) + .from(userResources) + .fullJoin( + roleResources, + eq(userResources.resourceId, roleResources.resourceId) ) - ); + .where( + or( + eq(userResources.userId, req.user!.userId), + eq(roleResources.roleId, req.userOrgRoleId!) + ) + ); } else { - accessibleResources = await db.select({ - resourceId: resources.resourceId - }).from(resources).where(eq(resources.orgId, orgId)); + accessibleResources = await db + .select({ + resourceId: resources.resourceId + }) + .from(resources) + .where(eq(resources.orgId, orgId)); } const accessibleResourceIds = accessibleResources.map( @@ -236,7 +174,7 @@ export async function listResources( .from(resources) .where(inArray(resources.resourceId, accessibleResourceIds)); - const baseQuery = queryResources(accessibleResourceIds, siteId, orgId); + const baseQuery = queryResources(accessibleResourceIds, orgId); const resourcesList = await baseQuery!.limit(limit).offset(offset); const totalCountResult = await countQuery; diff --git a/server/routers/resource/transferResource.ts b/server/routers/resource/transferResource.ts deleted file mode 100644 index a99405df..00000000 --- a/server/routers/resource/transferResource.ts +++ /dev/null @@ -1,214 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { z } from "zod"; -import { db } from "@server/db"; -import { newts, resources, sites, targets } from "@server/db"; -import { eq } from "drizzle-orm"; -import response from "@server/lib/response"; -import HttpCode from "@server/types/HttpCode"; -import createHttpError from "http-errors"; -import logger from "@server/logger"; -import { fromError } from "zod-validation-error"; -import { addPeer } from "../gerbil/peers"; -import { addTargets, removeTargets } from "../newt/targets"; -import { getAllowedIps } from "../target/helpers"; -import { OpenAPITags, registry } from "@server/openApi"; - -const transferResourceParamsSchema = z - .object({ - resourceId: z - .string() - .transform(Number) - .pipe(z.number().int().positive()) - }) - .strict(); - -const transferResourceBodySchema = z - .object({ - siteId: z.number().int().positive() - }) - .strict(); - -registry.registerPath({ - method: "post", - path: "/resource/{resourceId}/transfer", - description: - "Transfer a resource to a different site. This will also transfer the targets associated with the resource.", - tags: [OpenAPITags.Resource], - request: { - params: transferResourceParamsSchema, - body: { - content: { - "application/json": { - schema: transferResourceBodySchema - } - } - } - }, - responses: {} -}); - -export async function transferResource( - req: Request, - res: Response, - next: NextFunction -): Promise { - try { - const parsedParams = transferResourceParamsSchema.safeParse(req.params); - if (!parsedParams.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedParams.error).toString() - ) - ); - } - - const parsedBody = transferResourceBodySchema.safeParse(req.body); - if (!parsedBody.success) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - fromError(parsedBody.error).toString() - ) - ); - } - - const { resourceId } = parsedParams.data; - const { siteId } = parsedBody.data; - - const [oldResource] = await db - .select() - .from(resources) - .where(eq(resources.resourceId, resourceId)) - .limit(1); - - if (!oldResource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) - ); - } - - if (oldResource.siteId === siteId) { - return next( - createHttpError( - HttpCode.BAD_REQUEST, - `Resource is already assigned to this site` - ) - ); - } - - const [newSite] = await db - .select() - .from(sites) - .where(eq(sites.siteId, siteId)) - .limit(1); - - if (!newSite) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Site with ID ${siteId} not found` - ) - ); - } - - const [oldSite] = await db - .select() - .from(sites) - .where(eq(sites.siteId, oldResource.siteId)) - .limit(1); - - if (!oldSite) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Site with ID ${oldResource.siteId} not found` - ) - ); - } - - const [updatedResource] = await db - .update(resources) - .set({ siteId }) - .where(eq(resources.resourceId, resourceId)) - .returning(); - - if (!updatedResource) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Resource with ID ${resourceId} not found` - ) - ); - } - - const resourceTargets = await db - .select() - .from(targets) - .where(eq(targets.resourceId, resourceId)); - - if (resourceTargets.length > 0) { - ////// REMOVE THE TARGETS FROM THE OLD SITE ////// - if (oldSite.pubKey) { - if (oldSite.type == "wireguard") { - await addPeer(oldSite.exitNodeId!, { - publicKey: oldSite.pubKey, - allowedIps: await getAllowedIps(oldSite.siteId) - }); - } else if (oldSite.type == "newt") { - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, oldSite.siteId)) - .limit(1); - - removeTargets( - newt.newtId, - resourceTargets, - updatedResource.protocol, - updatedResource.proxyPort - ); - } - } - - ////// ADD THE TARGETS TO THE NEW SITE ////// - if (newSite.pubKey) { - if (newSite.type == "wireguard") { - await addPeer(newSite.exitNodeId!, { - publicKey: newSite.pubKey, - allowedIps: await getAllowedIps(newSite.siteId) - }); - } else if (newSite.type == "newt") { - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, newSite.siteId)) - .limit(1); - - addTargets( - newt.newtId, - resourceTargets, - updatedResource.protocol, - updatedResource.proxyPort - ); - } - } - } - - return response(res, { - data: updatedResource, - success: true, - error: false, - message: "Resource transferred successfully", - status: HttpCode.OK - }); - } catch (error) { - logger.error(error); - return next( - createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") - ); - } -} diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 5cf68c2b..30acc0c1 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -20,7 +20,6 @@ import { tlsNameSchema } from "@server/lib/schemas"; import { subdomainSchema } from "@server/lib/schemas"; import { registry } from "@server/openApi"; import { OpenAPITags } from "@server/openApi"; -import { build } from "@server/build"; const updateResourceParamsSchema = z .object({ @@ -44,7 +43,8 @@ const updateHttpResourceBodySchema = z enabled: z.boolean().optional(), stickySession: z.boolean().optional(), tlsServerName: z.string().nullable().optional(), - setHostHeader: z.string().nullable().optional() + setHostHeader: z.string().nullable().optional(), + skipToIdpId: z.number().int().positive().nullable().optional() }) .strict() .refine((data) => Object.keys(data).length > 0, { @@ -91,8 +91,8 @@ const updateRawResourceBodySchema = z name: z.string().min(1).max(255).optional(), proxyPort: z.number().int().min(1).max(65535).optional(), stickySession: z.boolean().optional(), - enabled: z.boolean().optional(), - enableProxy: z.boolean().optional() + enabled: z.boolean().optional() + // enableProxy: z.boolean().optional() // always true now }) .strict() .refine((data) => Object.keys(data).length > 0, { diff --git a/server/routers/role/addRoleSite.ts b/server/routers/role/addRoleSite.ts index 58da9879..d268eed4 100644 --- a/server/routers/role/addRoleSite.ts +++ b/server/routers/role/addRoleSite.ts @@ -60,18 +60,18 @@ export async function addRoleSite( }) .returning(); - const siteResources = await db - .select() - .from(resources) - .where(eq(resources.siteId, siteId)); - - for (const resource of siteResources) { - await trx.insert(roleResources).values({ - roleId, - resourceId: resource.resourceId - }); - } - + // const siteResources = await db + // .select() + // .from(resources) + // .where(eq(resources.siteId, siteId)); + // + // for (const resource of siteResources) { + // await trx.insert(roleResources).values({ + // roleId, + // resourceId: resource.resourceId + // }); + // } + // return response(res, { data: newRoleSite[0], success: true, diff --git a/server/routers/role/index.ts b/server/routers/role/index.ts index 0194c1f0..bbbe4ba8 100644 --- a/server/routers/role/index.ts +++ b/server/routers/role/index.ts @@ -1,6 +1,5 @@ export * from "./addRoleAction"; export * from "../resource/setResourceRoles"; -export * from "./addRoleSite"; export * from "./createRole"; export * from "./deleteRole"; export * from "./getRole"; @@ -11,5 +10,4 @@ export * from "./listRoles"; export * from "./listRoleSites"; export * from "./removeRoleAction"; export * from "./removeRoleResource"; -export * from "./removeRoleSite"; -export * from "./updateRole"; \ No newline at end of file +export * from "./updateRole"; diff --git a/server/routers/role/removeRoleSite.ts b/server/routers/role/removeRoleSite.ts index c88e4711..2670272d 100644 --- a/server/routers/role/removeRoleSite.ts +++ b/server/routers/role/removeRoleSite.ts @@ -71,22 +71,22 @@ export async function removeRoleSite( ); } - const siteResources = await db - .select() - .from(resources) - .where(eq(resources.siteId, siteId)); - - for (const resource of siteResources) { - await trx - .delete(roleResources) - .where( - and( - eq(roleResources.roleId, roleId), - eq(roleResources.resourceId, resource.resourceId) - ) - ) - .returning(); - } + // const siteResources = await db + // .select() + // .from(resources) + // .where(eq(resources.siteId, siteId)); + // + // for (const resource of siteResources) { + // await trx + // .delete(roleResources) + // .where( + // and( + // eq(roleResources.roleId, roleId), + // eq(roleResources.resourceId, resource.resourceId) + // ) + // ) + // .returning(); + // } }); return response(res, { diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts new file mode 100644 index 00000000..4d80c7a0 --- /dev/null +++ b/server/routers/siteResource/createSiteResource.ts @@ -0,0 +1,171 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, newts } from "@server/db"; +import { siteResources, sites, orgs, SiteResource } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; +import { addTargets } from "../client/targets"; + +const createSiteResourceParamsSchema = z + .object({ + siteId: z.string().transform(Number).pipe(z.number().int().positive()), + orgId: z.string() + }) + .strict(); + +const createSiteResourceSchema = z + .object({ + name: z.string().min(1).max(255), + protocol: z.enum(["tcp", "udp"]), + proxyPort: z.number().int().positive(), + destinationPort: z.number().int().positive(), + destinationIp: z.string().ip(), + enabled: z.boolean().default(true) + }) + .strict(); + +export type CreateSiteResourceBody = z.infer; +export type CreateSiteResourceResponse = SiteResource; + +registry.registerPath({ + method: "put", + path: "/org/{orgId}/site/{siteId}/resource", + description: "Create a new site resource.", + tags: [OpenAPITags.Client, OpenAPITags.Org], + request: { + params: createSiteResourceParamsSchema, + body: { + content: { + "application/json": { + schema: createSiteResourceSchema + } + } + } + }, + responses: {} +}); + +export async function createSiteResource( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = createSiteResourceParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = createSiteResourceSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { siteId, orgId } = parsedParams.data; + const { + name, + protocol, + proxyPort, + destinationPort, + destinationIp, + enabled + } = parsedBody.data; + + // Verify the site exists and belongs to the org + const [site] = await db + .select() + .from(sites) + .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) + .limit(1); + + if (!site) { + return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); + } + + // check if resource with same protocol and proxy port already exists + const [existingResource] = await db + .select() + .from(siteResources) + .where( + and( + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId), + eq(siteResources.protocol, protocol), + eq(siteResources.proxyPort, proxyPort) + ) + ) + .limit(1); + if (existingResource && existingResource.siteResourceId) { + return next( + createHttpError( + HttpCode.CONFLICT, + "A resource with the same protocol and proxy port already exists" + ) + ); + } + + // Create the site resource + const [newSiteResource] = await db + .insert(siteResources) + .values({ + siteId, + orgId, + name, + protocol, + proxyPort, + destinationPort, + destinationIp, + enabled + }) + .returning(); + + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); + + if (!newt) { + return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); + } + + await addTargets(newt.newtId, destinationIp, destinationPort, protocol); + + logger.info( + `Created site resource ${newSiteResource.siteResourceId} for site ${siteId}` + ); + + return response(res, { + data: newSiteResource, + success: true, + error: false, + message: "Site resource created successfully", + status: HttpCode.CREATED + }); + } catch (error) { + logger.error("Error creating site resource:", error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to create site resource" + ) + ); + } +} diff --git a/server/routers/siteResource/deleteSiteResource.ts b/server/routers/siteResource/deleteSiteResource.ts new file mode 100644 index 00000000..df29faf5 --- /dev/null +++ b/server/routers/siteResource/deleteSiteResource.ts @@ -0,0 +1,124 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, newts, sites } from "@server/db"; +import { siteResources } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; +import { removeTargets } from "../client/targets"; + +const deleteSiteResourceParamsSchema = z + .object({ + siteResourceId: z.string().transform(Number).pipe(z.number().int().positive()), + siteId: z.string().transform(Number).pipe(z.number().int().positive()), + orgId: z.string() + }) + .strict(); + +export type DeleteSiteResourceResponse = { + message: string; +}; + +registry.registerPath({ + method: "delete", + path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", + description: "Delete a site resource.", + tags: [OpenAPITags.Client, OpenAPITags.Org], + request: { + params: deleteSiteResourceParamsSchema + }, + responses: {} +}); + +export async function deleteSiteResource( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = deleteSiteResourceParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { siteResourceId, siteId, orgId } = parsedParams.data; + + const [site] = await db + .select() + .from(sites) + .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) + .limit(1); + + if (!site) { + return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); + } + + // Check if site resource exists + const [existingSiteResource] = await db + .select() + .from(siteResources) + .where(and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + )) + .limit(1); + + if (!existingSiteResource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Site resource not found" + ) + ); + } + + // Delete the site resource + await db + .delete(siteResources) + .where(and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + )); + + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); + + if (!newt) { + return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); + } + + await removeTargets( + newt.newtId, + existingSiteResource.destinationIp, + existingSiteResource.destinationPort, + existingSiteResource.protocol + ); + + logger.info(`Deleted site resource ${siteResourceId} for site ${siteId}`); + + return response(res, { + data: { message: "Site resource deleted successfully" }, + success: true, + error: false, + message: "Site resource deleted successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error("Error deleting site resource:", error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to delete site resource")); + } +} diff --git a/server/routers/siteResource/getSiteResource.ts b/server/routers/siteResource/getSiteResource.ts new file mode 100644 index 00000000..914706cd --- /dev/null +++ b/server/routers/siteResource/getSiteResource.ts @@ -0,0 +1,83 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { siteResources, SiteResource } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; + +const getSiteResourceParamsSchema = z + .object({ + siteResourceId: z.string().transform(Number).pipe(z.number().int().positive()), + siteId: z.string().transform(Number).pipe(z.number().int().positive()), + orgId: z.string() + }) + .strict(); + +export type GetSiteResourceResponse = SiteResource; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", + description: "Get a specific site resource.", + tags: [OpenAPITags.Client, OpenAPITags.Org], + request: { + params: getSiteResourceParamsSchema + }, + responses: {} +}); + +export async function getSiteResource( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = getSiteResourceParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { siteResourceId, siteId, orgId } = parsedParams.data; + + // Get the site resource + const [siteResource] = await db + .select() + .from(siteResources) + .where(and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + )) + .limit(1); + + if (!siteResource) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Site resource not found" + ) + ); + } + + return response(res, { + data: siteResource, + success: true, + error: false, + message: "Site resource retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error("Error getting site resource:", error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to get site resource")); + } +} diff --git a/server/routers/siteResource/index.ts b/server/routers/siteResource/index.ts new file mode 100644 index 00000000..2c3e2526 --- /dev/null +++ b/server/routers/siteResource/index.ts @@ -0,0 +1,6 @@ +export * from "./createSiteResource"; +export * from "./deleteSiteResource"; +export * from "./getSiteResource"; +export * from "./updateSiteResource"; +export * from "./listSiteResources"; +export * from "./listAllSiteResourcesByOrg"; diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts new file mode 100644 index 00000000..948fc2c2 --- /dev/null +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -0,0 +1,111 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { siteResources, sites, SiteResource } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; + +const listAllSiteResourcesByOrgParamsSchema = z + .object({ + orgId: z.string() + }) + .strict(); + +const listAllSiteResourcesByOrgQuerySchema = z.object({ + limit: z + .string() + .optional() + .default("1000") + .transform(Number) + .pipe(z.number().int().positive()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) +}); + +export type ListAllSiteResourcesByOrgResponse = { + siteResources: (SiteResource & { siteName: string, siteNiceId: string })[]; +}; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/site-resources", + description: "List all site resources for an organization.", + tags: [OpenAPITags.Client, OpenAPITags.Org], + request: { + params: listAllSiteResourcesByOrgParamsSchema, + query: listAllSiteResourcesByOrgQuerySchema + }, + responses: {} +}); + +export async function listAllSiteResourcesByOrg( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = listAllSiteResourcesByOrgParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedQuery = listAllSiteResourcesByOrgQuerySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + const { limit, offset } = parsedQuery.data; + + // Get all site resources for the org with site names + const siteResourcesList = await db + .select({ + siteResourceId: siteResources.siteResourceId, + siteId: siteResources.siteId, + orgId: siteResources.orgId, + name: siteResources.name, + protocol: siteResources.protocol, + proxyPort: siteResources.proxyPort, + destinationPort: siteResources.destinationPort, + destinationIp: siteResources.destinationIp, + enabled: siteResources.enabled, + siteName: sites.name, + siteNiceId: sites.niceId + }) + .from(siteResources) + .innerJoin(sites, eq(siteResources.siteId, sites.siteId)) + .where(eq(siteResources.orgId, orgId)) + .limit(limit) + .offset(offset); + + return response(res, { + data: { siteResources: siteResourcesList }, + success: true, + error: false, + message: "Site resources retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error("Error listing all site resources by org:", error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to list site resources")); + } +} diff --git a/server/routers/siteResource/listSiteResources.ts b/server/routers/siteResource/listSiteResources.ts new file mode 100644 index 00000000..7fdb7a85 --- /dev/null +++ b/server/routers/siteResource/listSiteResources.ts @@ -0,0 +1,118 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { siteResources, sites, SiteResource } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; + +const listSiteResourcesParamsSchema = z + .object({ + siteId: z.string().transform(Number).pipe(z.number().int().positive()), + orgId: z.string() + }) + .strict(); + +const listSiteResourcesQuerySchema = z.object({ + limit: z + .string() + .optional() + .default("100") + .transform(Number) + .pipe(z.number().int().positive()), + offset: z + .string() + .optional() + .default("0") + .transform(Number) + .pipe(z.number().int().nonnegative()) +}); + +export type ListSiteResourcesResponse = { + siteResources: SiteResource[]; +}; + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/site/{siteId}/resources", + description: "List site resources for a site.", + tags: [OpenAPITags.Client, OpenAPITags.Org], + request: { + params: listSiteResourcesParamsSchema, + query: listSiteResourcesQuerySchema + }, + responses: {} +}); + +export async function listSiteResources( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = listSiteResourcesParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedQuery = listSiteResourcesQuerySchema.safeParse(req.query); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + + const { siteId, orgId } = parsedParams.data; + const { limit, offset } = parsedQuery.data; + + // Verify the site exists and belongs to the org + const site = await db + .select() + .from(sites) + .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) + .limit(1); + + if (site.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "Site not found" + ) + ); + } + + // Get site resources + const siteResourcesList = await db + .select() + .from(siteResources) + .where(and( + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + )) + .limit(limit) + .offset(offset); + + return response(res, { + data: { siteResources: siteResourcesList }, + success: true, + error: false, + message: "Site resources retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error("Error listing site resources:", error); + return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "Failed to list site resources")); + } +} diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts new file mode 100644 index 00000000..bd717463 --- /dev/null +++ b/server/routers/siteResource/updateSiteResource.ts @@ -0,0 +1,196 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db, newts, sites } from "@server/db"; +import { siteResources, SiteResource } from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import { eq, and } from "drizzle-orm"; +import { fromError } from "zod-validation-error"; +import logger from "@server/logger"; +import { OpenAPITags, registry } from "@server/openApi"; +import { addTargets } from "../client/targets"; + +const updateSiteResourceParamsSchema = z + .object({ + siteResourceId: z + .string() + .transform(Number) + .pipe(z.number().int().positive()), + siteId: z.string().transform(Number).pipe(z.number().int().positive()), + orgId: z.string() + }) + .strict(); + +const updateSiteResourceSchema = z + .object({ + name: z.string().min(1).max(255).optional(), + protocol: z.enum(["tcp", "udp"]).optional(), + proxyPort: z.number().int().positive().optional(), + destinationPort: z.number().int().positive().optional(), + destinationIp: z.string().ip().optional(), + enabled: z.boolean().optional() + }) + .strict(); + +export type UpdateSiteResourceBody = z.infer; +export type UpdateSiteResourceResponse = SiteResource; + +registry.registerPath({ + method: "post", + path: "/org/{orgId}/site/{siteId}/resource/{siteResourceId}", + description: "Update a site resource.", + tags: [OpenAPITags.Client, OpenAPITags.Org], + request: { + params: updateSiteResourceParamsSchema, + body: { + content: { + "application/json": { + schema: updateSiteResourceSchema + } + } + } + }, + responses: {} +}); + +export async function updateSiteResource( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = updateSiteResourceParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = updateSiteResourceSchema.safeParse(req.body); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { siteResourceId, siteId, orgId } = parsedParams.data; + const updateData = parsedBody.data; + + const [site] = await db + .select() + .from(sites) + .where(and(eq(sites.siteId, siteId), eq(sites.orgId, orgId))) + .limit(1); + + if (!site) { + return next(createHttpError(HttpCode.NOT_FOUND, "Site not found")); + } + + // Check if site resource exists + const [existingSiteResource] = await db + .select() + .from(siteResources) + .where( + and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + ) + ) + .limit(1); + + if (!existingSiteResource) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Site resource not found") + ); + } + + const protocol = updateData.protocol || existingSiteResource.protocol; + const proxyPort = + updateData.proxyPort || existingSiteResource.proxyPort; + + // check if resource with same protocol and proxy port already exists + const [existingResource] = await db + .select() + .from(siteResources) + .where( + and( + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId), + eq(siteResources.protocol, protocol), + eq(siteResources.proxyPort, proxyPort) + ) + ) + .limit(1); + if ( + existingResource && + existingResource.siteResourceId !== siteResourceId + ) { + return next( + createHttpError( + HttpCode.CONFLICT, + "A resource with the same protocol and proxy port already exists" + ) + ); + } + + // Update the site resource + const [updatedSiteResource] = await db + .update(siteResources) + .set(updateData) + .where( + and( + eq(siteResources.siteResourceId, siteResourceId), + eq(siteResources.siteId, siteId), + eq(siteResources.orgId, orgId) + ) + ) + .returning(); + + const [newt] = await db + .select() + .from(newts) + .where(eq(newts.siteId, site.siteId)) + .limit(1); + + if (!newt) { + return next(createHttpError(HttpCode.NOT_FOUND, "Newt not found")); + } + + await addTargets( + newt.newtId, + updatedSiteResource.destinationIp, + updatedSiteResource.destinationPort, + updatedSiteResource.protocol + ); + + logger.info( + `Updated site resource ${siteResourceId} for site ${siteId}` + ); + + return response(res, { + data: updatedSiteResource, + success: true, + error: false, + message: "Site resource updated successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error("Error updating site resource:", error); + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Failed to update site resource" + ) + ); + } +} diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index ffea1571..7a3acd55 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -26,6 +26,7 @@ const createTargetParamsSchema = z const createTargetSchema = z .object({ + siteId: z.number().int().positive(), ip: z.string().refine(isTargetValid), method: z.string().optional().nullable(), port: z.number().int().min(1).max(65535), @@ -98,17 +99,41 @@ export async function createTarget( ); } + const siteId = targetData.siteId; + const [site] = await db .select() .from(sites) - .where(eq(sites.siteId, resource.siteId!)) + .where(eq(sites.siteId, siteId)) .limit(1); if (!site) { return next( createHttpError( HttpCode.NOT_FOUND, - `Site with ID ${resource.siteId} not found` + `Site with ID ${siteId} not found` + ) + ); + } + + const existingTargets = await db + .select() + .from(targets) + .where(eq(targets.resourceId, resourceId)); + + const existingTarget = existingTargets.find( + (target) => + target.ip === targetData.ip && + target.port === targetData.port && + target.method === targetData.method && + target.siteId === targetData.siteId + ); + + if (existingTarget) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Target with IP ${targetData.ip}, port ${targetData.port}, method ${targetData.method} already exists for resource ID ${resourceId}` ) ); } @@ -173,7 +198,12 @@ export async function createTarget( .where(eq(newts.siteId, site.siteId)) .limit(1); - addTargets(newt.newtId, newTarget, resource.protocol, resource.proxyPort); + await addTargets( + newt.newtId, + newTarget, + resource.protocol, + resource.proxyPort + ); } } } diff --git a/server/routers/target/deleteTarget.ts b/server/routers/target/deleteTarget.ts index 6eadeccd..596691e4 100644 --- a/server/routers/target/deleteTarget.ts +++ b/server/routers/target/deleteTarget.ts @@ -76,38 +76,38 @@ export async function deleteTarget( ); } - const [site] = await db - .select() - .from(sites) - .where(eq(sites.siteId, resource.siteId!)) - .limit(1); - - if (!site) { - return next( - createHttpError( - HttpCode.NOT_FOUND, - `Site with ID ${resource.siteId} not found` - ) - ); - } - - if (site.pubKey) { - if (site.type == "wireguard") { - await addPeer(site.exitNodeId!, { - publicKey: site.pubKey, - allowedIps: await getAllowedIps(site.siteId) - }); - } else if (site.type == "newt") { - // get the newt on the site by querying the newt table for siteId - const [newt] = await db - .select() - .from(newts) - .where(eq(newts.siteId, site.siteId)) - .limit(1); - - removeTargets(newt.newtId, [deletedTarget], resource.protocol, resource.proxyPort); - } - } + // const [site] = await db + // .select() + // .from(sites) + // .where(eq(sites.siteId, resource.siteId!)) + // .limit(1); + // + // if (!site) { + // return next( + // createHttpError( + // HttpCode.NOT_FOUND, + // `Site with ID ${resource.siteId} not found` + // ) + // ); + // } + // + // if (site.pubKey) { + // if (site.type == "wireguard") { + // await addPeer(site.exitNodeId!, { + // publicKey: site.pubKey, + // allowedIps: await getAllowedIps(site.siteId) + // }); + // } else if (site.type == "newt") { + // // get the newt on the site by querying the newt table for siteId + // const [newt] = await db + // .select() + // .from(newts) + // .where(eq(newts.siteId, site.siteId)) + // .limit(1); + // + // removeTargets(newt.newtId, [deletedTarget], resource.protocol, resource.proxyPort); + // } + // } return response(res, { data: null, diff --git a/server/routers/target/getTarget.ts b/server/routers/target/getTarget.ts index 071ec8a6..b0691087 100644 --- a/server/routers/target/getTarget.ts +++ b/server/routers/target/getTarget.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db } from "@server/db"; +import { db, Target } from "@server/db"; import { targets } from "@server/db"; import { eq } from "drizzle-orm"; import response from "@server/lib/response"; @@ -16,6 +16,8 @@ const getTargetSchema = z }) .strict(); +type GetTargetResponse = Target; + registry.registerPath({ method: "get", path: "/target/{targetId}", @@ -60,7 +62,7 @@ export async function getTarget( ); } - return response(res, { + return response(res, { data: target[0], success: true, error: false, diff --git a/server/routers/target/helpers.ts b/server/routers/target/helpers.ts index e5aa2ba9..4935d28a 100644 --- a/server/routers/target/helpers.ts +++ b/server/routers/target/helpers.ts @@ -8,29 +8,21 @@ export async function pickPort(siteId: number): Promise<{ internalPort: number; targetIps: string[]; }> { - const resourcesRes = await db - .select() - .from(resources) - .where(eq(resources.siteId, siteId)); - - // TODO: is this all inefficient? // Fetch targets for all resources of this site const targetIps: string[] = []; const targetInternalPorts: number[] = []; - await Promise.all( - resourcesRes.map(async (resource) => { - const targetsRes = await db - .select() - .from(targets) - .where(eq(targets.resourceId, resource.resourceId)); - targetsRes.forEach((target) => { - targetIps.push(`${target.ip}/32`); - if (target.internalPort) { - targetInternalPorts.push(target.internalPort); - } - }); - }) - ); + + const targetsRes = await db + .select() + .from(targets) + .where(eq(targets.siteId, siteId)); + + targetsRes.forEach((target) => { + targetIps.push(`${target.ip}/32`); + if (target.internalPort) { + targetInternalPorts.push(target.internalPort); + } + }); let internalPort!: number; // pick a port random port from 40000 to 65535 that is not in use @@ -43,28 +35,20 @@ export async function pickPort(siteId: number): Promise<{ break; } } + currentBannedPorts.push(internalPort); return { internalPort, targetIps }; } export async function getAllowedIps(siteId: number) { - // TODO: is this all inefficient? - - const resourcesRes = await db - .select() - .from(resources) - .where(eq(resources.siteId, siteId)); - // Fetch targets for all resources of this site - const targetIps = await Promise.all( - resourcesRes.map(async (resource) => { - const targetsRes = await db - .select() - .from(targets) - .where(eq(targets.resourceId, resource.resourceId)); - return targetsRes.map((target) => `${target.ip}/32`); - }) - ); + const targetsRes = await db + .select() + .from(targets) + .where(eq(targets.siteId, siteId)); + + const targetIps = targetsRes.map((target) => `${target.ip}/32`); + return targetIps.flat(); } diff --git a/server/routers/target/index.ts b/server/routers/target/index.ts index b128edcd..dc1323f7 100644 --- a/server/routers/target/index.ts +++ b/server/routers/target/index.ts @@ -2,4 +2,4 @@ export * from "./getTarget"; export * from "./createTarget"; export * from "./deleteTarget"; export * from "./updateTarget"; -export * from "./listTargets"; \ No newline at end of file +export * from "./listTargets"; diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index 44f27d48..eab8f1c8 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -1,4 +1,4 @@ -import { db } from "@server/db"; +import { db, sites } from "@server/db"; import { targets } from "@server/db"; import HttpCode from "@server/types/HttpCode"; import response from "@server/lib/response"; @@ -42,11 +42,12 @@ function queryTargets(resourceId: number) { method: targets.method, port: targets.port, enabled: targets.enabled, - resourceId: targets.resourceId - // resourceName: resources.name, + resourceId: targets.resourceId, + siteId: targets.siteId, + siteType: sites.type }) .from(targets) - // .leftJoin(resources, eq(targets.resourceId, resources.resourceId)) + .leftJoin(sites, eq(sites.siteId, targets.siteId)) .where(eq(targets.resourceId, resourceId)); return baseQuery; diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index 0b7c4692..67d9a8df 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -22,6 +22,7 @@ const updateTargetParamsSchema = z const updateTargetBodySchema = z .object({ + siteId: z.number().int().positive(), ip: z.string().refine(isTargetValid), method: z.string().min(1).max(10).optional().nullable(), port: z.number().int().min(1).max(65535).optional(), @@ -77,6 +78,7 @@ export async function updateTarget( } const { targetId } = parsedParams.data; + const { siteId } = parsedBody.data; const [target] = await db .select() @@ -111,14 +113,42 @@ export async function updateTarget( const [site] = await db .select() .from(sites) - .where(eq(sites.siteId, resource.siteId!)) + .where(eq(sites.siteId, siteId)) .limit(1); if (!site) { return next( createHttpError( HttpCode.NOT_FOUND, - `Site with ID ${resource.siteId} not found` + `Site with ID ${siteId} not found` + ) + ); + } + + const targetData = { + ...target, + ...parsedBody.data + }; + + const existingTargets = await db + .select() + .from(targets) + .where(eq(targets.resourceId, target.resourceId)); + + const foundTarget = existingTargets.find( + (target) => + target.targetId !== targetId && // Exclude the current target being updated + target.ip === targetData.ip && + target.port === targetData.port && + target.method === targetData.method && + target.siteId === targetData.siteId + ); + + if (foundTarget) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + `Target with IP ${targetData.ip}, port ${targetData.port}, and method ${targetData.method} already exists on the same site.` ) ); } @@ -157,7 +187,12 @@ export async function updateTarget( .where(eq(newts.siteId, site.siteId)) .limit(1); - addTargets(newt.newtId, [updatedTarget], resource.protocol, resource.proxyPort); + await addTargets( + newt.newtId, + [updatedTarget], + resource.protocol, + resource.proxyPort + ); } } return response(res, { diff --git a/server/routers/traefik/getTraefikConfig.ts b/server/routers/traefik/getTraefikConfig.ts index 882a296a..e3b62176 100644 --- a/server/routers/traefik/getTraefikConfig.ts +++ b/server/routers/traefik/getTraefikConfig.ts @@ -1,11 +1,21 @@ import { Request, Response } from "express"; import { db, exitNodes } from "@server/db"; -import { and, eq, inArray, or, isNull } from "drizzle-orm"; +import { and, eq, inArray, or, isNull, ne } from "drizzle-orm"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import config from "@server/lib/config"; import { orgs, resources, sites, Target, targets } from "@server/db"; +// Extended Target interface that includes site information +interface TargetWithSite extends Target { + site: { + siteId: number; + type: string; + subnet: string | null; + exitNodeId: number | null; + }; +} + let currentExitNodeId: number; export async function traefikConfigProvider( @@ -44,8 +54,9 @@ export async function traefikConfigProvider( } } - // Get the site(s) on this exit node - const resourcesWithRelations = await tx + // Get resources with their targets and sites in a single optimized query + // Start from sites on this exit node, then join to targets and resources + const resourcesWithTargetsAndSites = await tx .select({ // Resource fields resourceId: resources.resourceId, @@ -56,67 +67,82 @@ export async function traefikConfigProvider( protocol: resources.protocol, subdomain: resources.subdomain, domainId: resources.domainId, - // Site fields - site: { - siteId: sites.siteId, - type: sites.type, - subnet: sites.subnet, - exitNodeId: sites.exitNodeId - }, enabled: resources.enabled, stickySession: resources.stickySession, tlsServerName: resources.tlsServerName, setHostHeader: resources.setHostHeader, - enableProxy: resources.enableProxy + enableProxy: resources.enableProxy, + // Target fields + targetId: targets.targetId, + targetEnabled: targets.enabled, + ip: targets.ip, + method: targets.method, + port: targets.port, + internalPort: targets.internalPort, + // Site fields + siteId: sites.siteId, + siteType: sites.type, + subnet: sites.subnet, + exitNodeId: sites.exitNodeId }) - .from(resources) - .innerJoin(sites, eq(sites.siteId, resources.siteId)) + .from(sites) + .innerJoin(targets, eq(targets.siteId, sites.siteId)) + .innerJoin(resources, eq(resources.resourceId, targets.resourceId)) .where( - or( - eq(sites.exitNodeId, currentExitNodeId), - isNull(sites.exitNodeId) + and( + eq(targets.enabled, true), + eq(resources.enabled, true), + or( + eq(sites.exitNodeId, currentExitNodeId), + isNull(sites.exitNodeId) + ) ) ); - // Get all resource IDs from the first query - const resourceIds = resourcesWithRelations.map((r) => r.resourceId); + // Group by resource and include targets with their unique site data + const resourcesMap = new Map(); - // Second query to get all enabled targets for these resources - const allTargets = - resourceIds.length > 0 - ? await tx - .select({ - resourceId: targets.resourceId, - targetId: targets.targetId, - ip: targets.ip, - method: targets.method, - port: targets.port, - internalPort: targets.internalPort, - enabled: targets.enabled - }) - .from(targets) - .where( - and( - inArray(targets.resourceId, resourceIds), - eq(targets.enabled, true) - ) - ) - : []; + resourcesWithTargetsAndSites.forEach((row) => { + const resourceId = row.resourceId; - // Create a map for fast target lookup by resourceId - const targetsMap = allTargets.reduce((map, target) => { - if (!map.has(target.resourceId)) { - map.set(target.resourceId, []); + if (!resourcesMap.has(resourceId)) { + resourcesMap.set(resourceId, { + resourceId: row.resourceId, + fullDomain: row.fullDomain, + ssl: row.ssl, + http: row.http, + proxyPort: row.proxyPort, + protocol: row.protocol, + subdomain: row.subdomain, + domainId: row.domainId, + enabled: row.enabled, + stickySession: row.stickySession, + tlsServerName: row.tlsServerName, + setHostHeader: row.setHostHeader, + enableProxy: row.enableProxy, + targets: [] + }); } - map.get(target.resourceId).push(target); - return map; - }, new Map()); - // Combine the data - return resourcesWithRelations.map((resource) => ({ - ...resource, - targets: targetsMap.get(resource.resourceId) || [] - })); + // Add target with its associated site data + resourcesMap.get(resourceId).targets.push({ + resourceId: row.resourceId, + targetId: row.targetId, + ip: row.ip, + method: row.method, + port: row.port, + internalPort: row.internalPort, + enabled: row.targetEnabled, + site: { + siteId: row.siteId, + type: row.siteType, + subnet: row.subnet, + exitNodeId: row.exitNodeId + } + }); + }); + + return Array.from(resourcesMap.values()); }); if (!allResources.length) { @@ -167,8 +193,7 @@ export async function traefikConfigProvider( }; for (const resource of allResources) { - const targets = resource.targets as Target[]; - const site = resource.site; + const targets = resource.targets as TargetWithSite[]; const routerName = `${resource.resourceId}-router`; const serviceName = `${resource.resourceId}-service`; @@ -272,13 +297,13 @@ export async function traefikConfigProvider( config_output.http.services![serviceName] = { loadBalancer: { servers: targets - .filter((target: Target) => { + .filter((target: TargetWithSite) => { if (!target.enabled) { return false; } if ( - site.type === "local" || - site.type === "wireguard" + target.site.type === "local" || + target.site.type === "wireguard" ) { if ( !target.ip || @@ -287,27 +312,27 @@ export async function traefikConfigProvider( ) { return false; } - } else if (site.type === "newt") { + } else if (target.site.type === "newt") { if ( !target.internalPort || !target.method || - !site.subnet + !target.site.subnet ) { return false; } } return true; }) - .map((target: Target) => { + .map((target: TargetWithSite) => { if ( - site.type === "local" || - site.type === "wireguard" + target.site.type === "local" || + target.site.type === "wireguard" ) { return { url: `${target.method}://${target.ip}:${target.port}` }; - } else if (site.type === "newt") { - const ip = site.subnet!.split("/")[0]; + } else if (target.site.type === "newt") { + const ip = target.site.subnet!.split("/")[0]; return { url: `${target.method}://${ip}:${target.internalPort}` }; @@ -393,34 +418,34 @@ export async function traefikConfigProvider( config_output[protocol].services[serviceName] = { loadBalancer: { servers: targets - .filter((target: Target) => { + .filter((target: TargetWithSite) => { if (!target.enabled) { return false; } if ( - site.type === "local" || - site.type === "wireguard" + target.site.type === "local" || + target.site.type === "wireguard" ) { if (!target.ip || !target.port) { return false; } - } else if (site.type === "newt") { - if (!target.internalPort || !site.subnet) { + } else if (target.site.type === "newt") { + if (!target.internalPort || !target.site.subnet) { return false; } } return true; }) - .map((target: Target) => { + .map((target: TargetWithSite) => { if ( - site.type === "local" || - site.type === "wireguard" + target.site.type === "local" || + target.site.type === "wireguard" ) { return { address: `${target.ip}:${target.port}` }; - } else if (site.type === "newt") { - const ip = site.subnet!.split("/")[0]; + } else if (target.site.type === "newt") { + const ip = target.site.subnet!.split("/")[0]; return { address: `${ip}:${target.internalPort}` }; diff --git a/server/routers/user/addUserSite.ts b/server/routers/user/addUserSite.ts index c55d5463..f094e20e 100644 --- a/server/routers/user/addUserSite.ts +++ b/server/routers/user/addUserSite.ts @@ -43,17 +43,17 @@ export async function addUserSite( }) .returning(); - const siteResources = await trx - .select() - .from(resources) - .where(eq(resources.siteId, siteId)); - - for (const resource of siteResources) { - await trx.insert(userResources).values({ - userId, - resourceId: resource.resourceId - }); - } + // const siteResources = await trx + // .select() + // .from(resources) + // .where(eq(resources.siteId, siteId)); + // + // for (const resource of siteResources) { + // await trx.insert(userResources).values({ + // userId, + // resourceId: resource.resourceId + // }); + // } return response(res, { data: newUserSite[0], diff --git a/server/routers/user/removeUserSite.ts b/server/routers/user/removeUserSite.ts index 200999fd..7dbb4a15 100644 --- a/server/routers/user/removeUserSite.ts +++ b/server/routers/user/removeUserSite.ts @@ -71,22 +71,22 @@ export async function removeUserSite( ); } - const siteResources = await trx - .select() - .from(resources) - .where(eq(resources.siteId, siteId)); - - for (const resource of siteResources) { - await trx - .delete(userResources) - .where( - and( - eq(userResources.userId, userId), - eq(userResources.resourceId, resource.resourceId) - ) - ) - .returning(); - } + // const siteResources = await trx + // .select() + // .from(resources) + // .where(eq(resources.siteId, siteId)); + // + // for (const resource of siteResources) { + // await trx + // .delete(userResources) + // .where( + // and( + // eq(userResources.userId, userId), + // eq(userResources.resourceId, resource.resourceId) + // ) + // ) + // .returning(); + // } }); return response(res, { diff --git a/server/routers/ws/messageHandlers.ts b/server/routers/ws/messageHandlers.ts index d85cc277..05faece3 100644 --- a/server/routers/ws/messageHandlers.ts +++ b/server/routers/ws/messageHandlers.ts @@ -23,7 +23,7 @@ export const messageHandlers: Record = { "olm/ping": handleOlmPingMessage, "newt/socket/status": handleDockerStatusMessage, "newt/socket/containers": handleDockerContainersMessage, - "newt/ping/request": handleNewtPingRequestMessage, + "newt/ping/request": handleNewtPingRequestMessage }; startOfflineChecker(); // this is to handle the offline check for olms diff --git a/server/setup/scriptsPg/1.9.0.ts b/server/setup/scriptsPg/1.9.0.ts index 22259cae..a12f5617 100644 --- a/server/setup/scriptsPg/1.9.0.ts +++ b/server/setup/scriptsPg/1.9.0.ts @@ -22,4 +22,4 @@ export default async function migration() { console.log("Unable to add setupTokens table:", e); throw e; } -} \ No newline at end of file +} diff --git a/server/setup/scriptsSqlite/1.9.0.ts b/server/setup/scriptsSqlite/1.9.0.ts index a4a20dda..83dbf9d0 100644 --- a/server/setup/scriptsSqlite/1.9.0.ts +++ b/server/setup/scriptsSqlite/1.9.0.ts @@ -32,4 +32,4 @@ export default async function migration() { console.log("Unable to add setupTokens table:", e); throw e; } -} \ No newline at end of file +} diff --git a/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx b/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx deleted file mode 100644 index a675213a..00000000 --- a/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx +++ /dev/null @@ -1,36 +0,0 @@ -"use client"; - -import { ColumnDef } from "@tanstack/react-table"; -import { DataTable } from "@app/components/ui/data-table"; -import { useTranslations } from 'next-intl'; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - createResource?: () => void; -} - -export function ResourcesDataTable({ - columns, - data, - createResource -}: DataTableProps) { - - const t = useTranslations(); - - return ( - - ); -} diff --git a/src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx b/src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx deleted file mode 100644 index 50f6fd0b..00000000 --- a/src/app/[orgId]/settings/resources/ResourcesSplashCard.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -import React, { useState, useEffect } from "react"; -import { Server, Lock, Key, Users, X, ArrowRight } from "lucide-react"; // Replace with actual imports -import { Card, CardContent } from "@app/components/ui/card"; -import { Button } from "@app/components/ui/button"; -import { useTranslations } from "next-intl"; - -export const ResourcesSplashCard = () => { - const [isDismissed, setIsDismissed] = useState(false); - - const key = "resources-splash-dismissed"; - - useEffect(() => { - const dismissed = localStorage.getItem(key); - if (dismissed === "true") { - setIsDismissed(true); - } - }, []); - - const handleDismiss = () => { - setIsDismissed(true); - localStorage.setItem(key, "true"); - }; - - const t = useTranslations(); - - if (isDismissed) { - return null; - } - - return ( - - - -

-

- - {t('resources')} -

-

- {t('resourcesDescription')} -

-
    -
  • - - {t('resourcesWireGuardConnect')} -
  • -
  • - - {t('resourcesMultipleAuthenticationMethods')} -
  • -
  • - - {t('resourcesUsersRolesAccess')} -
  • -
-
- - - ); -}; - -export default ResourcesSplashCard; diff --git a/src/app/[orgId]/settings/resources/ResourcesTable.tsx b/src/app/[orgId]/settings/resources/ResourcesTable.tsx index e64fb4e3..a4209bee 100644 --- a/src/app/[orgId]/settings/resources/ResourcesTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesTable.tsx @@ -1,7 +1,16 @@ "use client"; -import { ColumnDef } from "@tanstack/react-table"; -import { ResourcesDataTable } from "./ResourcesDataTable"; +import { + ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, + getPaginationRowModel, + SortingState, + getSortedRowModel, + ColumnFiltersState, + getFilteredRowModel +} from "@tanstack/react-table"; import { DropdownMenu, DropdownMenuContent, @@ -10,18 +19,16 @@ import { } from "@app/components/ui/dropdown-menu"; import { Button } from "@app/components/ui/button"; import { - Copy, ArrowRight, ArrowUpDown, MoreHorizontal, - Check, ArrowUpRight, ShieldOff, ShieldCheck } from "lucide-react"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog"; import { formatAxiosError } from "@app/lib/api"; import { toast } from "@app/hooks/useToast"; @@ -31,17 +38,37 @@ import CopyToClipboard from "@app/components/CopyToClipboard"; import { Switch } from "@app/components/ui/switch"; import { AxiosResponse } from "axios"; import { UpdateResourceResponse } from "@server/routers/resource"; +import { ListSitesResponse } from "@server/routers/site"; import { useTranslations } from "next-intl"; import { InfoPopup } from "@app/components/ui/info-popup"; -import { Badge } from "@app/components/ui/badge"; +import { Input } from "@app/components/ui/input"; +import { DataTablePagination } from "@app/components/DataTablePagination"; +import { Plus, Search } from "lucide-react"; +import { Card, CardContent, CardHeader } from "@app/components/ui/card"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@app/components/ui/table"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger +} from "@app/components/ui/tabs"; +import { useSearchParams } from "next/navigation"; +import EditInternalResourceDialog from "@app/components/EditInternalResourceDialog"; +import CreateInternalResourceDialog from "@app/components/CreateInternalResourceDialog"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; export type ResourceRow = { id: number; name: string; orgId: string; domain: string; - site: string; - siteId: string; authState: string; http: boolean; protocol: string; @@ -50,20 +77,147 @@ export type ResourceRow = { domainId?: string; }; -type ResourcesTableProps = { - resources: ResourceRow[]; +export type InternalResourceRow = { + id: number; + name: string; orgId: string; + siteName: string; + protocol: string; + proxyPort: number | null; + siteId: number; + siteNiceId: string; + destinationIp: string; + destinationPort: number; }; -export default function SitesTable({ resources, orgId }: ResourcesTableProps) { +type Site = ListSitesResponse["sites"][0]; + +type ResourcesTableProps = { + resources: ResourceRow[]; + internalResources: InternalResourceRow[]; + orgId: string; + defaultView?: "proxy" | "internal"; +}; + +export default function SitesTable({ + resources, + internalResources, + orgId, + defaultView = "proxy" +}: ResourcesTableProps) { const router = useRouter(); + const searchParams = useSearchParams(); const t = useTranslations(); - const api = createApiClient(useEnvContext()); + const { env } = useEnvContext(); + + const api = createApiClient({ env }); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [selectedResource, setSelectedResource] = useState(); + const [selectedInternalResource, setSelectedInternalResource] = + useState(); + const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); + const [editingResource, setEditingResource] = + useState(); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [sites, setSites] = useState([]); + + const [proxySorting, setProxySorting] = useState([]); + const [proxyColumnFilters, setProxyColumnFilters] = + useState([]); + const [proxyGlobalFilter, setProxyGlobalFilter] = useState([]); + + const [internalSorting, setInternalSorting] = useState([]); + const [internalColumnFilters, setInternalColumnFilters] = + useState([]); + const [internalGlobalFilter, setInternalGlobalFilter] = useState([]); + + const currentView = searchParams.get("view") || defaultView; + + useEffect(() => { + const fetchSites = async () => { + try { + const res = await api.get>( + `/org/${orgId}/sites` + ); + setSites(res.data.data.sites); + } catch (error) { + console.error("Failed to fetch sites:", error); + } + }; + + if (orgId) { + fetchSites(); + } + }, [orgId]); + + const handleTabChange = (value: string) => { + const params = new URLSearchParams(searchParams); + if (value === "internal") { + params.set("view", "internal"); + } else { + params.delete("view"); + } + + const newUrl = `${window.location.pathname}${params.toString() ? "?" + params.toString() : ""}`; + router.replace(newUrl, { scroll: false }); + }; + + const getSearchInput = () => { + if (currentView === "internal") { + return ( +
+ + internalTable.setGlobalFilter( + String(e.target.value) + ) + } + className="w-full pl-8" + /> + +
+ ); + } + return ( +
+ + proxyTable.setGlobalFilter(String(e.target.value)) + } + className="w-full pl-8" + /> + +
+ ); + }; + + const getActionButton = () => { + if (currentView === "internal") { + return ( + + ); + } + return ( + + ); + }; const deleteResource = (resourceId: number) => { api.delete(`/resource/${resourceId}`) @@ -81,6 +235,26 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { }); }; + const deleteInternalResource = async ( + resourceId: number, + siteId: number + ) => { + try { + await api.delete( + `/org/${orgId}/site/${siteId}/resource/${resourceId}` + ); + router.refresh(); + setIsDeleteModalOpen(false); + } catch (e) { + console.error(t("resourceErrorDelete"), e); + toast({ + variant: "destructive", + title: t("resourceErrorDelte"), + description: formatAxiosError(e, t("v")) + }); + } + }; + async function toggleResourceEnabled(val: boolean, resourceId: number) { const res = await api .post>( @@ -101,7 +275,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { }); } - const columns: ColumnDef[] = [ + const proxyColumns: ColumnDef[] = [ { accessorKey: "name", header: ({ column }) => { @@ -118,35 +292,6 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { ); } }, - { - accessorKey: "site", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const resourceRow = row.original; - return ( - - - - ); - } - }, { accessorKey: "protocol", header: t("protocol"), @@ -225,10 +370,12 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { toggleResourceEnabled(val, row.original.id) } @@ -289,6 +436,163 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { } ]; + const internalColumns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => { + return ( + + ); + } + }, + { + accessorKey: "siteName", + header: t("siteName"), + cell: ({ row }) => { + const resourceRow = row.original; + return ( + + + + ); + } + }, + { + accessorKey: "protocol", + header: t("protocol"), + cell: ({ row }) => { + const resourceRow = row.original; + return {resourceRow.protocol.toUpperCase()}; + } + }, + { + accessorKey: "proxyPort", + header: t("proxyPort"), + cell: ({ row }) => { + const resourceRow = row.original; + return ( + + ); + } + }, + { + accessorKey: "destination", + header: t("resourcesTableDestination"), + cell: ({ row }) => { + const resourceRow = row.original; + const destination = `${resourceRow.destinationIp}:${resourceRow.destinationPort}`; + return ; + } + }, + + { + id: "actions", + cell: ({ row }) => { + const resourceRow = row.original; + return ( +
+ + + + + + { + setSelectedInternalResource( + resourceRow + ); + setIsDeleteModalOpen(true); + }} + > + + {t("delete")} + + + + + +
+ ); + } + } + ]; + + const proxyTable = useReactTable({ + data: resources, + columns: proxyColumns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setProxySorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setProxyColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + onGlobalFilterChange: setProxyGlobalFilter, + initialState: { + pagination: { + pageSize: 20, + pageIndex: 0 + } + }, + state: { + sorting: proxySorting, + columnFilters: proxyColumnFilters, + globalFilter: proxyGlobalFilter + } + }); + + const internalTable = useReactTable({ + data: internalResources, + columns: internalColumns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setInternalSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setInternalColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + onGlobalFilterChange: setInternalGlobalFilter, + initialState: { + pagination: { + pageSize: 20, + pageIndex: 0 + } + }, + state: { + sorting: internalSorting, + columnFilters: internalColumnFilters, + globalFilter: internalGlobalFilter + } + }); + return ( <> {selectedResource && ( @@ -320,11 +624,271 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) { /> )} - { - router.push(`/${orgId}/settings/resources/create`); + {selectedInternalResource && ( + { + setIsDeleteModalOpen(val); + setSelectedInternalResource(null); + }} + dialog={ +
+

+ {t("resourceQuestionRemove", { + selectedResource: + selectedInternalResource?.name || + selectedInternalResource?.id + })} +

+ +

{t("resourceMessageRemove")}

+ +

{t("resourceMessageConfirm")}

+
+ } + buttonText={t("resourceDeleteConfirm")} + onConfirm={async () => + deleteInternalResource( + selectedInternalResource!.id, + selectedInternalResource!.siteId + ) + } + string={selectedInternalResource.name} + title={t("resourceDelete")} + /> + )} + +
+ + + +
+ {getSearchInput()} + + {env.flags.enableClients && ( + + + {t("resourcesTableProxyResources")} + + + {t("resourcesTableClientResources")} + + + )} +
+
+ {getActionButton()} +
+
+ + + + + {proxyTable + .getHeaderGroups() + .map((headerGroup) => ( + + {headerGroup.headers.map( + (header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header + .column + .columnDef + .header, + header.getContext() + )} + + ) + )} + + ))} + + + {proxyTable.getRowModel().rows + ?.length ? ( + proxyTable + .getRowModel() + .rows.map((row) => ( + + {row + .getVisibleCells() + .map((cell) => ( + + {flexRender( + cell + .column + .columnDef + .cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {t( + "resourcesTableNoProxyResourcesFound" + )} + + + )} + +
+
+ +
+
+ +
+ + + {t( + "resourcesTableTheseResourcesForUseWith" + )}{" "} + + {t("resourcesTableClients")} + + {" "} + {t( + "resourcesTableAndOnlyAccessibleInternally" + )} + + +
+ + + {internalTable + .getHeaderGroups() + .map((headerGroup) => ( + + {headerGroup.headers.map( + (header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header + .column + .columnDef + .header, + header.getContext() + )} + + ) + )} + + ))} + + + {internalTable.getRowModel().rows + ?.length ? ( + internalTable + .getRowModel() + .rows.map((row) => ( + + {row + .getVisibleCells() + .map((cell) => ( + + {flexRender( + cell + .column + .columnDef + .cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {t( + "resourcesTableNoInternalResourcesFound" + )} + + + )} + +
+
+ +
+
+
+
+
+
+ + {editingResource && ( + { + router.refresh(); + setEditingResource(null); + }} + /> + )} + + { + router.refresh(); }} /> diff --git a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx index 68331ff9..af7d96fc 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/ResourceInfoBox.tsx @@ -10,35 +10,22 @@ import { InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { useDockerSocket } from "@app/hooks/useDockerSocket"; import { useTranslations } from "next-intl"; -import { AxiosResponse } from "axios"; -import { useEffect, useState } from "react"; -import { Button } from "@/components/ui/button"; -import { RotateCw } from "lucide-react"; -import { createApiClient } from "@app/lib/api"; import { build } from "@server/build"; type ResourceInfoBoxType = {}; export default function ResourceInfoBox({}: ResourceInfoBoxType) { - const { resource, authInfo, site } = useResourceContext(); - const api = createApiClient(useEnvContext()); + const { resource, authInfo } = useResourceContext(); - const { isEnabled, isAvailable } = useDockerSocket(site!); const t = useTranslations(); const fullUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`; return ( - - - {t("resourceInfo")} - - - + + {resource.http ? ( <> @@ -71,12 +58,6 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { /> - - {t("site")} - - {resource.siteName} - - {/* {isEnabled && ( Socket @@ -117,7 +98,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { /> - {build == "oss" && ( + {/* {build == "oss" && ( {t("externalProxyEnabled")} @@ -130,7 +111,7 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) { - )} + )} */} )} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx index c8f6255c..9bb9919a 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/authentication/page.tsx @@ -49,6 +49,15 @@ import { UserType } from "@server/types/UserTypes"; import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { InfoIcon } from "lucide-react"; import { useTranslations } from "next-intl"; +import { CheckboxWithLabel } from "@app/components/ui/checkbox"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { Separator } from "@app/components/ui/separator"; const UsersRolesFormSchema = z.object({ roles: z.array( @@ -110,6 +119,14 @@ export default function ResourceAuthenticationPage() { resource.emailWhitelistEnabled ); + const [autoLoginEnabled, setAutoLoginEnabled] = useState( + resource.skipToIdpId !== null && resource.skipToIdpId !== undefined + ); + const [selectedIdpId, setSelectedIdpId] = useState( + resource.skipToIdpId || null + ); + const [allIdps, setAllIdps] = useState<{ id: number; text: string }[]>([]); + const [loadingSaveUsersRoles, setLoadingSaveUsersRoles] = useState(false); const [loadingSaveWhitelist, setLoadingSaveWhitelist] = useState(false); @@ -139,7 +156,8 @@ export default function ResourceAuthenticationPage() { resourceRolesResponse, usersResponse, resourceUsersResponse, - whitelist + whitelist, + idpsResponse ] = await Promise.all([ api.get>( `/org/${org?.org.orgId}/roles` @@ -155,7 +173,12 @@ export default function ResourceAuthenticationPage() { ), api.get>( `/resource/${resource.resourceId}/whitelist` - ) + ), + api.get< + AxiosResponse<{ + idps: { idpId: number; name: string }[]; + }> + >("/idp") ]); setAllRoles( @@ -200,6 +223,21 @@ export default function ResourceAuthenticationPage() { })) ); + setAllIdps( + idpsResponse.data.data.idps.map((idp) => ({ + id: idp.idpId, + text: idp.name + })) + ); + + if ( + autoLoginEnabled && + !selectedIdpId && + idpsResponse.data.data.idps.length > 0 + ) { + setSelectedIdpId(idpsResponse.data.data.idps[0].idpId); + } + setPageLoading(false); } catch (e) { console.error(e); @@ -260,6 +298,16 @@ export default function ResourceAuthenticationPage() { try { setLoadingSaveUsersRoles(true); + // Validate that an IDP is selected if auto login is enabled + if (autoLoginEnabled && !selectedIdpId) { + toast({ + variant: "destructive", + title: t("error"), + description: t("selectIdpRequired") + }); + return; + } + const jobs = [ api.post(`/resource/${resource.resourceId}/roles`, { roleIds: data.roles.map((i) => parseInt(i.id)) @@ -268,14 +316,16 @@ export default function ResourceAuthenticationPage() { userIds: data.users.map((i) => i.id) }), api.post(`/resource/${resource.resourceId}`, { - sso: ssoEnabled + sso: ssoEnabled, + skipToIdpId: autoLoginEnabled ? selectedIdpId : null }) ]; await Promise.all(jobs); updateResource({ - sso: ssoEnabled + sso: ssoEnabled, + skipToIdpId: autoLoginEnabled ? selectedIdpId : null }); updateAuthInfo({ @@ -542,6 +592,89 @@ export default function ResourceAuthenticationPage() { /> )} + + {ssoEnabled && allIdps.length > 0 && ( +
+
+ { + setAutoLoginEnabled( + checked as boolean + ); + if ( + checked && + allIdps.length > 0 + ) { + setSelectedIdpId( + allIdps[0].id + ); + } else { + setSelectedIdpId( + null + ); + } + }} + /> +

+ {t( + "autoLoginExternalIdpDescription" + )} +

+
+ + {autoLoginEnabled && ( +
+ + +
+ )} +
+ )} diff --git a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx index b4e14d64..8c5ee667 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/general/page.tsx @@ -14,19 +14,6 @@ import { FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem -} from "@/components/ui/command"; -import { cn } from "@app/lib/cn"; -import { - Popover, - PopoverContent, - PopoverTrigger -} from "@/components/ui/popover"; import { useResourceContext } from "@app/hooks/useResourceContext"; import { ListSitesResponse } from "@server/routers/site"; import { useEffect, useState } from "react"; @@ -45,25 +32,11 @@ import { SettingsSectionFooter } from "@app/components/Settings"; import { useOrgContext } from "@app/hooks/useOrgContext"; -import CustomDomainInput from "../CustomDomainInput"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { subdomainSchema, tlsNameSchema } from "@server/lib/schemas"; -import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"; -import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group"; import { Label } from "@app/components/ui/label"; import { ListDomainsResponse } from "@server/routers/domain"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from "@app/components/ui/select"; -import { - UpdateResourceResponse, - updateResourceRule -} from "@server/routers/resource"; +import { UpdateResourceResponse } from "@server/routers/resource"; import { SwitchInput } from "@app/components/SwitchInput"; import { useTranslations } from "next-intl"; import { Checkbox } from "@app/components/ui/checkbox"; @@ -81,12 +54,6 @@ import DomainPicker from "@app/components/DomainPicker"; import { Globe } from "lucide-react"; import { build } from "@server/build"; -const TransferFormSchema = z.object({ - siteId: z.number() -}); - -type TransferFormValues = z.infer; - export default function GeneralForm() { const [formKey, setFormKey] = useState(0); const params = useParams(); @@ -127,7 +94,7 @@ export default function GeneralForm() { name: z.string().min(1).max(255), domainId: z.string().optional(), proxyPort: z.number().int().min(1).max(65535).optional(), - enableProxy: z.boolean().optional() + // enableProxy: z.boolean().optional() }) .refine( (data) => { @@ -156,18 +123,11 @@ export default function GeneralForm() { subdomain: resource.subdomain ? resource.subdomain : undefined, domainId: resource.domainId || undefined, proxyPort: resource.proxyPort || undefined, - enableProxy: resource.enableProxy || false + // enableProxy: resource.enableProxy || false }, mode: "onChange" }); - const transferForm = useForm({ - resolver: zodResolver(TransferFormSchema), - defaultValues: { - siteId: resource.siteId ? Number(resource.siteId) : undefined - } - }); - useEffect(() => { const fetchSites = async () => { const res = await api.get>( @@ -221,9 +181,9 @@ export default function GeneralForm() { subdomain: data.subdomain, domainId: data.domainId, proxyPort: data.proxyPort, - ...(!resource.http && { - enableProxy: data.enableProxy - }) + // ...(!resource.http && { + // enableProxy: data.enableProxy + // }) } ) .catch((e) => { @@ -251,9 +211,9 @@ export default function GeneralForm() { subdomain: data.subdomain, fullDomain: resource.fullDomain, proxyPort: data.proxyPort, - ...(!resource.http && { - enableProxy: data.enableProxy - }), + // ...(!resource.http && { + // enableProxy: data.enableProxy + // }) }); router.refresh(); @@ -261,40 +221,6 @@ export default function GeneralForm() { setSaveLoading(false); } - async function onTransfer(data: TransferFormValues) { - setTransferLoading(true); - - const res = await api - .post(`resource/${resource?.resourceId}/transfer`, { - siteId: data.siteId - }) - .catch((e) => { - toast({ - variant: "destructive", - title: t("resourceErrorTransfer"), - description: formatAxiosError( - e, - t("resourceErrorTransferDescription") - ) - }); - }); - - if (res && res.status === 200) { - toast({ - title: t("resourceTransferred"), - description: t("resourceTransferredDescription") - }); - router.refresh(); - - updateResource({ - siteName: - sites.find((site) => site.siteId === data.siteId)?.name || - "" - }); - } - setTransferLoading(false); - } - return ( !loadingPage && ( <> @@ -410,7 +336,7 @@ export default function GeneralForm() { )} /> - {build == "oss" && ( + {/* {build == "oss" && ( )} /> - )} + )} */} )} {resource.http && (
- +
@@ -466,7 +394,9 @@ export default function GeneralForm() { ) } > - Edit Domain + {t( + "resourceEditDomain" + )}
@@ -490,140 +420,6 @@ export default function GeneralForm() { - - - - - {t("resourceTransfer")} - - - {t("resourceTransferDescription")} - - - - - -
- - ( - - - {t("siteDestination")} - - - - - - - - - - - - {t( - "sitesNotFound" - )} - - - {sites.map( - ( - site - ) => ( - { - transferForm.setValue( - "siteId", - site.siteId - ); - setOpen( - false - ); - }} - > - { - site.name - } - - - ) - )} - - - - - - - )} - /> - - -
-
- - - - -
>( `/resource/${params.resourceId}`, @@ -44,19 +43,6 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { redirect(`/${params.orgId}/settings/resources`); } - // Fetch site info - if (resource.siteId) { - try { - const res = await internal.get>( - `/site/${resource.siteId}`, - await authCookieHeader() - ); - site = res.data.data; - } catch { - redirect(`/${params.orgId}/settings/resources`); - } - } - try { const res = await internal.get< AxiosResponse @@ -119,7 +105,6 @@ export default async function ResourceLayout(props: ResourceLayoutProps) { diff --git a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx index 7ab02c7e..c6584219 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx @@ -3,7 +3,6 @@ import { useEffect, useState, use } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Select, SelectContent, @@ -34,12 +33,12 @@ import { getPaginationRowModel, getCoreRowModel, useReactTable, - flexRender + flexRender, + Row } from "@tanstack/react-table"; import { Table, TableBody, - TableCaption, TableCell, TableHead, TableHeader, @@ -51,7 +50,7 @@ import { ArrayElement } from "@server/types/ArrayElement"; import { formatAxiosError } from "@app/lib/api/formatAxiosError"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { createApiClient } from "@app/lib/api"; -import { GetSiteResponse } from "@server/routers/site"; +import { GetSiteResponse, ListSitesResponse } from "@server/routers/site"; import { SettingsContainer, SettingsSection, @@ -59,28 +58,48 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionFooter, - SettingsSectionForm, - SettingsSectionGrid + SettingsSectionForm } from "@app/components/Settings"; import { SwitchInput } from "@app/components/SwitchInput"; import { useRouter } from "next/navigation"; import { isTargetValid } from "@server/lib/validators"; import { tlsNameSchema } from "@server/lib/schemas"; -import { ChevronsUpDown } from "lucide-react"; import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger -} from "@app/components/ui/collapsible"; + CheckIcon, + ChevronsUpDown, + Settings, + Heart, + Check, + CircleCheck, + CircleX +} from "lucide-react"; import { ContainersSelector } from "@app/components/ContainersSelector"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; +import { DockerManager, DockerState } from "@app/lib/docker"; +import { Container } from "@server/routers/site"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { cn } from "@app/lib/cn"; +import { CaretSortIcon } from "@radix-ui/react-icons"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import { Badge } from "@app/components/ui/badge"; const addTargetSchema = z.object({ ip: z.string().refine(isTargetValid), method: z.string().nullable(), - port: z.coerce.number().int().positive() + port: z.coerce.number().int().positive(), + siteId: z.number().int().positive() }); const targetsSettingsSchema = z.object({ @@ -91,12 +110,13 @@ type LocalTarget = Omit< ArrayElement & { new?: boolean; updated?: boolean; + siteType: string | null; }, "protocol" >; export default function ReverseProxyTargets(props: { - params: Promise<{ resourceId: number }>; + params: Promise<{ resourceId: number; orgId: string }>; }) { const params = use(props.params); const t = useTranslations(); @@ -106,15 +126,48 @@ export default function ReverseProxyTargets(props: { const api = createApiClient(useEnvContext()); const [targets, setTargets] = useState([]); - const [site, setSite] = useState(); const [targetsToRemove, setTargetsToRemove] = useState([]); + const [sites, setSites] = useState([]); + const [dockerStates, setDockerStates] = useState>(new Map()); + + const initializeDockerForSite = async (siteId: number) => { + if (dockerStates.has(siteId)) { + return; // Already initialized + } + + const dockerManager = new DockerManager(api, siteId); + const dockerState = await dockerManager.initializeDocker(); + + setDockerStates(prev => new Map(prev.set(siteId, dockerState))); + }; + + const refreshContainersForSite = async (siteId: number) => { + const dockerManager = new DockerManager(api, siteId); + const containers = await dockerManager.fetchContainers(); + + setDockerStates(prev => { + const newMap = new Map(prev); + const existingState = newMap.get(siteId); + if (existingState) { + newMap.set(siteId, { ...existingState, containers }); + } + return newMap; + }); + }; + + const getDockerStateForSite = (siteId: number): DockerState => { + return dockerStates.get(siteId) || { + isEnabled: false, + isAvailable: false, + containers: [] + }; + }; const [httpsTlsLoading, setHttpsTlsLoading] = useState(false); const [targetsLoading, setTargetsLoading] = useState(false); const [proxySettingsLoading, setProxySettingsLoading] = useState(false); const [pageLoading, setPageLoading] = useState(true); - const [isAdvancedOpen, setIsAdvancedOpen] = useState(false); const router = useRouter(); const proxySettingsSchema = z.object({ @@ -167,6 +220,14 @@ export default function ReverseProxyTargets(props: { const watchedIp = addTargetForm.watch("ip"); const watchedPort = addTargetForm.watch("port"); + const watchedSiteId = addTargetForm.watch("siteId"); + + const handleContainerSelect = (hostname: string, port?: number) => { + addTargetForm.setValue("ip", hostname); + if (port) { + addTargetForm.setValue("port", port); + } + }; const tlsSettingsForm = useForm({ resolver: zodResolver(tlsSettingsSchema), @@ -216,28 +277,64 @@ export default function ReverseProxyTargets(props: { }; fetchTargets(); - const fetchSite = async () => { - try { - const res = await api.get>( - `/site/${resource.siteId}` - ); - - if (res.status === 200) { - setSite(res.data.data); - } - } catch (err) { - console.error(err); - toast({ - variant: "destructive", - title: t("siteErrorFetch"), - description: formatAxiosError( - err, - t("siteErrorFetchDescription") - ) + const fetchSites = async () => { + const res = await api + .get< + AxiosResponse + >(`/org/${params.orgId}/sites`) + .catch((e) => { + toast({ + variant: "destructive", + title: t("sitesErrorFetch"), + description: formatAxiosError( + e, + t("sitesErrorFetchDescription") + ) + }); }); + + if (res?.status === 200) { + setSites(res.data.data.sites); + + // Initialize Docker for newt sites + const newtSites = res.data.data.sites.filter(site => site.type === "newt"); + for (const site of newtSites) { + initializeDockerForSite(site.siteId); + } + + // If there's only one site, set it as the default in the form + if (res.data.data.sites.length) { + addTargetForm.setValue( + "siteId", + res.data.data.sites[0].siteId + ); + } } }; - fetchSite(); + fetchSites(); + + // const fetchSite = async () => { + // try { + // const res = await api.get>( + // `/site/${resource.siteId}` + // ); + // + // if (res.status === 200) { + // setSite(res.data.data); + // } + // } catch (err) { + // console.error(err); + // toast({ + // variant: "destructive", + // title: t("siteErrorFetch"), + // description: formatAxiosError( + // err, + // t("siteErrorFetchDescription") + // ) + // }); + // } + // }; + // fetchSite(); }, []); async function addTarget(data: z.infer) { @@ -246,7 +343,8 @@ export default function ReverseProxyTargets(props: { (target) => target.ip === data.ip && target.port === data.port && - target.method === data.method + target.method === data.method && + target.siteId === data.siteId ); if (isDuplicate) { @@ -258,34 +356,37 @@ export default function ReverseProxyTargets(props: { return; } - if (site && site.type == "wireguard" && site.subnet) { - // make sure that the target IP is within the site subnet - const targetIp = data.ip; - const subnet = site.subnet; - try { - if (!isIPInSubnet(targetIp, subnet)) { - toast({ - variant: "destructive", - title: t("targetWireGuardErrorInvalidIp"), - description: t( - "targetWireGuardErrorInvalidIpDescription" - ) - }); - return; - } - } catch (error) { - console.error(error); - toast({ - variant: "destructive", - title: t("targetWireGuardErrorInvalidIp"), - description: t("targetWireGuardErrorInvalidIpDescription") - }); - return; - } - } + // if (site && site.type == "wireguard" && site.subnet) { + // // make sure that the target IP is within the site subnet + // const targetIp = data.ip; + // const subnet = site.subnet; + // try { + // if (!isIPInSubnet(targetIp, subnet)) { + // toast({ + // variant: "destructive", + // title: t("targetWireGuardErrorInvalidIp"), + // description: t( + // "targetWireGuardErrorInvalidIpDescription" + // ) + // }); + // return; + // } + // } catch (error) { + // console.error(error); + // toast({ + // variant: "destructive", + // title: t("targetWireGuardErrorInvalidIp"), + // description: t("targetWireGuardErrorInvalidIpDescription") + // }); + // return; + // } + // } + + const site = sites.find((site) => site.siteId === data.siteId); const newTarget: LocalTarget = { ...data, + siteType: site?.type || null, enabled: true, targetId: new Date().getTime(), new: true, @@ -311,10 +412,16 @@ export default function ReverseProxyTargets(props: { }; async function updateTarget(targetId: number, data: Partial) { + const site = sites.find((site) => site.siteId === data.siteId); setTargets( targets.map((target) => target.targetId === targetId - ? { ...target, ...data, updated: true } + ? { + ...target, + ...data, + updated: true, + siteType: site?.type || null + } : target ) ); @@ -332,7 +439,8 @@ export default function ReverseProxyTargets(props: { ip: target.ip, port: target.port, method: target.method, - enabled: target.enabled + enabled: target.enabled, + siteId: target.siteId }; if (target.new) { @@ -403,6 +511,135 @@ export default function ReverseProxyTargets(props: { } const columns: ColumnDef[] = [ + { + accessorKey: "siteId", + header: t("site"), + cell: ({ row }) => { + const selectedSite = sites.find( + (site) => site.siteId === row.original.siteId + ); + + const handleContainerSelectForTarget = ( + hostname: string, + port?: number + ) => { + updateTarget(row.original.targetId, { + ...row.original, + ip: hostname + }); + if (port) { + updateTarget(row.original.targetId, { + ...row.original, + port: port + }); + } + }; + + return ( +
+ + + + + + + + + + {t("siteNotFound")} + + + {sites.map((site) => ( + { + updateTarget( + row.original + .targetId, + { + siteId: site.siteId + } + ); + }} + > + + {site.name} + + ))} + + + + + + {selectedSite && selectedSite.type === "newt" && (() => { + const dockerState = getDockerStateForSite(selectedSite.siteId); + return ( + refreshContainersForSite(selectedSite.siteId)} + /> + ); + })()} +
+ ); + } + }, + ...(resource.http + ? [ + { + accessorKey: "method", + header: t("method"), + cell: ({ row }: { row: Row }) => ( + + ) + } + ] + : []), { accessorKey: "ip", header: t("targetAddr"), @@ -412,6 +649,7 @@ export default function ReverseProxyTargets(props: { className="min-w-[150px]" onBlur={(e) => updateTarget(row.original.targetId, { + ...row.original, ip: e.target.value }) } @@ -428,6 +666,7 @@ export default function ReverseProxyTargets(props: { className="min-w-[100px]" onBlur={(e) => updateTarget(row.original.targetId, { + ...row.original, port: parseInt(e.target.value, 10) }) } @@ -451,7 +690,7 @@ export default function ReverseProxyTargets(props: { // // // ), - // }, + // }, { accessorKey: "enabled", header: t("enabled"), @@ -459,7 +698,10 @@ export default function ReverseProxyTargets(props: { - updateTarget(row.original.targetId, { enabled: val }) + updateTarget(row.original.targetId, { + ...row.original, + enabled: val + }) } /> ) @@ -489,33 +731,6 @@ export default function ReverseProxyTargets(props: { } ]; - if (resource.http) { - const methodCol: ColumnDef = { - accessorKey: "method", - header: t("method"), - cell: ({ row }) => ( - - ) - }; - - // add this to the first column - columns.unshift(methodCol); - } - const table = useReactTable({ data: targets, columns, @@ -545,221 +760,355 @@ export default function ReverseProxyTargets(props: { - -
+
+ - {targets.length >= 2 && ( +
( - - - { - field.onChange(val); - }} - /> - + + + {t("site")} + +
+ + + + + + + + + + + + {t( + "siteNotFound" + )} + + + {sites.map( + ( + site + ) => ( + { + addTargetForm.setValue( + "siteId", + site.siteId + ); + }} + > + + { + site.name + } + + ) + )} + + + + + + + {field.value && + (() => { + const selectedSite = + sites.find( + (site) => + site.siteId === + field.value + ); + return selectedSite && + selectedSite.type === + "newt" ? (() => { + const dockerState = getDockerStateForSite(selectedSite.siteId); + return ( + refreshContainersForSite(selectedSite.siteId)} + /> + ); + })() : null; + })()} +
+
)} /> - )} - - - -
- -
- {resource.http && ( + {resource.http && ( + ( + + + {t("method")} + + + + + + + )} + /> + )} + ( - + - {t("method")} + {t("targetAddr")} - + )} /> - )} - - ( - - - {t("targetAddr")} - - - - - {site && site.type == "newt" && ( - { - addTargetForm.setValue( - "ip", - hostname - ); - if (port) { - addTargetForm.setValue( - "port", - port - ); - } - }} - /> - )} - - - )} - /> - ( - - - {t("targetPort")} - - - - - - - )} - /> - -
-
- - - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef - .header, - header.getContext() - )} - - ))} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} - - ))} - - )) - ) : ( - - ( + + + {t("targetPort")} + + + + + + + )} + /> +
+ {t("targetSubmit")} + +
+ + +
+ + {targets.length > 0 ? ( + <> +
+ {t("targetsList")} +
+ +
+ + ( + + + { + field.onChange( + val + ); + }} + /> + + + )} + /> + + +
+
+ + + {table + .getHeaderGroups() + .map((headerGroup) => ( + + {headerGroup.headers.map( + (header) => ( + + {header.isPlaceholder + ? null + : flexRender( + header + .column + .columnDef + .header, + header.getContext() + )} + + ) + )} + + ))} + + + {table.getRowModel().rows?.length ? ( + table + .getRowModel() + .rows.map((row) => ( + + {row + .getVisibleCells() + .map((cell) => ( + + {flexRender( + cell + .column + .columnDef + .cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + + {t("targetNoOne")} + + + )} + + {/* */} + {/* {t('targetNoOneDescription')} */} + {/* */} +
+
+ + ) : ( +
+

+ {t("targetNoOne")} +

+
+ )}
@@ -885,7 +1234,7 @@ export default function ReverseProxyTargets(props: { proxySettingsLoading } > - {t("saveAllSettings")} + {t("saveSettings")} diff --git a/src/app/[orgId]/settings/resources/create/page.tsx b/src/app/[orgId]/settings/resources/create/page.tsx index a8d926fe..438b8917 100644 --- a/src/app/[orgId]/settings/resources/create/page.tsx +++ b/src/app/[orgId]/settings/resources/create/page.tsx @@ -42,9 +42,7 @@ import { SelectTrigger, SelectValue } from "@app/components/ui/select"; -import { subdomainSchema } from "@server/lib/schemas"; import { ListDomainsResponse } from "@server/routers/domain"; -import LoaderPlaceholder from "@app/components/PlaceHolderLoader"; import { Command, CommandEmpty, @@ -66,10 +64,33 @@ import Link from "next/link"; import { useTranslations } from "next-intl"; import DomainPicker from "@app/components/DomainPicker"; import { build } from "@server/build"; +import { ContainersSelector } from "@app/components/ContainersSelector"; +import { + ColumnDef, + getFilteredRowModel, + getSortedRowModel, + getPaginationRowModel, + getCoreRowModel, + useReactTable, + flexRender, + Row +} from "@tanstack/react-table"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow +} from "@app/components/ui/table"; +import { Switch } from "@app/components/ui/switch"; +import { ArrayElement } from "@server/types/ArrayElement"; +import { isTargetValid } from "@server/lib/validators"; +import { ListTargetsResponse } from "@server/routers/target"; +import { DockerManager, DockerState } from "@app/lib/docker"; const baseResourceFormSchema = z.object({ name: z.string().min(1).max(255), - siteId: z.number(), http: z.boolean() }); @@ -80,8 +101,15 @@ const httpResourceFormSchema = z.object({ const tcpUdpResourceFormSchema = z.object({ protocol: z.string(), - proxyPort: z.number().int().min(1).max(65535), - enableProxy: z.boolean().default(false) + proxyPort: z.number().int().min(1).max(65535) + // enableProxy: z.boolean().default(false) +}); + +const addTargetSchema = z.object({ + ip: z.string().refine(isTargetValid), + method: z.string().nullable(), + port: z.coerce.number().int().positive(), + siteId: z.number().int().positive() }); type BaseResourceFormValues = z.infer; @@ -97,6 +125,15 @@ interface ResourceTypeOption { disabled?: boolean; } +type LocalTarget = Omit< + ArrayElement & { + new?: boolean; + updated?: boolean; + siteType: string | null; + }, + "protocol" +>; + export default function Page() { const { env } = useEnvContext(); const api = createApiClient({ env }); @@ -113,6 +150,11 @@ export default function Page() { const [showSnippets, setShowSnippets] = useState(false); const [resourceId, setResourceId] = useState(null); + // Target management state + const [targets, setTargets] = useState([]); + const [targetsToRemove, setTargetsToRemove] = useState([]); + const [dockerStates, setDockerStates] = useState>(new Map()); + const resourceTypes: ReadonlyArray = [ { id: "http", @@ -147,11 +189,128 @@ export default function Page() { resolver: zodResolver(tcpUdpResourceFormSchema), defaultValues: { protocol: "tcp", - proxyPort: undefined, - enableProxy: false + proxyPort: undefined + // enableProxy: false } }); + const addTargetForm = useForm({ + resolver: zodResolver(addTargetSchema), + defaultValues: { + ip: "", + method: baseForm.watch("http") ? "http" : null, + port: "" as any as number + } as z.infer + }); + + const watchedIp = addTargetForm.watch("ip"); + const watchedPort = addTargetForm.watch("port"); + const watchedSiteId = addTargetForm.watch("siteId"); + + const handleContainerSelect = (hostname: string, port?: number) => { + addTargetForm.setValue("ip", hostname); + if (port) { + addTargetForm.setValue("port", port); + } + }; + + const initializeDockerForSite = async (siteId: number) => { + if (dockerStates.has(siteId)) { + return; // Already initialized + } + + const dockerManager = new DockerManager(api, siteId); + const dockerState = await dockerManager.initializeDocker(); + + setDockerStates(prev => new Map(prev.set(siteId, dockerState))); + }; + + const refreshContainersForSite = async (siteId: number) => { + const dockerManager = new DockerManager(api, siteId); + const containers = await dockerManager.fetchContainers(); + + setDockerStates(prev => { + const newMap = new Map(prev); + const existingState = newMap.get(siteId); + if (existingState) { + newMap.set(siteId, { ...existingState, containers }); + } + return newMap; + }); + }; + + const getDockerStateForSite = (siteId: number): DockerState => { + return dockerStates.get(siteId) || { + isEnabled: false, + isAvailable: false, + containers: [] + }; + }; + + async function addTarget(data: z.infer) { + // Check if target with same IP, port and method already exists + const isDuplicate = targets.some( + (target) => + target.ip === data.ip && + target.port === data.port && + target.method === data.method && + target.siteId === data.siteId + ); + + if (isDuplicate) { + toast({ + variant: "destructive", + title: t("targetErrorDuplicate"), + description: t("targetErrorDuplicateDescription") + }); + return; + } + + const site = sites.find((site) => site.siteId === data.siteId); + + const newTarget: LocalTarget = { + ...data, + siteType: site?.type || null, + enabled: true, + targetId: new Date().getTime(), + new: true, + resourceId: 0 // Will be set when resource is created + }; + + setTargets([...targets, newTarget]); + addTargetForm.reset({ + ip: "", + method: baseForm.watch("http") ? "http" : null, + port: "" as any as number + }); + } + + const removeTarget = (targetId: number) => { + setTargets([ + ...targets.filter((target) => target.targetId !== targetId) + ]); + + if (!targets.find((target) => target.targetId === targetId)?.new) { + setTargetsToRemove([...targetsToRemove, targetId]); + } + }; + + async function updateTarget(targetId: number, data: Partial) { + const site = sites.find((site) => site.siteId === data.siteId); + setTargets( + targets.map((target) => + target.targetId === targetId + ? { + ...target, + ...data, + updated: true, + siteType: site?.type || null + } + : target + ) + ); + } + async function onSubmit() { setCreateLoading(true); @@ -161,7 +320,6 @@ export default function Page() { try { const payload = { name: baseData.name, - siteId: baseData.siteId, http: baseData.http }; @@ -176,15 +334,15 @@ export default function Page() { const tcpUdpData = tcpUdpForm.getValues(); Object.assign(payload, { protocol: tcpUdpData.protocol, - proxyPort: tcpUdpData.proxyPort, - enableProxy: tcpUdpData.enableProxy + proxyPort: tcpUdpData.proxyPort + // enableProxy: tcpUdpData.enableProxy }); } const res = await api .put< AxiosResponse - >(`/org/${orgId}/site/${baseData.siteId}/resource/`, payload) + >(`/org/${orgId}/resource/`, payload) .catch((e) => { toast({ variant: "destructive", @@ -200,18 +358,45 @@ export default function Page() { const id = res.data.data.resourceId; setResourceId(id); + // Create targets if any exist + if (targets.length > 0) { + try { + for (const target of targets) { + const data = { + ip: target.ip, + port: target.port, + method: target.method, + enabled: target.enabled, + siteId: target.siteId + }; + + await api.put(`/resource/${id}/target`, data); + } + } catch (targetError) { + console.error("Error creating targets:", targetError); + toast({ + variant: "destructive", + title: t("targetErrorCreate"), + description: formatAxiosError( + targetError, + t("targetErrorCreateDescription") + ) + }); + } + } + if (isHttp) { router.push(`/${orgId}/settings/resources/${id}`); } else { const tcpUdpData = tcpUdpForm.getValues(); // Only show config snippets if enableProxy is explicitly true - if (tcpUdpData.enableProxy === true) { - setShowSnippets(true); - router.refresh(); - } else { - // If enableProxy is false or undefined, go directly to resource page - router.push(`/${orgId}/settings/resources/${id}`); - } + // if (tcpUdpData.enableProxy === true) { + setShowSnippets(true); + router.refresh(); + // } else { + // // If enableProxy is false or undefined, go directly to resource page + // router.push(`/${orgId}/settings/resources/${id}`); + // } } } } catch (e) { @@ -249,8 +434,16 @@ export default function Page() { if (res?.status === 200) { setSites(res.data.data.sites); - if (res.data.data.sites.length > 0) { - baseForm.setValue( + // Initialize Docker for newt sites + for (const site of res.data.data.sites) { + if (site.type === "newt") { + initializeDockerForSite(site.siteId); + } + } + + // If there's only one site, set it as the default in the form + if (res.data.data.sites.length) { + addTargetForm.setValue( "siteId", res.data.data.sites[0].siteId ); @@ -292,6 +485,216 @@ export default function Page() { load(); }, []); + const columns: ColumnDef[] = [ + { + accessorKey: "siteId", + header: t("site"), + cell: ({ row }) => { + const selectedSite = sites.find( + (site) => site.siteId === row.original.siteId + ); + + const handleContainerSelectForTarget = ( + hostname: string, + port?: number + ) => { + updateTarget(row.original.targetId, { + ...row.original, + ip: hostname + }); + if (port) { + updateTarget(row.original.targetId, { + ...row.original, + port: port + }); + } + }; + + return ( +
+ + + + + + + + + + {t("siteNotFound")} + + + {sites.map((site) => ( + { + updateTarget( + row.original + .targetId, + { + siteId: site.siteId + } + ); + }} + > + + {site.name} + + ))} + + + + + + {selectedSite && selectedSite.type === "newt" && (() => { + const dockerState = getDockerStateForSite(selectedSite.siteId); + return ( + refreshContainersForSite(selectedSite.siteId)} + /> + ); + })()} +
+ ); + } + }, + ...(baseForm.watch("http") + ? [ + { + accessorKey: "method", + header: t("method"), + cell: ({ row }: { row: Row }) => ( + + ) + } + ] + : []), + { + accessorKey: "ip", + header: t("targetAddr"), + cell: ({ row }) => ( + + updateTarget(row.original.targetId, { + ...row.original, + ip: e.target.value + }) + } + /> + ) + }, + { + accessorKey: "port", + header: t("targetPort"), + cell: ({ row }) => ( + + updateTarget(row.original.targetId, { + ...row.original, + port: parseInt(e.target.value, 10) + }) + } + /> + ) + }, + { + accessorKey: "enabled", + header: t("enabled"), + cell: ({ row }) => ( + + updateTarget(row.original.targetId, { + ...row.original, + enabled: val + }) + } + /> + ) + }, + { + id: "actions", + cell: ({ row }) => ( + <> +
+ +
+ + ) + } + ]; + + const table = useReactTable({ + data: targets, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + state: { + pagination: { + pageIndex: 0, + pageSize: 1000 + } + } + }); + return ( <>
@@ -348,104 +751,6 @@ export default function Page() { )} /> - - ( - - - {t("site")} - - - - - - - - - - - - - {t( - "siteNotFound" - )} - - - {sites.map( - ( - site - ) => ( - { - baseForm.setValue( - "siteId", - site.siteId - ); - }} - > - - { - site.name - } - - ) - )} - - - - - - - - {t( - "siteSelectionDescription" - )} - - - )} - /> @@ -471,6 +776,13 @@ export default function Page() { "http", value === "http" ); + // Update method default when switching resource type + addTargetForm.setValue( + "method", + value === "http" + ? "http" + : null + ); }} cols={2} /> @@ -616,7 +928,7 @@ export default function Page() { )} /> - {build == "oss" && ( + {/* {build == "oss" && ( )} /> - )} + )} */} @@ -662,6 +974,379 @@ export default function Page() { )} + + + + {t("targets")} + + + {t("targetsDescription")} + + + +
+
+ +
+ ( + + + {t("site")} + +
+ + + + + + + + + + + + {t( + "siteNotFound" + )} + + + {sites.map( + ( + site + ) => ( + { + addTargetForm.setValue( + "siteId", + site.siteId + ); + }} + > + + { + site.name + } + + ) + )} + + + + + + + {field.value && + (() => { + const selectedSite = + sites.find( + ( + site + ) => + site.siteId === + field.value + ); + return selectedSite && + selectedSite.type === + "newt" ? (() => { + const dockerState = getDockerStateForSite(selectedSite.siteId); + return ( + refreshContainersForSite(selectedSite.siteId)} + /> + ); + })() : null; + })()} +
+ +
+ )} + /> + + {baseForm.watch("http") && ( + ( + + + {t( + "method" + )} + + + + + + + )} + /> + )} + + ( + + + {t( + "targetAddr" + )} + + + + + + + )} + /> + ( + + + {t( + "targetPort" + )} + + + + + + + )} + /> + +
+
+ +
+ + {targets.length > 0 ? ( + <> +
+ {t("targetsList")} +
+
+ + + {table + .getHeaderGroups() + .map( + ( + headerGroup + ) => ( + + {headerGroup.headers.map( + ( + header + ) => ( + + {header.isPlaceholder + ? null + : flexRender( + header + .column + .columnDef + .header, + header.getContext() + )} + + ) + )} + + ) + )} + + + {table.getRowModel() + .rows?.length ? ( + table + .getRowModel() + .rows.map( + (row) => ( + + {row + .getVisibleCells() + .map( + ( + cell + ) => ( + + {flexRender( + cell + .column + .columnDef + .cell, + cell.getContext() + )} + + ) + )} + + ) + ) + ) : ( + + + {t( + "targetNoOne" + )} + + + )} + +
+
+ + ) : ( +
+

+ {t("targetNoOne")} +

+
+ )} +
+
+
diff --git a/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx b/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx index 6094f167..36ab1727 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/SiteInfoCard.tsx @@ -33,9 +33,7 @@ export default function SiteInfoCard({}: SiteInfoCardProps) { return ( - - {t("siteInfo")} - + {(site.type == "newt" || site.type == "wireguard") && ( <> diff --git a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx index f92a5090..8bd8dc4b 100644 --- a/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx +++ b/src/app/[orgId]/settings/sites/[niceId]/general/page.tsx @@ -38,12 +38,14 @@ import { Tag, TagInput } from "@app/components/tags/tag-input"; const GeneralFormSchema = z.object({ name: z.string().nonempty("Name is required"), dockerSocketEnabled: z.boolean().optional(), - remoteSubnets: z.array( - z.object({ - id: z.string(), - text: z.string() - }) - ).optional() + remoteSubnets: z + .array( + z.object({ + id: z.string(), + text: z.string() + }) + ) + .optional() }); type GeneralFormValues = z.infer; @@ -55,7 +57,9 @@ export default function GeneralPage() { const api = createApiClient(useEnvContext()); const [loading, setLoading] = useState(false); - const [activeCidrTagIndex, setActiveCidrTagIndex] = useState(null); + const [activeCidrTagIndex, setActiveCidrTagIndex] = useState( + null + ); const router = useRouter(); const t = useTranslations(); @@ -66,10 +70,10 @@ export default function GeneralPage() { name: site?.name, dockerSocketEnabled: site?.dockerSocketEnabled ?? false, remoteSubnets: site?.remoteSubnets - ? site.remoteSubnets.split(',').map((subnet, index) => ({ - id: subnet.trim(), - text: subnet.trim() - })) + ? site.remoteSubnets.split(",").map((subnet, index) => ({ + id: subnet.trim(), + text: subnet.trim() + })) : [] }, mode: "onChange" @@ -82,7 +86,10 @@ export default function GeneralPage() { .post(`/site/${site?.siteId}`, { name: data.name, dockerSocketEnabled: data.dockerSocketEnabled, - remoteSubnets: data.remoteSubnets?.map(subnet => subnet.text).join(',') || '' + remoteSubnets: + data.remoteSubnets + ?.map((subnet) => subnet.text) + .join(",") || "" }) .catch((e) => { toast({ @@ -98,7 +105,8 @@ export default function GeneralPage() { updateSite({ name: data.name, dockerSocketEnabled: data.dockerSocketEnabled, - remoteSubnets: data.remoteSubnets?.map(subnet => subnet.text).join(',') || '' + remoteSubnets: + data.remoteSubnets?.map((subnet) => subnet.text).join(",") || "" }); toast({ @@ -145,42 +153,64 @@ export default function GeneralPage() { )} /> - ( - - {t("remoteSubnets")} - - { - form.setValue( - "remoteSubnets", - newSubnets as Tag[] - ); - }} - validateTag={(tag) => { - // Basic CIDR validation regex - const cidrRegex = /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/; - return cidrRegex.test(tag); - }} - allowDuplicates={false} - sortTags={true} - /> - - - {t("remoteSubnetsDescription")} - - - - )} - /> + {env.flags.enableClients && + site.type === "newt" ? ( + ( + + + {t("remoteSubnets")} + + + { + form.setValue( + "remoteSubnets", + newSubnets as Tag[] + ); + }} + validateTag={(tag) => { + // Basic CIDR validation regex + const cidrRegex = + /^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/; + return cidrRegex.test( + tag + ); + }} + allowDuplicates={false} + sortTags={true} + /> + + + {t( + "remoteSubnetsDescription" + )} + + + + )} + /> + ) : null} {site && site.type === "newt" && ( {t("siteConfiguration")}

-
+
{t("setupToken")} - + + + {t("setupTokenDescription")} + )} diff --git a/src/app/auth/resource/[resourceId]/AutoLoginHandler.tsx b/src/app/auth/resource/[resourceId]/AutoLoginHandler.tsx new file mode 100644 index 00000000..c489a759 --- /dev/null +++ b/src/app/auth/resource/[resourceId]/AutoLoginHandler.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { GenerateOidcUrlResponse } from "@server/routers/idp"; +import { AxiosResponse } from "axios"; +import { useRouter } from "next/navigation"; +import { + Card, + CardHeader, + CardTitle, + CardContent, + CardDescription +} from "@app/components/ui/card"; +import { Alert, AlertDescription } from "@app/components/ui/alert"; +import { Loader2, CheckCircle2, AlertCircle } from "lucide-react"; +import { useTranslations } from "next-intl"; + +type AutoLoginHandlerProps = { + resourceId: number; + skipToIdpId: number; + redirectUrl: string; +}; + +export default function AutoLoginHandler({ + resourceId, + skipToIdpId, + redirectUrl +}: AutoLoginHandlerProps) { + const { env } = useEnvContext(); + const api = createApiClient({ env }); + const router = useRouter(); + const t = useTranslations(); + + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function initiateAutoLogin() { + setLoading(true); + + try { + const res = await api.post< + AxiosResponse + >(`/auth/idp/${skipToIdpId}/oidc/generate-url`, { + redirectUrl + }); + + if (res.data.data.redirectUrl) { + // Redirect to the IDP for authentication + window.location.href = res.data.data.redirectUrl; + } else { + setError(t("autoLoginErrorNoRedirectUrl")); + } + } catch (e) { + console.error("Failed to generate OIDC URL:", e); + setError(formatAxiosError(e, t("autoLoginErrorGeneratingUrl"))); + } finally { + setLoading(false); + } + } + + initiateAutoLogin(); + }, []); + + return ( +
+ + + {t("autoLoginTitle")} + {t("autoLoginDescription")} + + + {loading && ( +
+ + {t("autoLoginProcessing")} +
+ )} + {!loading && !error && ( +
+ + {t("autoLoginRedirecting")} +
+ )} + {error && ( + + + + {t("autoLoginError")} + {error} + + + )} +
+
+
+ ); +} diff --git a/src/app/auth/resource/[resourceId]/page.tsx b/src/app/auth/resource/[resourceId]/page.tsx index 9032ae18..347d3586 100644 --- a/src/app/auth/resource/[resourceId]/page.tsx +++ b/src/app/auth/resource/[resourceId]/page.tsx @@ -15,6 +15,7 @@ import AccessToken from "./AccessToken"; import { pullEnv } from "@app/lib/pullEnv"; import { LoginFormIDP } from "@app/components/LoginForm"; import { ListIdpsResponse } from "@server/routers/idp"; +import AutoLoginHandler from "./AutoLoginHandler"; export const dynamic = "force-dynamic"; @@ -30,11 +31,13 @@ export default async function ResourceAuthPage(props: { const env = pullEnv(); + const authHeader = await authCookieHeader(); + let authInfo: GetResourceAuthInfoResponse | undefined; try { const res = await internal.get< AxiosResponse - >(`/resource/${params.resourceId}/auth`, await authCookieHeader()); + >(`/resource/${params.resourceId}/auth`, authHeader); if (res && res.status === 200) { authInfo = res.data.data; @@ -62,10 +65,9 @@ export default async function ResourceAuthPage(props: { const redirectPort = new URL(searchParams.redirect).port; const serverResourceHostWithPort = `${serverResourceHost}:${redirectPort}`; - if (serverResourceHost === redirectHost) { redirectUrl = searchParams.redirect; - } else if ( serverResourceHostWithPort === redirectHost ) { + } else if (serverResourceHostWithPort === redirectHost) { redirectUrl = searchParams.redirect; } } catch (e) {} @@ -144,6 +146,19 @@ export default async function ResourceAuthPage(props: { name: idp.name })) as LoginFormIDP[]; + if (authInfo.skipToIdpId && authInfo.skipToIdpId !== null) { + const idp = loginIdps.find((idp) => idp.idpId === authInfo.skipToIdpId); + if (idp) { + return ( + + ); + } + } + return ( <> {userIsUnauthorized && isSSOOnly ? ( diff --git a/src/components/ContainersSelector.tsx b/src/components/ContainersSelector.tsx index 0f09fb5a..7ed31b62 100644 --- a/src/components/ContainersSelector.tsx +++ b/src/components/ContainersSelector.tsx @@ -43,35 +43,30 @@ import { } from "@/components/ui/dropdown-menu"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Search, RefreshCw, Filter, Columns } from "lucide-react"; -import { GetSiteResponse, Container } from "@server/routers/site"; -import { useDockerSocket } from "@app/hooks/useDockerSocket"; +import { Container } from "@server/routers/site"; import { useTranslations } from "next-intl"; - -// Type definitions based on the JSON structure +import { FaDocker } from "react-icons/fa"; interface ContainerSelectorProps { - site: GetSiteResponse; + site: { siteId: number; name: string; type: string }; + containers: Container[]; + isAvailable: boolean; onContainerSelect?: (hostname: string, port?: number) => void; + onRefresh?: () => void; } export const ContainersSelector: FC = ({ site, - onContainerSelect + containers, + isAvailable, + onContainerSelect, + onRefresh }) => { const [open, setOpen] = useState(false); const t = useTranslations(); - const { isAvailable, containers, fetchContainers } = useDockerSocket(site); - - useEffect(() => { - console.log("DockerSocket isAvailable:", isAvailable); - if (isAvailable) { - fetchContainers(); - } - }, [isAvailable]); - - if (!site || !isAvailable) { + if (!site || !isAvailable || site.type !== "newt") { return null; } @@ -84,13 +79,14 @@ export const ContainersSelector: FC = ({ return ( <> - setOpen(true)} + title={t("viewDockerContainers")} > - {t("viewDockerContainers")} - + + @@ -106,7 +102,7 @@ export const ContainersSelector: FC = ({ fetchContainers()} + onRefresh={onRefresh || (() => {})} />
@@ -263,7 +259,9 @@ const DockerContainersTable: FC<{ size="sm" className="h-6 px-2 text-xs hover:bg-muted" > - {t("containerLabelsCount", { count: labelEntries.length })} + {t("containerLabelsCount", { + count: labelEntries.length + })} @@ -279,7 +277,10 @@ const DockerContainersTable: FC<{ {key}
- {value || t("containerLabelEmpty")} + {value || + t( + "containerLabelEmpty" + )}
))} @@ -316,7 +317,9 @@ const DockerContainersTable: FC<{ onContainerSelect(row.original, ports[0])} + onClick={() => + onContainerSelect(row.original, ports[0]) + } disabled={row.original.state !== "running"} > {t("select")} @@ -415,9 +420,7 @@ const DockerContainersTable: FC<{ hideStoppedContainers) && containers.length > 0 ? ( <> -

- {t("noContainersMatchingFilters")} -

+

{t("noContainersMatchingFilters")}

{hideContainersWithoutPorts && (
@@ -463,7 +464,9 @@ const DockerContainersTable: FC<{
setSearchInput(event.target.value) @@ -473,7 +476,10 @@ const DockerContainersTable: FC<{ {searchInput && table.getFilteredRowModel().rows.length > 0 && (
- {t("searchResultsCount", { count: table.getFilteredRowModel().rows.length })} + {t("searchResultsCount", { + count: table.getFilteredRowModel().rows + .length + })}
)}
@@ -644,7 +650,9 @@ const DockerContainersTable: FC<{ {t("searching")} ) : ( - t("noContainersFoundMatching", { filter: globalFilter }) + t("noContainersFoundMatching", { + filter: globalFilter + }) )} diff --git a/src/components/CreateInternalResourceDialog.tsx b/src/components/CreateInternalResourceDialog.tsx new file mode 100644 index 00000000..3c4841d7 --- /dev/null +++ b/src/components/CreateInternalResourceDialog.tsx @@ -0,0 +1,422 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Button } from "@app/components/ui/button"; +import { Input } from "@app/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList +} from "@app/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@app/components/ui/popover"; +import { Check, ChevronsUpDown } from "lucide-react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { toast } from "@app/hooks/useToast"; +import { useTranslations } from "next-intl"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { ListSitesResponse } from "@server/routers/site"; +import { cn } from "@app/lib/cn"; + +type Site = ListSitesResponse["sites"][0]; + +type CreateInternalResourceDialogProps = { + open: boolean; + setOpen: (val: boolean) => void; + orgId: string; + sites: Site[]; + onSuccess?: () => void; +}; + +export default function CreateInternalResourceDialog({ + open, + setOpen, + orgId, + sites, + onSuccess +}: CreateInternalResourceDialogProps) { + const t = useTranslations(); + const api = createApiClient(useEnvContext()); + const [isSubmitting, setIsSubmitting] = useState(false); + + const formSchema = z.object({ + name: z + .string() + .min(1, t("createInternalResourceDialogNameRequired")) + .max(255, t("createInternalResourceDialogNameMaxLength")), + siteId: z.number().int().positive(t("createInternalResourceDialogPleaseSelectSite")), + protocol: z.enum(["tcp", "udp"]), + proxyPort: z + .number() + .int() + .positive() + .min(1, t("createInternalResourceDialogProxyPortMin")) + .max(65535, t("createInternalResourceDialogProxyPortMax")), + destinationIp: z.string().ip(t("createInternalResourceDialogInvalidIPAddressFormat")), + destinationPort: z + .number() + .int() + .positive() + .min(1, t("createInternalResourceDialogDestinationPortMin")) + .max(65535, t("createInternalResourceDialogDestinationPortMax")) + }); + + type FormData = z.infer; + + const availableSites = sites.filter( + (site) => site.type === "newt" && site.subnet + ); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: "", + siteId: availableSites[0]?.siteId || 0, + protocol: "tcp", + proxyPort: undefined, + destinationIp: "", + destinationPort: undefined + } + }); + + useEffect(() => { + if (open && availableSites.length > 0) { + form.reset({ + name: "", + siteId: availableSites[0].siteId, + protocol: "tcp", + proxyPort: undefined, + destinationIp: "", + destinationPort: undefined + }); + } + }, [open]); + + const handleSubmit = async (data: FormData) => { + setIsSubmitting(true); + try { + await api.put(`/org/${orgId}/site/${data.siteId}/resource`, { + name: data.name, + protocol: data.protocol, + proxyPort: data.proxyPort, + destinationIp: data.destinationIp, + destinationPort: data.destinationPort, + enabled: true + }); + + toast({ + title: t("createInternalResourceDialogSuccess"), + description: t("createInternalResourceDialogInternalResourceCreatedSuccessfully"), + variant: "default" + }); + + onSuccess?.(); + setOpen(false); + } catch (error) { + console.error("Error creating internal resource:", error); + toast({ + title: t("createInternalResourceDialogError"), + description: formatAxiosError( + error, + t("createInternalResourceDialogFailedToCreateInternalResource") + ), + variant: "destructive" + }); + } finally { + setIsSubmitting(false); + } + }; + + if (availableSites.length === 0) { + return ( + + + + {t("createInternalResourceDialogNoSitesAvailable")} + + {t("createInternalResourceDialogNoSitesAvailableDescription")} + + + + + + + + ); + } + + return ( + + + + {t("createInternalResourceDialogCreateClientResource")} + + {t("createInternalResourceDialogCreateClientResourceDescription")} + + + +
+ + {/* Resource Properties Form */} +
+

+ {t("createInternalResourceDialogResourceProperties")} +

+
+ ( + + {t("createInternalResourceDialogName")} + + + + + + )} + /> + +
+ ( + + {t("createInternalResourceDialogSite")} + + + + + + + + + + + {t("createInternalResourceDialogNoSitesFound")} + + {availableSites.map((site) => ( + { + field.onChange(site.siteId); + }} + > + + {site.name} + + ))} + + + + + + + + )} + /> + + ( + + + {t("createInternalResourceDialogProtocol")} + + + + + )} + /> +
+ + ( + + {t("createInternalResourceDialogSitePort")} + + + field.onChange( + e.target.value === "" ? undefined : parseInt(e.target.value) + ) + } + /> + + + {t("createInternalResourceDialogSitePortDescription")} + + + + )} + /> +
+
+ + {/* Target Configuration Form */} +
+

+ {t("createInternalResourceDialogTargetConfiguration")} +

+
+
+ ( + + + {t("createInternalResourceDialogDestinationIP")} + + + + + + {t("createInternalResourceDialogDestinationIPDescription")} + + + + )} + /> + + ( + + + {t("createInternalResourceDialogDestinationPort")} + + + + field.onChange( + e.target.value === "" ? undefined : parseInt(e.target.value) + ) + } + /> + + + {t("createInternalResourceDialogDestinationPortDescription")} + + + + )} + /> +
+
+
+
+ +
+ + + + +
+
+ ); +} diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 5f4104ea..1fc856c9 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -3,13 +3,28 @@ import { useState, useEffect, useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator +} from "@/components/ui/command"; +import { + Popover, + PopoverContent, + PopoverTrigger +} from "@/components/ui/popover"; import { AlertCircle, CheckCircle2, Building2, Zap, + Check, + ChevronsUpDown, ArrowUpDown } from "lucide-react"; import { Alert, AlertDescription } from "@/components/ui/alert"; @@ -19,9 +34,9 @@ import { toast } from "@/hooks/useToast"; import { ListDomainsResponse } from "@server/routers/domain/listDomains"; import { AxiosResponse } from "axios"; import { cn } from "@/lib/cn"; -import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { useTranslations } from "next-intl"; import { build } from "@server/build"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; type OrganizationDomain = { domainId: string; @@ -39,17 +54,15 @@ type AvailableOption = { type DomainOption = { id: string; domain: string; - type: "organization" | "provided"; + type: "organization" | "provided" | "provided-search"; verified?: boolean; domainType?: "ns" | "cname" | "wildcard"; domainId?: string; domainNamespaceId?: string; - subdomain?: string; }; -interface DomainPickerProps { +interface DomainPicker2Props { orgId: string; - cols?: number; onDomainChange?: (domainInfo: { domainId: string; domainNamespaceId?: string; @@ -58,34 +71,37 @@ interface DomainPickerProps { fullDomain: string; baseDomain: string; }) => void; + cols?: number; } -export default function DomainPicker({ +export default function DomainPicker2({ orgId, - cols, - onDomainChange -}: DomainPickerProps) { + onDomainChange, + cols = 2 +}: DomainPicker2Props) { const { env } = useEnvContext(); const api = createApiClient({ env }); const t = useTranslations(); - const [userInput, setUserInput] = useState(""); - const [selectedOption, setSelectedOption] = useState( - null - ); + const [subdomainInput, setSubdomainInput] = useState(""); + const [selectedBaseDomain, setSelectedBaseDomain] = + useState(null); const [availableOptions, setAvailableOptions] = useState( [] ); - const [isChecking, setIsChecking] = useState(false); const [organizationDomains, setOrganizationDomains] = useState< OrganizationDomain[] >([]); const [loadingDomains, setLoadingDomains] = useState(false); + const [open, setOpen] = useState(false); + + // Provided domain search states + const [userInput, setUserInput] = useState(""); + const [isChecking, setIsChecking] = useState(false); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); - const [activeTab, setActiveTab] = useState< - "all" | "organization" | "provided" - >("all"); const [providedDomainsShown, setProvidedDomainsShown] = useState(3); + const [selectedProvidedDomain, setSelectedProvidedDomain] = + useState(null); useEffect(() => { const loadOrganizationDomains = async () => { @@ -107,6 +123,41 @@ export default function DomainPicker({ type: domain.type as "ns" | "cname" | "wildcard" })); setOrganizationDomains(domains); + + // Auto-select first available domain + if (domains.length > 0) { + // Select the first organization domain + const firstOrgDomain = domains[0]; + const domainOption: DomainOption = { + id: `org-${firstOrgDomain.domainId}`, + domain: firstOrgDomain.baseDomain, + type: "organization", + verified: firstOrgDomain.verified, + domainType: firstOrgDomain.type, + domainId: firstOrgDomain.domainId + }; + setSelectedBaseDomain(domainOption); + + onDomainChange?.({ + domainId: firstOrgDomain.domainId, + type: "organization", + subdomain: undefined, + fullDomain: firstOrgDomain.baseDomain, + baseDomain: firstOrgDomain.baseDomain + }); + } else if (build === "saas" || build === "enterprise") { + // If no organization domains, select the provided domain option + const domainOptionText = + build === "enterprise" + ? "Provided Domain" + : "Free Provided Domain"; + const freeDomainOption: DomainOption = { + id: "provided-search", + domain: domainOptionText, + type: "provided-search" + }; + setSelectedBaseDomain(freeDomainOption); + } } } catch (error) { console.error("Failed to load organization domains:", error); @@ -123,135 +174,131 @@ export default function DomainPicker({ loadOrganizationDomains(); }, [orgId, api]); - // Generate domain options based on user input - const generateDomainOptions = (): DomainOption[] => { + const checkAvailability = useCallback( + async (input: string) => { + if (!input.trim()) { + setAvailableOptions([]); + setIsChecking(false); + return; + } + + setIsChecking(true); + try { + const checkSubdomain = input + .toLowerCase() + .replace(/\./g, "-") + .replace(/[^a-z0-9-]/g, "") + .replace(/-+/g, "-"); + } catch (error) { + console.error("Failed to check domain availability:", error); + setAvailableOptions([]); + toast({ + variant: "destructive", + title: "Error", + description: "Failed to check domain availability" + }); + } finally { + setIsChecking(false); + } + }, + [api] + ); + + const debouncedCheckAvailability = useCallback( + debounce(checkAvailability, 500), + [checkAvailability] + ); + + useEffect(() => { + if (selectedBaseDomain?.type === "provided-search") { + setProvidedDomainsShown(3); + setSelectedProvidedDomain(null); + + if (userInput.trim()) { + setIsChecking(true); + debouncedCheckAvailability(userInput); + } else { + setAvailableOptions([]); + setIsChecking(false); + } + } + }, [userInput, debouncedCheckAvailability, selectedBaseDomain]); + + const generateDropdownOptions = (): DomainOption[] => { const options: DomainOption[] = []; - if (!userInput.trim()) return options; - - // Add organization domain options organizationDomains.forEach((orgDomain) => { - if (orgDomain.type === "cname") { - // For CNAME domains, check if the user input matches exactly - if ( - orgDomain.baseDomain.toLowerCase() === - userInput.toLowerCase() - ) { - options.push({ - id: `org-${orgDomain.domainId}`, - domain: orgDomain.baseDomain, - type: "organization", - verified: orgDomain.verified, - domainType: "cname", - domainId: orgDomain.domainId - }); - } - } else if (orgDomain.type === "ns") { - // For NS domains, check if the user input could be a subdomain - const userInputLower = userInput.toLowerCase(); - const baseDomainLower = orgDomain.baseDomain.toLowerCase(); - - // Check if user input ends with the base domain - if (userInputLower.endsWith(`.${baseDomainLower}`)) { - const subdomain = userInputLower.slice( - 0, - -(baseDomainLower.length + 1) - ); - options.push({ - id: `org-${orgDomain.domainId}`, - domain: userInput, - type: "organization", - verified: orgDomain.verified, - domainType: "ns", - domainId: orgDomain.domainId, - subdomain: subdomain - }); - } else if (userInputLower === baseDomainLower) { - // Exact match for base domain - options.push({ - id: `org-${orgDomain.domainId}`, - domain: orgDomain.baseDomain, - type: "organization", - verified: orgDomain.verified, - domainType: "ns", - domainId: orgDomain.domainId - }); - } - } else if (orgDomain.type === "wildcard") { - // For wildcard domains, allow the base domain or multiple levels up - const userInputLower = userInput.toLowerCase(); - const baseDomainLower = orgDomain.baseDomain.toLowerCase(); - - // Check if user input is exactly the base domain - if (userInputLower === baseDomainLower) { - options.push({ - id: `org-${orgDomain.domainId}`, - domain: orgDomain.baseDomain, - type: "organization", - verified: orgDomain.verified, - domainType: "wildcard", - domainId: orgDomain.domainId - }); - } - // Check if user input ends with the base domain (allows multiple level subdomains) - else if (userInputLower.endsWith(`.${baseDomainLower}`)) { - const subdomain = userInputLower.slice( - 0, - -(baseDomainLower.length + 1) - ); - // Allow multiple levels (subdomain can contain dots) - options.push({ - id: `org-${orgDomain.domainId}`, - domain: userInput, - type: "organization", - verified: orgDomain.verified, - domainType: "wildcard", - domainId: orgDomain.domainId, - subdomain: subdomain - }); - } - } - }); - - // Add provided domain options (always try to match provided domains) - availableOptions.forEach((option) => { options.push({ - id: `provided-${option.domainNamespaceId}`, - domain: option.fullDomain, - type: "provided", - domainNamespaceId: option.domainNamespaceId, - domainId: option.domainId + id: `org-${orgDomain.domainId}`, + domain: orgDomain.baseDomain, + type: "organization", + verified: orgDomain.verified, + domainType: orgDomain.type, + domainId: orgDomain.domainId }); }); - // Sort options - return options.sort((a, b) => { - const comparison = a.domain.localeCompare(b.domain); - return sortOrder === "asc" ? comparison : -comparison; - }); + if (build === "saas" || build === "enterprise") { + const domainOptionText = + build === "enterprise" + ? "Provided Domain" + : "Free Provided Domain"; + options.push({ + id: "provided-search", + domain: domainOptionText, + type: "provided-search" + }); + } + + return options; }; - const domainOptions = generateDomainOptions(); + const dropdownOptions = generateDropdownOptions(); - // Filter options based on active tab - const filteredOptions = domainOptions.filter((option) => { - if (activeTab === "all") return true; - return option.type === activeTab; - }); + const validateSubdomain = ( + subdomain: string, + baseDomain: DomainOption + ): boolean => { + if (!baseDomain) return false; - // Separate organization and provided options for pagination - const organizationOptions = filteredOptions.filter( - (opt) => opt.type === "organization" - ); - const allProvidedOptions = filteredOptions.filter( - (opt) => opt.type === "provided" - ); - const providedOptions = allProvidedOptions.slice(0, providedDomainsShown); - const hasMoreProvided = allProvidedOptions.length > providedDomainsShown; + if (baseDomain.type === "provided-search") { + return /^[a-zA-Z0-9-]+$/.test(subdomain); + } - // Handle option selection - const handleOptionSelect = (option: DomainOption) => { - setSelectedOption(option); + if (baseDomain.type === "organization") { + if (baseDomain.domainType === "cname") { + return subdomain === ""; + } else if (baseDomain.domainType === "ns") { + return /^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$/.test(subdomain); + } else if (baseDomain.domainType === "wildcard") { + return /^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*$/.test(subdomain); + } + } + + return false; + }; + + // Handle base domain selection + const handleBaseDomainSelect = (option: DomainOption) => { + setSelectedBaseDomain(option); + setOpen(false); + + if (option.domainType === "cname") { + setSubdomainInput(""); + } + + if (option.type === "provided-search") { + setUserInput(""); + setAvailableOptions([]); + setSelectedProvidedDomain(null); + onDomainChange?.({ + domainId: option.domainId!, + type: "organization", + subdomain: undefined, + fullDomain: option.domain, + baseDomain: option.domain + }); + } if (option.type === "organization") { if (option.domainType === "cname") { @@ -262,258 +309,413 @@ export default function DomainPicker({ fullDomain: option.domain, baseDomain: option.domain }); - } else if (option.domainType === "ns") { - const subdomain = option.subdomain || ""; + } else { onDomainChange?.({ domainId: option.domainId!, type: "organization", - subdomain: subdomain || undefined, + subdomain: undefined, fullDomain: option.domain, baseDomain: option.domain }); - } else if (option.domainType === "wildcard") { + } + } + }; + + const handleSubdomainChange = (value: string) => { + const validInput = value.replace(/[^a-zA-Z0-9.-]/g, ""); + setSubdomainInput(validInput); + + setSelectedProvidedDomain(null); + + if (selectedBaseDomain && selectedBaseDomain.type === "organization") { + const isValid = validateSubdomain(validInput, selectedBaseDomain); + if (isValid) { + const fullDomain = validInput + ? `${validInput}.${selectedBaseDomain.domain}` + : selectedBaseDomain.domain; onDomainChange?.({ - domainId: option.domainId!, + domainId: selectedBaseDomain.domainId!, type: "organization", - subdomain: option.subdomain || undefined, - fullDomain: option.domain, - baseDomain: option.subdomain - ? option.domain.split(".").slice(1).join(".") - : option.domain + subdomain: validInput || undefined, + fullDomain: fullDomain, + baseDomain: selectedBaseDomain.domain + }); + } else if (validInput === "") { + onDomainChange?.({ + domainId: selectedBaseDomain.domainId!, + type: "organization", + subdomain: undefined, + fullDomain: selectedBaseDomain.domain, + baseDomain: selectedBaseDomain.domain }); } - } else if (option.type === "provided") { - // Extract subdomain from full domain - const parts = option.domain.split("."); - const subdomain = parts[0]; - const baseDomain = parts.slice(1).join("."); + } + }; + + const handleProvidedDomainInputChange = (value: string) => { + const validInput = value.replace(/[^a-zA-Z0-9.-]/g, ""); + setUserInput(validInput); + + // Clear selected domain when user types + if (selectedProvidedDomain) { + setSelectedProvidedDomain(null); onDomainChange?.({ - domainId: option.domainId!, - domainNamespaceId: option.domainNamespaceId, + domainId: "", type: "provided", - subdomain: subdomain, - fullDomain: option.domain, - baseDomain: baseDomain + subdomain: undefined, + fullDomain: "", + baseDomain: "" }); } }; + const handleProvidedDomainSelect = (option: AvailableOption) => { + setSelectedProvidedDomain(option); + + const parts = option.fullDomain.split("."); + const subdomain = parts[0]; + const baseDomain = parts.slice(1).join("."); + + onDomainChange?.({ + domainId: option.domainId, + domainNamespaceId: option.domainNamespaceId, + type: "provided", + subdomain: subdomain, + fullDomain: option.fullDomain, + baseDomain: baseDomain + }); + }; + + const isSubdomainValid = selectedBaseDomain + ? validateSubdomain(subdomainInput, selectedBaseDomain) + : true; + const showSubdomainInput = + selectedBaseDomain && + selectedBaseDomain.type === "organization" && + selectedBaseDomain.domainType !== "cname"; + const showProvidedDomainSearch = + selectedBaseDomain?.type === "provided-search"; + + const sortedAvailableOptions = availableOptions.sort((a, b) => { + const comparison = a.fullDomain.localeCompare(b.fullDomain); + return sortOrder === "asc" ? comparison : -comparison; + }); + + const displayedProvidedOptions = sortedAvailableOptions.slice( + 0, + providedDomainsShown + ); + const hasMoreProvided = + sortedAvailableOptions.length > providedDomainsShown; + return ( -
- {/* Domain Input */} -
- - { - // Only allow letters, numbers, hyphens, and periods - const validInput = e.target.value.replace( - /[^a-zA-Z0-9.-]/g, - "" - ); - setUserInput(validInput); - // Clear selection when input changes - setSelectedOption(null); - }} - /> -

- {build === "saas" - ? t("domainPickerDescriptionSaas") - : t("domainPickerDescription")} -

+
+
+
+ + { + if (showProvidedDomainSearch) { + handleProvidedDomainInputChange(e.target.value); + } else { + handleSubdomainChange(e.target.value); + } + }} + /> + {showSubdomainInput && !subdomainInput && ( +

+ {t("domainPickerEnterSubdomainOrLeaveBlank")} +

+ )} + {showProvidedDomainSearch && !userInput && ( +

+ {t("domainPickerEnterSubdomainToSearch")} +

+ )} +
+ +
+ + + + + + + + + +
+ {t("domainPickerNoDomainsFound")} +
+
+ + {organizationDomains.length > 0 && ( + <> + + + {organizationDomains.map( + (orgDomain) => ( + + handleBaseDomainSelect( + { + id: `org-${orgDomain.domainId}`, + domain: orgDomain.baseDomain, + type: "organization", + verified: + orgDomain.verified, + domainType: + orgDomain.type, + domainId: + orgDomain.domainId + } + ) + } + className="mx-2 rounded-md" + disabled={ + !orgDomain.verified + } + > +
+ +
+
+ + { + orgDomain.baseDomain + } + + + {orgDomain.type.toUpperCase()}{" "} + •{" "} + {orgDomain.verified + ? "Verified" + : "Unverified"} + +
+ +
+ ) + )} +
+
+ {(build === "saas" || + build === "enterprise") && ( + + )} + + )} + + {(build === "saas" || + build === "enterprise") && ( + + + + handleBaseDomainSelect({ + id: "provided-search", + domain: + build === + "enterprise" + ? "Provided Domain" + : "Free Provided Domain", + type: "provided-search" + }) + } + className="mx-2 rounded-md" + > +
+ +
+
+ + {build === "enterprise" + ? "Provided Domain" + : "Free Provided Domain"} + + + {t( + "domainPickerSearchForAvailableDomains" + )} + +
+ +
+
+
+ )} +
+
+
+
- {/* Tabs and Sort Toggle */} - {build === "saas" && ( -
- - setActiveTab( - value as "all" | "organization" | "provided" - ) - } - > - - - {t("domainPickerTabAll")} - - - {t("domainPickerTabOrganization")} - - {build == "saas" && ( - - {t("domainPickerTabProvided")} - - )} - - - -
- )} - - {/* Loading State */} - {isChecking && ( -
-
-
- {t("domainPickerCheckingAvailability")} -
-
- )} - - {/* No Options */} - {!isChecking && - filteredOptions.length === 0 && - userInput.trim() && ( - - - - {t("domainPickerNoMatchingDomains")} - - - )} - - {/* Domain Options */} - {!isChecking && filteredOptions.length > 0 && ( + {showProvidedDomainSearch && (
- {/* Organization Domains */} - {organizationOptions.length > 0 && ( -
- {build !== "oss" && ( -
- -

- {t("domainPickerOrganizationDomains")} -

-
- )} -
- {organizationOptions.map((option) => ( -
- option.verified && - handleOptionSelect(option) - } - > -
-
-
-

- {option.domain} -

- {/* */} - {/* {option.domainType} */} - {/* */} - {option.verified ? ( - - ) : ( - - )} -
- {option.subdomain && ( -

- {t( - "domainPickerSubdomain", - { - subdomain: - option.subdomain - } - )} -

- )} - {!option.verified && ( -

- Domain is unverified -

- )} -
-
-
- ))} + {isChecking && ( +
+
+
+ + {t("domainPickerCheckingAvailability")} +
)} - {/* Provided Domains */} - {providedOptions.length > 0 && ( + {!isChecking && + sortedAvailableOptions.length === 0 && + userInput.trim() && ( + + + + {t("domainPickerNoMatchingDomains")} + + + )} + + {!isChecking && sortedAvailableOptions.length > 0 && (
-
- -
- {t("domainPickerProvidedDomains")} -
-
-
- {providedOptions.map((option) => ( -
- handleOptionSelect(option) + { + const option = + displayedProvidedOptions.find( + (opt) => + opt.domainNamespaceId === value + ); + if (option) { + handleProvidedDomainSelect(option); + } + }} + className={`grid gap-2 grid-cols-1 sm:grid-cols-${cols}`} + > + {displayedProvidedOptions.map((option) => ( + ))} -
+ {hasMoreProvided && (
); } diff --git a/src/components/EditInternalResourceDialog.tsx b/src/components/EditInternalResourceDialog.tsx new file mode 100644 index 00000000..5d594d02 --- /dev/null +++ b/src/components/EditInternalResourceDialog.tsx @@ -0,0 +1,276 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Button } from "@app/components/ui/button"; +import { Input } from "@app/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from "@app/components/ui/select"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage +} from "@app/components/ui/form"; +import { + Credenza, + CredenzaBody, + CredenzaClose, + CredenzaContent, + CredenzaDescription, + CredenzaFooter, + CredenzaHeader, + CredenzaTitle +} from "@app/components/Credenza"; +import { toast } from "@app/hooks/useToast"; +import { useTranslations } from "next-intl"; +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { useEnvContext } from "@app/hooks/useEnvContext"; +import { Separator } from "@app/components/ui/separator"; + +type InternalResourceData = { + id: number; + name: string; + orgId: string; + siteName: string; + protocol: string; + proxyPort: number | null; + siteId: number; + destinationIp?: string; + destinationPort?: number; +}; + +type EditInternalResourceDialogProps = { + open: boolean; + setOpen: (val: boolean) => void; + resource: InternalResourceData; + orgId: string; + onSuccess?: () => void; +}; + +export default function EditInternalResourceDialog({ + open, + setOpen, + resource, + orgId, + onSuccess +}: EditInternalResourceDialogProps) { + const t = useTranslations(); + const api = createApiClient(useEnvContext()); + const [isSubmitting, setIsSubmitting] = useState(false); + + const formSchema = z.object({ + name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")), + protocol: z.enum(["tcp", "udp"]), + proxyPort: z.number().int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")), + destinationIp: z.string().ip(t("editInternalResourceDialogInvalidIPAddressFormat")), + destinationPort: z.number().int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")) + }); + + type FormData = z.infer; + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + name: resource.name, + protocol: resource.protocol as "tcp" | "udp", + proxyPort: resource.proxyPort || undefined, + destinationIp: resource.destinationIp || "", + destinationPort: resource.destinationPort || undefined + } + }); + + useEffect(() => { + if (open) { + form.reset({ + name: resource.name, + protocol: resource.protocol as "tcp" | "udp", + proxyPort: resource.proxyPort || undefined, + destinationIp: resource.destinationIp || "", + destinationPort: resource.destinationPort || undefined + }); + } + }, [open, resource, form]); + + const handleSubmit = async (data: FormData) => { + setIsSubmitting(true); + try { + // Update the site resource + await api.post(`/org/${orgId}/site/${resource.siteId}/resource/${resource.id}`, { + name: data.name, + protocol: data.protocol, + proxyPort: data.proxyPort, + destinationIp: data.destinationIp, + destinationPort: data.destinationPort + }); + + toast({ + title: t("editInternalResourceDialogSuccess"), + description: t("editInternalResourceDialogInternalResourceUpdatedSuccessfully"), + variant: "default" + }); + + onSuccess?.(); + setOpen(false); + } catch (error) { + console.error("Error updating internal resource:", error); + toast({ + title: t("editInternalResourceDialogError"), + description: formatAxiosError(error, t("editInternalResourceDialogFailedToUpdateInternalResource")), + variant: "destructive" + }); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + + {t("editInternalResourceDialogEditClientResource")} + + {t("editInternalResourceDialogUpdateResourceProperties", { resourceName: resource.name })} + + + +
+ + {/* Resource Properties Form */} +
+

{t("editInternalResourceDialogResourceProperties")}

+
+ ( + + {t("editInternalResourceDialogName")} + + + + + + )} + /> + +
+ ( + + {t("editInternalResourceDialogProtocol")} + + + + )} + /> + + ( + + {t("editInternalResourceDialogSitePort")} + + field.onChange(parseInt(e.target.value) || 0)} + /> + + + + )} + /> +
+
+
+ + {/* Target Configuration Form */} +
+

{t("editInternalResourceDialogTargetConfiguration")}

+
+
+ ( + + {t("editInternalResourceDialogDestinationIP")} + + + + + + )} + /> + + ( + + {t("editInternalResourceDialogDestinationPort")} + + field.onChange(parseInt(e.target.value) || 0)} + /> + + + + )} + /> +
+
+
+
+ +
+ + + + +
+
+ ); +} diff --git a/src/components/LayoutSidebar.tsx b/src/components/LayoutSidebar.tsx index ce001f09..d309c11f 100644 --- a/src/components/LayoutSidebar.tsx +++ b/src/components/LayoutSidebar.tsx @@ -70,7 +70,7 @@ export function LayoutSidebar({ isCollapsed={isSidebarCollapsed} />
-
+
{!isAdminPage && user.serverAdmin && (
diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 7e8ad336..13bd87d3 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -150,7 +150,7 @@ export function SidebarNav({ {section.heading}
)} -
+
{section.items.map((item) => { const hydratedHref = hydrateHref(item.href); const isActive = pathname.startsWith(hydratedHref); diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index e6fad743..2c30ee73 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -9,7 +9,7 @@ const alertVariants = cva( variants: { variant: { default: "bg-card border text-foreground", - neutral: "bg-card border text-foreground", + neutral: "bg-card bg-muted border text-foreground", destructive: "border-destructive/50 border bg-destructive/10 text-destructive dark:border-destructive [&>svg]:text-destructive", success: diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index 6b22ddfe..fde1f12b 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -30,7 +30,15 @@ import { CardHeader, CardTitle } from "@app/components/ui/card"; +import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs"; import { useTranslations } from "next-intl"; +import { useMemo } from "react"; + +type TabFilter = { + id: string; + label: string; + filterFn: (row: any) => boolean; +}; type DataTableProps = { columns: ColumnDef[]; @@ -46,6 +54,8 @@ type DataTableProps = { id: string; desc: boolean; }; + tabs?: TabFilter[]; + defaultTab?: string; }; export function DataTable({ @@ -58,17 +68,36 @@ export function DataTable({ isRefreshing, searchPlaceholder = "Search...", searchColumn = "name", - defaultSort + defaultSort, + tabs, + defaultTab }: DataTableProps) { const [sorting, setSorting] = useState( defaultSort ? [defaultSort] : [] ); const [columnFilters, setColumnFilters] = useState([]); const [globalFilter, setGlobalFilter] = useState([]); + const [activeTab, setActiveTab] = useState( + defaultTab || tabs?.[0]?.id || "" + ); const t = useTranslations(); + // Apply tab filter to data + const filteredData = useMemo(() => { + if (!tabs || activeTab === "") { + return data; + } + + const activeTabFilter = tabs.find((tab) => tab.id === activeTab); + if (!activeTabFilter) { + return data; + } + + return data.filter(activeTabFilter.filterFn); + }, [data, tabs, activeTab]); + const table = useReactTable({ - data, + data: filteredData, columns, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), @@ -90,20 +119,49 @@ export function DataTable({ } }); + const handleTabChange = (value: string) => { + setActiveTab(value); + // Reset to first page when changing tabs + table.setPageIndex(0); + }; + return (
-
- - table.setGlobalFilter(String(e.target.value)) - } - className="w-full pl-8" - /> - +
+
+ + table.setGlobalFilter( + String(e.target.value) + ) + } + className="w-full pl-8" + /> + +
+ {tabs && tabs.length > 0 && ( + + + {tabs.map((tab) => ( + + {tab.label} ( + {data.filter(tab.filterFn).length}) + + ))} + + + )}
{onRefresh && ( diff --git a/src/components/ui/input-otp.tsx b/src/components/ui/input-otp.tsx index 9293541d..2afda77d 100644 --- a/src/components/ui/input-otp.tsx +++ b/src/components/ui/input-otp.tsx @@ -55,7 +55,7 @@ function InputOTPSlot({ data-slot="input-otp-slot" data-active={isActive} className={cn( - "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]", + "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-2xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]", className )} {...props} diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx index 880a44b7..eacaa12e 100644 --- a/src/components/ui/input.tsx +++ b/src/components/ui/input.tsx @@ -16,7 +16,7 @@ const Input = React.forwardRef( type={showPassword ? "text" : "password"} data-slot="input" className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base inset-shadow-2xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm shadow-2xs", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className @@ -43,7 +43,7 @@ const Input = React.forwardRef( type={type} data-slot="input" className={cn( - "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base inset-shadow-2xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", + "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm shadow-2xs", "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]", "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", className diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx index db231e17..03dd3d26 100644 --- a/src/components/ui/select.tsx +++ b/src/components/ui/select.tsx @@ -36,7 +36,7 @@ function SelectTrigger({ data-slot="select-trigger" data-size={size} className={cn( - "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 w-full", + "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-2xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 w-full", className )} {...props} diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index 7fa26a9e..94050ae2 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -29,7 +29,7 @@ const TabsTrigger = React.forwardRef< ) => void; updateAuthInfo: ( diff --git a/src/hooks/useDockerSocket.ts b/src/hooks/useDockerSocket.ts deleted file mode 100644 index dc4f08f4..00000000 --- a/src/hooks/useDockerSocket.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useCallback, useEffect, useState } from "react"; -import { useEnvContext } from "./useEnvContext"; -import { - Container, - GetDockerStatusResponse, - ListContainersResponse, - TriggerFetchResponse -} from "@server/routers/site"; -import { AxiosResponse } from "axios"; -import { toast } from "./useToast"; -import { Site } from "@server/db"; - -const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); - -export function useDockerSocket(site: Site) { - console.log(`useDockerSocket initialized for site ID: ${site.siteId}`); - - const [dockerSocket, setDockerSocket] = useState(); - const [containers, setContainers] = useState([]); - - const api = createApiClient(useEnvContext()); - - const { dockerSocketEnabled: rawIsEnabled = true, type: siteType } = site || {}; - const isEnabled = rawIsEnabled && siteType === "newt"; - const { isAvailable = false, socketPath } = dockerSocket || {}; - - const checkDockerSocket = useCallback(async () => { - if (!isEnabled) { - console.warn("Docker socket is not enabled for this site."); - return; - } - try { - const res = await api.post(`/site/${site.siteId}/docker/check`); - console.log("Docker socket check response:", res); - } catch (error) { - console.error("Failed to check Docker socket:", error); - } - }, [api, site.siteId, isEnabled]); - - const getDockerSocketStatus = useCallback(async () => { - if (!isEnabled) { - console.warn("Docker socket is not enabled for this site."); - return; - } - - try { - const res = await api.get>( - `/site/${site.siteId}/docker/status` - ); - - if (res.status === 200) { - setDockerSocket(res.data.data); - } else { - console.error("Failed to get Docker status:", res); - toast({ - variant: "destructive", - title: "Failed to get Docker status", - description: - "An error occurred while fetching Docker status." - }); - } - } catch (error) { - console.error("Failed to get Docker status:", error); - toast({ - variant: "destructive", - title: "Failed to get Docker status", - description: "An error occurred while fetching Docker status." - }); - } - }, [api, site.siteId, isEnabled]); - - const getContainers = useCallback( - async (maxRetries: number = 3) => { - if (!isEnabled || !isAvailable) { - console.warn("Docker socket is not enabled or available."); - return; - } - - const fetchContainerList = async () => { - if (!isEnabled || !isAvailable) { - return; - } - - let attempt = 0; - while (attempt < maxRetries) { - try { - const res = await api.get< - AxiosResponse - >(`/site/${site.siteId}/docker/containers`); - setContainers(res.data.data); - return res.data.data; - } catch (error: any) { - attempt++; - - // Check if the error is a 425 (Too Early) status - if (error?.response?.status === 425) { - if (attempt < maxRetries) { - console.log( - `Containers not ready yet (attempt ${attempt}/${maxRetries}). Retrying in 250ms...` - ); - await sleep(250); - continue; - } else { - console.warn( - "Max retry attempts reached. Containers may still be loading." - ); - // toast({ - // variant: "destructive", - // title: "Containers not ready", - // description: - // "Containers are still loading. Please try again in a moment." - // }); - } - } else { - console.error( - "Failed to fetch Docker containers:", - error - ); - toast({ - variant: "destructive", - title: "Failed to fetch containers", - description: formatAxiosError( - error, - "An error occurred while fetching containers" - ) - }); - } - break; - } - } - }; - - try { - const res = await api.post>( - `/site/${site.siteId}/docker/trigger` - ); - // TODO: identify a way to poll the server for latest container list periodically? - await fetchContainerList(); - return res.data.data; - } catch (error) { - console.error("Failed to trigger Docker containers:", error); - } - }, - [api, site.siteId, isEnabled, isAvailable] - ); - - // 2. Docker socket status monitoring - useEffect(() => { - if (!isEnabled || isAvailable) { - return; - } - - checkDockerSocket(); - getDockerSocketStatus(); - - }, [isEnabled, isAvailable, checkDockerSocket, getDockerSocketStatus]); - - return { - isEnabled, - isAvailable: isEnabled && isAvailable, - socketPath, - containers, - check: checkDockerSocket, - status: getDockerSocketStatus, - fetchContainers: getContainers - }; -} diff --git a/src/lib/docker.ts b/src/lib/docker.ts new file mode 100644 index 00000000..d463237b --- /dev/null +++ b/src/lib/docker.ts @@ -0,0 +1,136 @@ +import { createApiClient, formatAxiosError } from "@app/lib/api"; +import { + Container, + GetDockerStatusResponse, + ListContainersResponse, + TriggerFetchResponse +} from "@server/routers/site"; +import { AxiosResponse } from "axios"; + +const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + +export interface DockerState { + isEnabled: boolean; + isAvailable: boolean; + socketPath?: string; + containers: Container[]; +} + +export class DockerManager { + private api: any; + private siteId: number; + + constructor(api: any, siteId: number) { + this.api = api; + this.siteId = siteId; + } + + async checkDockerSocket(): Promise { + try { + const res = await this.api.post(`/site/${this.siteId}/docker/check`); + console.log("Docker socket check response:", res); + } catch (error) { + console.error("Failed to check Docker socket:", error); + } + } + + async getDockerSocketStatus(): Promise { + try { + const res = await this.api.get( + `/site/${this.siteId}/docker/status` + ); + + if (res.status === 200) { + return res.data.data as GetDockerStatusResponse; + } else { + console.error("Failed to get Docker status:", res); + return null; + } + } catch (error) { + console.error("Failed to get Docker status:", error); + return null; + } + } + + async fetchContainers(maxRetries: number = 3): Promise { + const fetchContainerList = async (): Promise => { + let attempt = 0; + while (attempt < maxRetries) { + try { + const res = await this.api.get( + `/site/${this.siteId}/docker/containers` + ); + return res.data.data as Container[]; + } catch (error: any) { + attempt++; + + // Check if the error is a 425 (Too Early) status + if (error?.response?.status === 425) { + if (attempt < maxRetries) { + console.log( + `Containers not ready yet (attempt ${attempt}/${maxRetries}). Retrying in 250ms...` + ); + await sleep(250); + continue; + } else { + console.warn( + "Max retry attempts reached. Containers may still be loading." + ); + } + } else { + console.error( + "Failed to fetch Docker containers:", + error + ); + throw error; + } + break; + } + } + return []; + }; + + try { + await this.api.post( + `/site/${this.siteId}/docker/trigger` + ); + return await fetchContainerList(); + } catch (error) { + console.error("Failed to trigger Docker containers:", error); + return []; + } + } + + async initializeDocker(): Promise { + console.log(`Initializing Docker for site ID: ${this.siteId}`); + + // For now, assume Docker is enabled for newt sites + const isEnabled = true; + + if (!isEnabled) { + return { + isEnabled: false, + isAvailable: false, + containers: [] + }; + } + + // Check and get Docker socket status + await this.checkDockerSocket(); + const dockerStatus = await this.getDockerSocketStatus(); + + const isAvailable = dockerStatus?.isAvailable || false; + let containers: Container[] = []; + + if (isAvailable) { + containers = await this.fetchContainers(); + } + + return { + isEnabled, + isAvailable, + socketPath: dockerStatus?.socketPath, + containers + }; + } +} diff --git a/src/providers/ResourceProvider.tsx b/src/providers/ResourceProvider.tsx index 4541035a..da6aca87 100644 --- a/src/providers/ResourceProvider.tsx +++ b/src/providers/ResourceProvider.tsx @@ -3,20 +3,17 @@ import ResourceContext from "@app/contexts/resourceContext"; import { GetResourceAuthInfoResponse } from "@server/routers/resource"; import { GetResourceResponse } from "@server/routers/resource/getResource"; -import { GetSiteResponse } from "@server/routers/site"; import { useState } from "react"; import { useTranslations } from "next-intl"; interface ResourceProviderProps { children: React.ReactNode; resource: GetResourceResponse; - site: GetSiteResponse | null; authInfo: GetResourceAuthInfoResponse; } export function ResourceProvider({ children, - site, resource: serverResource, authInfo: serverAuthInfo }: ResourceProviderProps) { @@ -66,7 +63,7 @@ export function ResourceProvider({ return ( {children} From b56db41d0b71c6eeed1db089a66561119d5b8f3f Mon Sep 17 00:00:00 2001 From: Pallavi Date: Fri, 15 Aug 2025 21:23:07 +0530 Subject: [PATCH 37/38] add missing hostmeta export for PG schema --- server/db/pg/schema.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index 2ba10e3e..06540f5e 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -648,3 +648,4 @@ export type UserClient = InferSelectModel; export type RoleClient = InferSelectModel; export type OrgDomains = InferSelectModel; export type SetupToken = InferSelectModel; +export type HostMeta = InferSelectModel; From 200a7fcd40338da62e1c6740f6b06588dafd4c2c Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Fri, 15 Aug 2025 16:00:58 -0700 Subject: [PATCH 38/38] fix sidebar spacing --- src/components/SidebarNav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SidebarNav.tsx b/src/components/SidebarNav.tsx index 13bd87d3..7e8ad336 100644 --- a/src/components/SidebarNav.tsx +++ b/src/components/SidebarNav.tsx @@ -150,7 +150,7 @@ export function SidebarNav({ {section.heading}
)} -
+
{section.items.map((item) => { const hydratedHref = hydrateHref(item.href); const isActive = pathname.startsWith(hydratedHref);