mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Compare commits
3 Commits
dependabot
...
12aea2901d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12aea2901d | ||
|
|
5ff56467ea | ||
|
|
3a8718a4b0 |
27
package-lock.json
generated
27
package-lock.json
generated
@@ -119,7 +119,7 @@
|
||||
"@dotenvx/dotenvx": "1.51.2",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
"@tanstack/react-query-devtools": "5.91.2",
|
||||
"@tanstack/react-query-devtools": "5.91.1",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"@types/cookie-parser": "1.4.10",
|
||||
"@types/cors": "2.8.19",
|
||||
@@ -146,7 +146,7 @@
|
||||
"esbuild": "0.27.2",
|
||||
"esbuild-node-externals": "1.20.1",
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.8.1",
|
||||
"prettier": "3.8.0",
|
||||
"react-email": "5.2.5",
|
||||
"tailwindcss": "4.1.18",
|
||||
"tsc-alias": "1.8.16",
|
||||
@@ -9326,9 +9326,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-devtools": {
|
||||
"version": "5.92.0",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.92.0.tgz",
|
||||
"integrity": "sha512-N8D27KH1vEpVacvZgJL27xC6yPFUy0Zkezn5gnB3L3gRCxlDeSuiya7fKge8Y91uMTnC8aSxBQhcK6ocY7alpQ==",
|
||||
"version": "5.91.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.91.1.tgz",
|
||||
"integrity": "sha512-l8bxjk6BMsCaVQH6NzQEE/bEgFy1hAs5qbgXl0xhzezlaQbPk6Mgz9BqEg2vTLPOHD8N4k+w/gdgCbEzecGyNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -9353,20 +9353,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query-devtools": {
|
||||
"version": "5.91.2",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.2.tgz",
|
||||
"integrity": "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg==",
|
||||
"version": "5.91.1",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.91.1.tgz",
|
||||
"integrity": "sha512-tRnJYwEbH0kAOuToy8Ew7bJw1lX3AjkkgSlf/vzb+NpnqmHPdWM+lA2DSdGQSLi1SU0PDRrrCI1vnZnci96CsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-devtools": "5.92.0"
|
||||
"@tanstack/query-devtools": "5.91.1"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-query": "^5.90.14",
|
||||
"@tanstack/react-query": "^5.90.10",
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
@@ -13944,6 +13944,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -20001,9 +20002,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.0.tgz",
|
||||
"integrity": "sha512-yEPsovQfpxYfgWNhCfECjG5AQaO+K3dp6XERmOepyPDVqcJm+bjyCVO3pmU+nAPe0N5dDvekfGezt/EIiRe1TA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
"@dotenvx/dotenvx": "1.51.2",
|
||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||
"@tailwindcss/postcss": "4.1.18",
|
||||
"@tanstack/react-query-devtools": "5.91.2",
|
||||
"@tanstack/react-query-devtools": "5.91.1",
|
||||
"@types/better-sqlite3": "7.6.13",
|
||||
"@types/cookie-parser": "1.4.10",
|
||||
"@types/cors": "2.8.19",
|
||||
@@ -170,7 +170,7 @@
|
||||
"esbuild": "0.27.2",
|
||||
"esbuild-node-externals": "1.20.1",
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.8.1",
|
||||
"prettier": "3.8.0",
|
||||
"react-email": "5.2.5",
|
||||
"tailwindcss": "4.1.18",
|
||||
"tsc-alias": "1.8.16",
|
||||
|
||||
@@ -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<number>().min(1),
|
||||
logoHeight: z.coerce.number<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" &&
|
||||
|
||||
@@ -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<number>().min(1),
|
||||
logoHeight: z.coerce.number<number>().min(1),
|
||||
@@ -405,9 +432,7 @@ export default function AuthPageBrandingForm({
|
||||
<Button
|
||||
variant="destructive"
|
||||
type="submit"
|
||||
loading={
|
||||
isUpdatingBranding || isDeletingBranding
|
||||
}
|
||||
loading={isDeletingBranding}
|
||||
disabled={
|
||||
isUpdatingBranding ||
|
||||
isDeletingBranding ||
|
||||
@@ -422,7 +447,7 @@ export default function AuthPageBrandingForm({
|
||||
<Button
|
||||
type="submit"
|
||||
form="auth-page-branding-form"
|
||||
loading={isUpdatingBranding || isDeletingBranding}
|
||||
loading={isUpdatingBranding}
|
||||
disabled={
|
||||
isUpdatingBranding ||
|
||||
isDeletingBranding ||
|
||||
|
||||
@@ -28,7 +28,6 @@ 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";
|
||||
@@ -59,8 +58,6 @@ 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 () => {
|
||||
@@ -108,8 +105,6 @@ 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({
|
||||
@@ -153,8 +148,6 @@ export default function ViewDevicesDialog({
|
||||
|
||||
function reset() {
|
||||
setDevices([]);
|
||||
setSelectedDevice(null);
|
||||
setIsArchiveModalOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -263,12 +256,7 @@ export default function ViewDevicesDialog({
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSelectedDevice(
|
||||
device
|
||||
);
|
||||
setIsArchiveModalOpen(
|
||||
true
|
||||
);
|
||||
archiveDevice(device.olmId);
|
||||
}}
|
||||
>
|
||||
{t(
|
||||
@@ -361,34 +349,6 @@ 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"}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user