Compare commits

...

6 Commits

Author SHA1 Message Date
miloschwartz
12aea2901d fix depreated zod warning 2026-01-26 14:11:03 -08:00
miloschwartz
5ff56467ea error response improvements to logo url 2026-01-26 14:00:22 -08:00
miloschwartz
3a8718a4b0 remove archive confirmtion on account devices dialog 2026-01-26 13:36:25 -08:00
Owen
37c4a7b690 Retry verify 2026-01-24 11:55:32 -08:00
Owen
b735e7c34d Fix #2314 2026-01-24 11:47:17 -08:00
Owen
5f85c3b3b8 Remove extra rebuild command 2026-01-24 11:35:45 -08:00
5 changed files with 118 additions and 94 deletions

View File

@@ -482,14 +482,32 @@ jobs:
echo "==> cosign sign (key) --recursive ${REF}" echo "==> cosign sign (key) --recursive ${REF}"
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}" cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
# Retry wrapper for verification to handle registry propagation delays
retry_verify() {
local cmd="$1"
local attempts=6
local delay=5
local i=1
until eval "$cmd"; do
if [ $i -ge $attempts ]; then
echo "Verification failed after $attempts attempts"
return 1
fi
echo "Verification not yet available. Retry $i/$attempts after ${delay}s..."
sleep $delay
i=$((i+1))
delay=$((delay*2))
# Cap the delay to avoid very long waits
if [ $delay -gt 60 ]; then delay=60; fi
done
return 0
}
echo "==> cosign verify (public key) ${REF}" echo "==> cosign verify (public key) ${REF}"
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${REF}' -o text"
echo "==> cosign verify (keyless policy) ${REF}" echo "==> cosign verify (keyless policy) ${REF}"
cosign verify \ retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text"
--certificate-oidc-issuer "${issuer}" \
--certificate-identity-regexp "${id_regex}" \
"${REF}" -o text
echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}" echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
done done

View File

