Compare commits

..

4 Commits

Author SHA1 Message Date
dependabot[bot]
7e689ac90d Bump the prod-patch-updates group across 1 directory with 12 updates
Bumps the prod-patch-updates group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@react-email/components](https://github.com/resend/react-email/tree/HEAD/packages/components) | `1.0.2` | `1.0.6` |
| [@react-email/render](https://github.com/resend/react-email/tree/HEAD/packages/render) | `2.0.0` | `2.0.4` |
| [@react-email/tailwind](https://github.com/resend/react-email/tree/HEAD/packages/tailwind) | `2.0.2` | `2.0.3` |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.90.12` | `5.90.20` |
| [axios](https://github.com/axios/axios) | `1.13.2` | `1.13.4` |
| [cors](https://github.com/expressjs/cors) | `2.8.5` | `2.8.6` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `16.1.0` | `16.1.6` |
| [maxmind](https://github.com/runk/node-maxmind) | `5.0.1` | `5.0.5` |
| [nodemailer](https://github.com/nodemailer/nodemailer) | `7.0.11` | `7.0.13` |
| [react](https://github.com/facebook/react/tree/HEAD/packages/react) | `19.2.3` | `19.2.4` |
| [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) | `19.2.3` | `19.2.4` |
| [zod](https://github.com/colinhacks/zod) | `4.3.5` | `4.3.6` |



Updates `@react-email/components` from 1.0.2 to 1.0.6
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/components/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/components@1.0.6/packages/components)

Updates `@react-email/render` from 2.0.0 to 2.0.4
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/render/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/render@2.0.4/packages/render)

Updates `@react-email/tailwind` from 2.0.2 to 2.0.3
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/tailwind/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/tailwind@2.0.3/packages/tailwind)

Updates `@tanstack/react-query` from 5.90.12 to 5.90.20
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.90.20/packages/react-query)

Updates `axios` from 1.13.2 to 1.13.4
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.2...v1.13.4)

Updates `cors` from 2.8.5 to 2.8.6
- [Release notes](https://github.com/expressjs/cors/releases)
- [Changelog](https://github.com/expressjs/cors/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/cors/compare/v2.8.5...v2.8.6)

Updates `eslint-config-next` from 16.1.0 to 16.1.6
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/commits/v16.1.6/packages/eslint-config-next)

Updates `maxmind` from 5.0.1 to 5.0.5
- [Release notes](https://github.com/runk/node-maxmind/releases)
- [Commits](https://github.com/runk/node-maxmind/compare/v5.0.1...v5.0.5)

Updates `nodemailer` from 7.0.11 to 7.0.13
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v7.0.11...v7.0.13)

Updates `react` from 19.2.3 to 19.2.4
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.4/packages/react)

Updates `react-dom` from 19.2.3 to 19.2.4
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.4/packages/react-dom)

Updates `zod` from 4.3.5 to 4.3.6
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.3.5...v4.3.6)

---
updated-dependencies:
- dependency-name: "@react-email/components"
  dependency-version: 1.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@react-email/render"
  dependency-version: 2.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@react-email/tailwind"
  dependency-version: 2.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.90.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: axios
  dependency-version: 1.13.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: cors
  dependency-version: 2.8.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: eslint-config-next
  dependency-version: 16.1.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: maxmind
  dependency-version: 5.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: nodemailer
  dependency-version: 7.0.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react
  dependency-version: 19.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: react-dom
  dependency-version: 19.2.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: zod
  dependency-version: 4.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 01:53:34 +00:00
MoweME
b0566d3c6f fix(i18n): correct German site terminology
Updates the German translation to use "Standort" (site) instead of "Seite" (page) for consistency with the site context.
2026-01-29 10:01:30 -08:00
MoweME
5dda8c384f fix(i18n): correct German translation strings
Corrects mistranslation of device timestamp labels and fixes product name reference in site tunnel settings.
2026-01-29 10:01:30 -08:00
Owen
cb569ff14d Properly insert PANGOLIN_SETUP_TOKEN into db
Fixes #2361
2026-01-28 15:03:31 -08:00
13 changed files with 2631 additions and 1046 deletions

View File

@@ -44,9 +44,19 @@ updates:
schedule:
interval: "daily"
groups:
patch-updates:
dev-patch-updates:
dependency-type: "development"
update-types:
- "patch"
minor-updates:
dev-minor-updates:
dependency-type: "development"
update-types:
- "minor"
prod-patch-updates:
dependency-type: "production"
update-types:
- "patch"
prod-minor-updates:
dependency-type: "production"
update-types:
- "minor"

View File

@@ -1,43 +1,63 @@
FROM node:24-alpine AS builder
WORKDIR /app
ARG BUILD=oss
ARG DATABASE=sqlite
RUN apk add --no-cache python3 make g++
# COPY package.json package-lock.json ./
COPY package*.json ./
RUN npm ci
COPY . .
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \
npm run set:$DATABASE && \
npm run set:$BUILD && \
npm run db:$DATABASE:generate && \
npm run build:$DATABASE && \
npm run build:cli
# test to make sure the build output is there and error if not
RUN test -f dist/server.mjs
# Prune dev dependencies and clean up to prepare for copy to runner
RUN npm prune --omit=dev && npm cache clean --force
FROM node:24-alpine AS runner
# OCI Image Labels - Build Args for dynamic values
ARG VERSION="dev"
ARG REVISION=""
ARG CREATED=""
ARG LICENSE="AGPL-3.0"
WORKDIR /app
ARG BUILD=oss
ARG DATABASE=sqlite
# Derive title and description based on BUILD type
ARG IMAGE_TITLE="Pangolin"
ARG IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
RUN apk add --no-cache curl tzdata python3 make g++
# COPY package.json package-lock.json ./
COPY package*.json ./
RUN npm ci
COPY . .
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
RUN echo "export const driver: \"pg\" | \"sqlite\" = \"$DATABASE\";" >> server/db/index.ts
RUN echo "export const build = \"$BUILD\" as \"saas\" | \"enterprise\" | \"oss\";" > server/build.ts
# Copy the appropriate TypeScript configuration based on build type
RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \
elif [ "$BUILD" = "saas" ]; then cp tsconfig.saas.json tsconfig.json; \
elif [ "$BUILD" = "enterprise" ]; then cp tsconfig.enterprise.json tsconfig.json; \
fi
# if the build is oss then remove the server/private directory
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi
RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema --out init; fi
RUN mkdir -p dist
RUN npm run next:build
RUN node esbuild.mjs -e server/index.ts -o dist/server.mjs -b $BUILD
RUN if [ "$DATABASE" = "pg" ]; then \
node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs; \
else \
node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs; \
fi
# test to make sure the build output is there and error if not
RUN test -f dist/server.mjs
RUN npm run build:cli
# Prune dev dependencies and clean up to prepare for copy to runner
RUN npm prune --omit=dev && npm cache clean --force
FROM node:24-alpine AS runner
WORKDIR /app
# Only curl and tzdata needed at runtime - no build tools!
@@ -46,10 +66,11 @@ RUN apk add --no-cache curl tzdata
# Copy pre-built node_modules from builder (already pruned to production only)
# This includes the compiled native modules like better-sqlite3
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/server/migrations ./dist/init
COPY --from=builder /app/init ./dist/init
COPY --from=builder /app/package.json ./package.json
COPY ./cli/wrapper.sh /usr/local/bin/pangctl

View File

@@ -97,7 +97,7 @@
"siteGeneralDescription": "Allgemeine Einstellungen für diesen Standort konfigurieren",
"siteSettingDescription": "Standorteinstellungen konfigurieren",
"siteSetting": "{siteName} Einstellungen",
"siteNewtTunnel": "Neuer Standort (empfohlen)",
"siteNewtTunnel": "Newt Standort (empfohlen)",
"siteNewtTunnelDescription": "Einfachster Weg, einen Einstiegspunkt in jedes Netzwerk zu erstellen. Keine zusätzliche Einrichtung.",
"siteWg": "Einfacher WireGuard Tunnel",
"siteWgDescription": "Verwende jeden WireGuard-Client, um einen Tunnel einzurichten. Manuelles NAT-Setup erforderlich.",
@@ -107,7 +107,7 @@
"siteSeeAll": "Alle Standorte anzeigen",
"siteTunnelDescription": "Legen Sie fest, wie Sie sich mit dem Standort verbinden möchten",
"siteNewtCredentials": "Zugangsdaten",
"siteNewtCredentialsDescription": "So wird sich die Seite mit dem Server authentifizieren",
"siteNewtCredentialsDescription": "So wird sich der Standort mit dem Server authentifizieren",
"remoteNodeCredentialsDescription": "So wird sich der entfernte Node mit dem Server authentifizieren",
"siteCredentialsSave": "Anmeldedaten speichern",
"siteCredentialsSaveDescription": "Du kannst das nur einmal sehen. Stelle sicher, dass du es an einen sicheren Ort kopierst.",
@@ -2503,7 +2503,7 @@
"deviceModel": "Gerätemodell",
"serialNumber": "Seriennummer",
"hostname": "Hostname",
"firstSeen": "Erster Blick",
"firstSeen": "Zuerst gesehen",
"lastSeen": "Zuletzt gesehen",
"biometricsEnabled": "Biometrie aktiviert",
"diskEncrypted": "Festplatte verschlüsselt",

3275
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,8 +12,6 @@
"license": "SEE LICENSE IN LICENSE AND README.md",
"scripts": {
"dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts",
"dev:check": "npx tsc --noEmit && npm run format:check",
"dev:setup": "cp config/config.example.yml config/config.yml && npm run set:oss && npm run set:sqlite && npm run db:sqlite:generate && npm run db:sqlite:push",
"db:pg:generate": "drizzle-kit generate --config=./drizzle.pg.config.ts",
"db:sqlite:generate": "drizzle-kit generate --config=./drizzle.sqlite.config.ts",
"db:pg:push": "npx tsx server/db/pg/migrate.ts",
@@ -26,13 +24,12 @@
"set:enterprise": "echo 'export const build = \"enterprise\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
"set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts",
"set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts",
"build:next": "next build",
"next:build": "next build",
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
"start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs",
"email": "email dev --dir server/emails/templates --port 3005",
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs",
"format:check": "prettier --check .",
"format": "prettier --write ."
},
"dependencies": {
@@ -63,58 +60,65 @@
"@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.15",
"@radix-ui/react-tooltip": "1.2.8",
"@react-email/components": "1.0.2",
"@react-email/render": "2.0.0",
"@react-email/tailwind": "2.0.2",
"@react-email/components": "1.0.6",
"@react-email/render": "2.0.4",
"@react-email/tailwind": "2.0.3",
"@simplewebauthn/browser": "13.2.2",
"@simplewebauthn/server": "13.2.2",
"@tailwindcss/forms": "0.5.11",
"@tanstack/react-query": "5.90.12",
"@tanstack/react-query": "5.90.20",
"@tanstack/react-table": "8.21.3",
"arctic": "3.7.0",
"axios": "1.13.2",
"axios": "1.13.4",
"better-sqlite3": "11.9.1",
"canvas-confetti": "1.9.4",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cmdk": "1.1.1",
"cookie": "1.1.1",
"cookie-parser": "1.4.7",
"cors": "2.8.5",
"cookies": "0.9.1",
"cors": "2.8.6",
"crypto-js": "4.2.0",
"d3": "7.9.0",
"date-fns": "4.1.0",
"drizzle-orm": "0.45.1",
"eslint": "9.39.2",
"eslint-config-next": "16.1.0",
"eslint-config-next": "16.1.6",
"express": "5.2.1",
"express-rate-limit": "8.2.1",
"glob": "13.0.0",
"helmet": "8.1.0",
"http-errors": "2.0.1",
"i": "0.3.7",
"input-otp": "1.4.2",
"ioredis": "5.9.2",
"jmespath": "0.16.0",
"js-yaml": "4.1.1",
"jsonwebtoken": "9.0.3",
"lucide-react": "0.562.0",
"maxmind": "5.0.1",
"maxmind": "5.0.5",
"moment": "2.30.1",
"next": "15.5.9",
"next-intl": "4.7.0",
"next-themes": "0.4.6",
"nextjs-toploader": "3.9.17",
"node-cache": "5.1.2",
"nodemailer": "7.0.11",
"node-fetch": "3.3.2",
"nodemailer": "7.0.13",
"npm": "11.7.0",
"nprogress": "0.2.0",
"oslo": "1.2.1",
"pg": "8.17.1",
"posthog-node": "5.23.0",
"qrcode.react": "4.2.0",
"react": "19.2.3",
"react": "19.2.4",
"react-day-picker": "9.13.0",
"react-dom": "19.2.3",
"react-dom": "19.2.4",
"react-easy-sort": "1.8.0",
"react-hook-form": "7.71.1",
"react-icons": "5.5.0",
"rebuild": "0.1.2",
"recharts": "2.15.4",
"reodotdev": "1.0.0",
"resend": "6.8.0",
@@ -132,7 +136,7 @@
"ws": "8.19.0",
"yaml": "2.8.2",
"yargs": "18.0.0",
"zod": "4.3.5",
"zod": "4.3.6",
"zod-validation-error": "5.0.0"
},
"devDependencies": {
@@ -150,10 +154,10 @@
"@types/jmespath": "0.15.2",
"@types/jsonwebtoken": "9.0.10",
"@types/node": "24.10.2",
"@types/nodemailer": "7.0.4",
"@types/nodemailer": "7.0.9",
"@types/nprogress": "0.2.3",
"@types/pg": "8.16.0",
"@types/react": "19.2.7",
"@types/react": "19.2.10",
"@types/react-dom": "19.2.3",
"@types/semver": "7.7.1",
"@types/swagger-ui-express": "4.1.8",

View File

@@ -37,55 +37,27 @@ const paramsSchema = z.strictObject({
const bodySchema = z.strictObject({
logoUrl: z
.union([
z.literal(""),
z
.url("Must be a valid URL")
.superRefine(async (url, ctx) => {
z.string().length(0),
z.url().refine(
async (url) => {
try {
const response = await fetch(url, {
method: "HEAD"
}).catch(() => {
// If HEAD fails (CORS or method not allowed), try GET
return fetch(url, { method: "GET" });
});
if (response.status !== 200) {
ctx.addIssue({
code: "custom",
message: `Failed to load image. Please check that the URL is accessible.`
});
return;
}
const contentType =
response.headers.get("content-type") ?? "";
if (!contentType.startsWith("image/")) {
ctx.addIssue({
code: "custom",
message: `URL does not point to an image. Please provide a URL to an image file (e.g., .png, .jpg, .svg).`
});
return;
}
const response = await fetch(url);
return (
response.status === 200 &&
(
response.headers.get("content-type") ?? ""
).startsWith("image/")
);
} catch (error) {
let errorMessage =
"Unable to verify image URL. Please check that the URL is accessible and points to an image file.";
if (error instanceof TypeError && error.message.includes("fetch")) {
errorMessage =
"Network error: Unable to reach the URL. Please check your internet connection and verify the URL is correct.";
} else if (error instanceof Error) {
errorMessage = `Error verifying URL: ${error.message}`;
}
ctx.addIssue({
code: "custom",
message: errorMessage
});
return false;
}
})
},
{
error: "Invalid logo URL, must be a valid image URL"
}
)
])
.transform((val) => (val === "" ? null : val))
.nullish(),
.optional(),
logoWidth: z.coerce.number<number>().min(1),
logoHeight: z.coerce.number<number>().min(1),
resourceTitle: z.string(),
@@ -145,8 +117,9 @@ export async function upsertLoginPageBranding(
typeof loginPageBranding
>;
// Empty strings are transformed to null by the schema, which will clear the logo URL in the database
// We keep it as null (not undefined) because undefined fields are omitted from Drizzle updates
if ((updateData.logoUrl ?? "").trim().length === 0) {
updateData.logoUrl = undefined;
}
if (
build !== "saas" &&

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, olms, users } from "@server/db";
import { db, olms } from "@server/db";
import { clients, currentFingerprint } from "@server/db";
import { eq, and } from "drizzle-orm";
import response from "@server/lib/response";
@@ -36,7 +36,6 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
currentFingerprint,
eq(olms.olmId, currentFingerprint.olmId)
)
.leftJoin(users, eq(clients.userId, users.userId))
.limit(1);
return res;
} else if (niceId && orgId) {
@@ -49,7 +48,6 @@ async function query(clientId?: number, niceId?: string, orgId?: string) {
currentFingerprint,
eq(olms.olmId, currentFingerprint.olmId)
)
.leftJoin(users, eq(clients.userId, users.userId))
.limit(1);
return res;
}
@@ -209,9 +207,6 @@ export type GetClientResponse = NonNullable<
olmId: string | null;
agent: string | null;
olmVersion: string | null;
userEmail: string | null;
userName: string | null;
userUsername: string | null;
fingerprint: {
username: string | null;
hostname: string | null;
@@ -327,9 +322,6 @@ export async function getClient(
olmId: client.olms ? client.olms.olmId : null,
agent: client.olms?.agent || null,
olmVersion: client.olms?.version || null,
userEmail: client.user?.email ?? null,
userName: client.user?.name ?? null,
userUsername: client.user?.username ?? null,
fingerprint: fingerprintData,
posture: postureData
};

View File

@@ -13,7 +13,6 @@ import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy";
import { validateSessionToken } from "@server/auth/sessions/app";
import { encodeHexLowerCase } from "@oslojs/encoding";
import { sha256 } from "@oslojs/crypto/sha2";
import { getUserDeviceName } from "@server/db/names";
import { buildSiteConfigurationForOlmClient } from "./buildConfiguration";
import { OlmErrorCodes, sendOlmError } from "./error";
import { handleFingerprintInsertion } from "./fingerprintingUtils";
@@ -98,21 +97,6 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return;
}
const deviceModel = fingerprint?.deviceModel ?? null;
const computedName = getUserDeviceName(deviceModel, client.name);
if (computedName && computedName !== client.name) {
await db
.update(clients)
.set({ name: computedName })
.where(eq(clients.clientId, client.clientId));
}
if (computedName && computedName !== olm.name) {
await db
.update(olms)
.set({ name: computedName })
.where(eq(olms.olmId, olm.olmId));
}
const [org] = await db
.select()
.from(orgs)

View File

@@ -43,52 +43,25 @@ export type AuthPageCustomizationProps = {
const AuthPageFormSchema = z.object({
logoUrl: z.union([
z.literal(""),
z.url("Must be a valid URL").superRefine(async (url, ctx) => {
try {
const response = await fetch(url, {
method: "HEAD"
}).catch(() => {
// If HEAD fails (CORS or method not allowed), try GET
return fetch(url, { method: "GET" });
});
if (response.status !== 200) {
ctx.addIssue({
code: "custom",
message: `Failed to load image. Please check that the URL is accessible.`
});
return;
z.string().length(0),
z.url().refine(
async (url) => {
try {
const response = await fetch(url);
return (
response.status === 200 &&
(response.headers.get("content-type") ?? "").startsWith(
"image/"
)
);
} catch (error) {
return false;
}
const contentType = response.headers.get("content-type") ?? "";
if (!contentType.startsWith("image/")) {
ctx.addIssue({
code: "custom",
message: `URL does not point to an image. Please provide a URL to an image file (e.g., .png, .jpg, .svg).`
});
return;
}
} catch (error) {
let errorMessage =
"Unable to verify image URL. Please check that the URL is accessible and points to an image file.";
if (
error instanceof TypeError &&
error.message.includes("fetch")
) {
errorMessage =
"Network error: Unable to reach the URL. Please check your internet connection and verify the URL is correct.";
} else if (error instanceof Error) {
errorMessage = `Error verifying URL: ${error.message}`;
}
ctx.addIssue({
code: "custom",
message: errorMessage
});
},
{
error: "Invalid logo URL, must be a valid image URL"
}
})
)
]),
logoWidth: z.coerce.number<number>().min(1),
logoHeight: z.coerce.number<number>().min(1),
@@ -432,7 +405,9 @@ export default function AuthPageBrandingForm({
<Button
variant="destructive"
type="submit"
loading={isDeletingBranding}
loading={
isUpdatingBranding || isDeletingBranding
}
disabled={
isUpdatingBranding ||
isDeletingBranding ||
@@ -447,7 +422,7 @@ export default function AuthPageBrandingForm({
<Button
type="submit"
form="auth-page-branding-form"
loading={isUpdatingBranding}
loading={isUpdatingBranding || isDeletingBranding}
disabled={
isUpdatingBranding ||
isDeletingBranding ||

View File

@@ -8,7 +8,6 @@ import {
InfoSections,
InfoSectionTitle
} from "@app/components/InfoSection";
import { getUserDisplayName } from "@app/lib/getUserDisplayName";
import { useTranslations } from "next-intl";
type ClientInfoCardProps = {};
@@ -17,12 +16,6 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
const { client, updateClient } = useClientContext();
const t = useTranslations();
const userDisplayName = getUserDisplayName({
email: client.userEmail,
name: client.userName,
username: client.userUsername
});
return (
<Alert>
<AlertDescription>
@@ -32,12 +25,8 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
<InfoSectionContent>{client.name}</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{userDisplayName ? t("user") : t("identifier")}
</InfoSectionTitle>
<InfoSectionContent>
{userDisplayName || client.niceId}
</InfoSectionContent>
<InfoSectionTitle>{t("identifier")}</InfoSectionTitle>
<InfoSectionContent>{client.niceId}</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>{t("status")}</InfoSectionTitle>

View File

@@ -1,13 +1,41 @@
"use client";
import * as React from "react";
import * as NProgress from "nprogress";
import NextTopLoader from "nextjs-toploader";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
export function TopLoader() {
return (
<NextTopLoader
color="var(--color-primary)"
showSpinner={false}
height={2}
/>
);
return (
<>
<NextTopLoader showSpinner={false} color="var(--color-primary)" />
<FinishingLoader />
</>
);
}
function FinishingLoader() {
const pathname = usePathname();
const router = useRouter();
const searchParams = useSearchParams();
React.useEffect(() => {
NProgress.done();
}, [pathname, router, searchParams]);
React.useEffect(() => {
const linkClickListener = (ev: MouseEvent) => {
const element = ev.target as HTMLElement;
const closestlink = element.closest("a");
const isOpenToNewTabClick =
ev.ctrlKey ||
ev.shiftKey ||
ev.metaKey || // apple
(ev.button && ev.button == 1); // middle click, >IE9 + everyone else
if (closestlink && isOpenToNewTabClick) {
NProgress.done();
}
};
window.addEventListener("click", linkClickListener);
return () => window.removeEventListener("click", linkClickListener);
}, []);
return null;
}

View File

@@ -28,6 +28,7 @@ import {
TableRow
} from "@app/components/ui/table";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@app/components/ui/tabs";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { Loader2, RefreshCw } from "lucide-react";
import moment from "moment";
import { useUserContext } from "@app/hooks/useUserContext";
@@ -58,6 +59,8 @@ export default function ViewDevicesDialog({
const [devices, setDevices] = useState<Device[]>([]);
const [loading, setLoading] = useState(false);
const [isArchiveModalOpen, setIsArchiveModalOpen] = useState(false);
const [selectedDevice, setSelectedDevice] = useState<Device | null>(null);
const [activeTab, setActiveTab] = useState<"available" | "archived">("available");
const fetchDevices = async () => {
@@ -105,6 +108,8 @@ export default function ViewDevicesDialog({
d.olmId === olmId ? { ...d, archived: true } : d
)
);
setIsArchiveModalOpen(false);
setSelectedDevice(null);
} catch (error: any) {
console.error("Error archiving device:", error);
toast({
@@ -148,6 +153,8 @@ export default function ViewDevicesDialog({
function reset() {
setDevices([]);
setSelectedDevice(null);
setIsArchiveModalOpen(false);
}
return (
@@ -256,7 +263,12 @@ export default function ViewDevicesDialog({
<Button
variant="outline"
onClick={() => {
archiveDevice(device.olmId);
setSelectedDevice(
device
);
setIsArchiveModalOpen(
true
);
}}
>
{t(
@@ -349,6 +361,34 @@ export default function ViewDevicesDialog({
</CredenzaFooter>
</CredenzaContent>
</Credenza>
{selectedDevice && (
<ConfirmDeleteDialog
open={isArchiveModalOpen}
setOpen={(val) => {
setIsArchiveModalOpen(val);
if (!val) {
setSelectedDevice(null);
}
}}
dialog={
<div className="space-y-2">
<p>
{t("deviceQuestionArchive") ||
"Are you sure you want to archive this device?"}
</p>
<p>
{t("deviceMessageArchive") ||
"The device will be archived and removed from your active devices list."}
</p>
</div>
}
buttonText={t("deviceArchiveConfirm") || "Archive Device"}
onConfirm={async () => archiveDevice(selectedDevice.olmId)}
string={selectedDevice.name || selectedDevice.olmId}
title={t("archiveDevice") || "Archive Device"}
/>
)}
</>
);
}

View File

@@ -91,7 +91,7 @@ export function NewtSiteInstallCommands({
- NEWT_SECRET=${secret}${acceptClientsEnv}`
],
"Docker Run": [
`docker run -dit --network host fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
`docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
]
},
kubernetes: {