diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 196676e9..685be384 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -44,19 +44,9 @@ updates: schedule: interval: "daily" groups: - dev-patch-updates: - dependency-type: "development" + patch-updates: update-types: - "patch" - dev-minor-updates: - dependency-type: "development" + minor-updates: update-types: - "minor" - prod-patch-updates: - dependency-type: "production" - update-types: - - "patch" - prod-minor-updates: - dependency-type: "production" - update-types: - - "minor" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 07371f77..487fa033 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,21 +1,11 @@ FROM node:24-alpine AS builder -# 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++ +RUN apk add --no-cache python3 make g++ # COPY package.json package-lock.json ./ COPY package*.json ./ @@ -23,41 +13,31 @@ 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 +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 -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 +# OCI Image Labels - Build Args for dynamic values +ARG VERSION="dev" +ARG REVISION="" +ARG CREATED="" +ARG LICENSE="AGPL-3.0" + +# 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" + WORKDIR /app # Only curl and tzdata needed at runtime - no build tools! @@ -66,11 +46,10 @@ 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/init ./dist/init +COPY --from=builder /app/server/migrations ./dist/init COPY --from=builder /app/package.json ./package.json COPY ./cli/wrapper.sh /usr/local/bin/pangctl diff --git a/package-lock.json b/package-lock.json index 6e1f9c46..5c8eac1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,10 +51,8 @@ "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", - "cookie": "1.1.1", "cookie-parser": "1.4.7", - "cookies": "0.9.1", - "cors": "2.8.6", + "cors": "2.8.5", "crypto-js": "4.2.0", "d3": "7.9.0", "date-fns": "4.1.0", @@ -66,7 +64,6 @@ "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", @@ -80,10 +77,7 @@ "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", - "node-fetch": "3.3.2", - "nodemailer": "7.0.13", - "npm": "11.8.0", - "nprogress": "0.2.0", + "nodemailer": "7.0.11", "oslo": "1.2.1", "pg": "8.18.0", "posthog-node": "5.24.7", @@ -94,7 +88,6 @@ "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.9.1", @@ -11026,19 +11019,6 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "license": "MIT" }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/cookie-parser": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", @@ -11067,19 +11047,6 @@ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "license": "MIT" }, - "node_modules/cookies": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", - "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "keygrip": "~1.1.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -11561,15 +11528,6 @@ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", "license": "BSD-2-Clause" }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -13804,29 +13762,6 @@ "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", "license": "MIT" }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -14030,18 +13965,6 @@ "node": ">= 0.6" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -14553,14 +14476,6 @@ "node": ">=10.17.0" } }, - "node_modules/i": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/i/-/i-0.3.7.tgz", - "integrity": "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==", - "engines": { - "node": ">=0.4" - } - }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -15351,19 +15266,6 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/keygrip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", - "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "license": "MIT", - "dependencies": { - "tsscmp": "1.0.6" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -16430,44 +16332,6 @@ "node": ">= 8.0.0" } }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/node-releases": { "version": "2.0.27", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", @@ -18662,15 +18526,6 @@ "yaml": "^2.8.0" } }, - "node_modules/optimist": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", - "integrity": "sha512-TCx0dXQzVtSCg2OgY/bO9hjM9cV4XYx09TVK+s3+FhkjT6LovsLe+pPMzpWf+6yXK/hUizs2gUoTw3jHM0VaTQ==", - "license": "MIT/X11", - "dependencies": { - "wordwrap": "~0.0.2" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -20657,20 +20512,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/rebuild": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/rebuild/-/rebuild-0.1.2.tgz", - "integrity": "sha512-EtDZ5IapND57htCrOOcfH7MzXCQKivzSZUIZIuc8H0xDHfmi9HDBZIyjT7Neh5GcUoxQ6hfsXluC+UrYLgGbZg==", - "dependencies": { - "optimist": "0.3.x" - }, - "bin": { - "rebuild": "cli.js" - }, - "engines": { - "node": ">=0.8.8" - } - }, "node_modules/recharts": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", @@ -22315,15 +22156,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tsscmp": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", - "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", - "license": "MIT", - "engines": { - "node": ">=0.6.x" - } - }, "node_modules/tsx": { "version": "4.21.0", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", @@ -22804,15 +22636,6 @@ "integrity": "sha512-jHl/NQgASfw5ZML3cnbjdfr/gXK5zO8a2xKSoCVe+5+EsIaO9tMTh7SsnfhESnCpZ+Xb6XBeU91wiuyERUPshQ==", "license": "BSD-3-Clause" }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, "node_modules/when-exit": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", @@ -22985,15 +22808,6 @@ "node": ">=0.10.0" } }, - "node_modules/wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", diff --git a/package.json b/package.json index 38a98982..61b2deaf 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "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", @@ -24,12 +26,13 @@ "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", - "next:build": "next build", + "build:next": "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": { @@ -75,10 +78,8 @@ "class-variance-authority": "0.7.1", "clsx": "2.1.1", "cmdk": "1.1.1", - "cookie": "1.1.1", "cookie-parser": "1.4.7", - "cookies": "0.9.1", - "cors": "2.8.6", + "cors": "2.8.5", "crypto-js": "4.2.0", "d3": "7.9.0", "date-fns": "4.1.0", @@ -90,7 +91,6 @@ "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", @@ -104,10 +104,7 @@ "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", - "node-fetch": "3.3.2", - "nodemailer": "7.0.13", - "npm": "11.8.0", - "nprogress": "0.2.0", + "nodemailer": "7.0.11", "oslo": "1.2.1", "pg": "8.18.0", "posthog-node": "5.24.7", @@ -118,7 +115,6 @@ "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.9.1", diff --git a/server/private/routers/loginPage/upsertLoginPageBranding.ts b/server/private/routers/loginPage/upsertLoginPageBranding.ts index 8782db3d..e6e365be 100644 --- a/server/private/routers/loginPage/upsertLoginPageBranding.ts +++ b/server/private/routers/loginPage/upsertLoginPageBranding.ts @@ -37,27 +37,55 @@ const paramsSchema = z.strictObject({ const bodySchema = z.strictObject({ logoUrl: z .union([ - z.string().length(0), - z.url().refine( - async (url) => { + z.literal(""), + z + .url("Must be a valid URL") + .superRefine(async (url, ctx) => { try { - const response = await fetch(url); - return ( - response.status === 200 && - ( - response.headers.get("content-type") ?? "" - ).startsWith("image/") - ); + 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; + } } catch (error) { - return false; + 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" - } - ) + }) ]) - .optional(), + .transform((val) => (val === "" ? null : val)) + .nullish(), logoWidth: z.coerce.number().min(1), logoHeight: z.coerce.number().min(1), resourceTitle: z.string(), @@ -117,9 +145,8 @@ export async function upsertLoginPageBranding( typeof loginPageBranding >; - if ((updateData.logoUrl ?? "").trim().length === 0) { - updateData.logoUrl = undefined; - } + // 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 ( build !== "saas" && diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index 138a286c..66a6432f 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -1,6 +1,6 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; -import { db, olms } from "@server/db"; +import { db, olms, users } from "@server/db"; import { clients, currentFingerprint } from "@server/db"; import { eq, and } from "drizzle-orm"; import response from "@server/lib/response"; @@ -36,6 +36,7 @@ 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) { @@ -48,6 +49,7 @@ 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; } @@ -207,6 +209,9 @@ 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; @@ -322,6 +327,9 @@ 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 }; diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index db156c2c..e4bb6f4f 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -13,6 +13,7 @@ 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"; @@ -97,6 +98,21 @@ 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) diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx index 4d070113..7bf563f4 100644 --- a/src/components/AuthPageBrandingForm.tsx +++ b/src/components/AuthPageBrandingForm.tsx @@ -43,25 +43,52 @@ export type AuthPageCustomizationProps = { const AuthPageFormSchema = z.object({ logoUrl: z.union([ - 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; + 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; } - }, - { - error: "Invalid logo URL, must be a valid image URL" + + 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 + }); } - ) + }) ]), logoWidth: z.coerce.number().min(1), logoHeight: z.coerce.number().min(1), @@ -405,9 +432,7 @@ export default function AuthPageBrandingForm({