@@ -37,27 +37,55 @@ const paramsSchema = z.strictObject({
const bodySchema = z.strictObject({ const bodySchema = z.strictObject({
logoUrl: z logoUrl: z
.union([ .union([
z.string().length(0), z.literal(""),
z.url().refine( z
async (url) => { .url("Must be a valid URL")
.superRefine(async (url, ctx) => {
try { try {
const response = await fetch(url); const response = await fetch(url, {
return ( method: "HEAD"
response.status === 200 && }).catch(() => {
( // If HEAD fails (CORS or method not allowed), try GET
response.headers.get("content-type") ?? "" return fetch(url, { method: "GET" });
).startsWith("image/") });
);
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) { } 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), logoWidth: z.coerce.number<number>().min(1),
logoHeight: z.coerce.number<number>().min(1), logoHeight: z.coerce.number<number>().min(1),
resourceTitle: z.string(), resourceTitle: z.string(),
@@ -78,7 +106,7 @@ export async function upsertLoginPageBranding(
next: NextFunction next: NextFunction
): Promise<any> { ): Promise<any> {
try { try {
const parsedBody = bodySchema.safeParse(req.body); const parsedBody = await bodySchema.safeParseAsync(req.body);
if (!parsedBody.success) { if (!parsedBody.success) {
return next( return next(
createHttpError( createHttpError(
@@ -117,9 +145,8 @@ export async function upsertLoginPageBranding(
typeof loginPageBranding typeof loginPageBranding
>; >;
if ((updateData.logoUrl ?? "").trim().length === 0) { // Empty strings are transformed to null by the schema, which will clear the logo URL in the database
updateData.logoUrl = undefined; // We keep it as null (not undefined) because undefined fields are omitted from Drizzle updates
}
if ( if (
build !== "saas" && build !== "saas" &&

View File

@@ -9,9 +9,6 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
import { sendTerminateClient } from "./terminate";
import { OlmErrorCodes } from "../olm/error";
const archiveClientSchema = z.strictObject({ const archiveClientSchema = z.strictObject({
clientId: z.string().transform(Number).pipe(z.int().positive()) clientId: z.string().transform(Number).pipe(z.int().positive())
@@ -77,9 +74,6 @@ export async function archiveClient(
.update(clients) .update(clients)
.set({ archived: true }) .set({ archived: true })
.where(eq(clients.clientId, clientId)); .where(eq(clients.clientId, clientId));
// Rebuild associations to clean up related data
await rebuildClientAssociationsFromClient(client, trx);
}); });
return response(res, { return response(res, {

View File

@@ -43,25 +43,52 @@ export type AuthPageCustomizationProps = {
const AuthPageFormSchema = z.object({ const AuthPageFormSchema = z.object({
logoUrl: z.union([ logoUrl: z.union([
z.string().length(0), z.literal(""),
z.url().refine( z.url("Must be a valid URL").superRefine(async (url, ctx) => {
async (url) => { try {
try { const response = await fetch(url, {
const response = await fetch(url); method: "HEAD"
return ( }).catch(() => {
response.status === 200 && // If HEAD fails (CORS or method not allowed), try GET
(response.headers.get("content-type") ?? "").startsWith( return fetch(url, { method: "GET" });
"image/" });
)
); if (response.status !== 200) {
} catch (error) { ctx.addIssue({
return false; code: "custom",
message: `Failed to load image. Please check that the URL is accessible.`
});
return;
} }
},
{ const contentType = response.headers.get("content-type") ?? "";
error: "Invalid logo URL, must be a valid image URL" 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), logoWidth: z.coerce.number<number>().min(1),
logoHeight: z.coerce.number<number>().min(1), logoHeight: z.coerce.number<number>().min(1),
@@ -405,9 +432,7 @@ export default function AuthPageBrandingForm({
<Button <Button
variant="destructive" variant="destructive"
type="submit" type="submit"
loading={ loading={isDeletingBranding}
isUpdatingBranding || isDeletingBranding
}
disabled={ disabled={
isUpdatingBranding || isUpdatingBranding ||
isDeletingBranding || isDeletingBranding ||
@@ -422,7 +447,7 @@ export default function AuthPageBrandingForm({
<Button <Button
type="submit" type="submit"
form="auth-page-branding-form" form="auth-page-branding-form"
loading={isUpdatingBranding || isDeletingBranding} loading={isUpdatingBranding}
disabled={ disabled={
isUpdatingBranding || isUpdatingBranding ||
isDeletingBranding || isDeletingBranding ||

View File

@@ -28,7 +28,6 @@ import {
TableRow TableRow
} from "@app/components/ui/table"; } from "@app/components/ui/table";
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@app/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@app/components/ui/tabs";
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
import { Loader2, RefreshCw } from "lucide-react"; import { Loader2, RefreshCw } from "lucide-react";
import moment from "moment"; import moment from "moment";
import { useUserContext } from "@app/hooks/useUserContext"; import { useUserContext } from "@app/hooks/useUserContext";
@@ -59,8 +58,6 @@ export default function ViewDevicesDialog({
const [devices, setDevices] = useState<Device[]>([]); const [devices, setDevices] = useState<Device[]>([]);
const [loading, setLoading] = useState(false); 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 [activeTab, setActiveTab] = useState<"available" | "archived">("available");
const fetchDevices = async () => { const fetchDevices = async () => {
@@ -108,8 +105,6 @@ export default function ViewDevicesDialog({
d.olmId === olmId ? { ...d, archived: true } : d d.olmId === olmId ? { ...d, archived: true } : d
) )
); );
setIsArchiveModalOpen(false);
setSelectedDevice(null);
} catch (error: any) { } catch (error: any) {
console.error("Error archiving device:", error); console.error("Error archiving device:", error);
toast({ toast({
@@ -153,8 +148,6 @@ export default function ViewDevicesDialog({
function reset() { function reset() {
setDevices([]); setDevices([]);
setSelectedDevice(null);
setIsArchiveModalOpen(false);
} }
return ( return (
@@ -263,12 +256,7 @@ export default function ViewDevicesDialog({
<Button <Button
variant="outline" variant="outline"
onClick={() => { onClick={() => {
setSelectedDevice( archiveDevice(device.olmId);
device
);
setIsArchiveModalOpen(
true
);
}} }}
> >
{t( {t(
@@ -361,34 +349,6 @@ export default function ViewDevicesDialog({
</CredenzaFooter> </CredenzaFooter>
</CredenzaContent> </CredenzaContent>
</Credenza> </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"}
/>
)}
</> </>
); );
} }