♻️ make logo URL optional

This commit is contained in:
Fred KISSIE
2026-01-20 02:45:39 +01:00
committed by Owen Schwartz
parent c92b5942fc
commit b7df8b7319
7 changed files with 58 additions and 23 deletions

View File

@@ -214,7 +214,7 @@ export const loginPageOrg = pgTable("loginPageOrg", {
export const loginPageBranding = pgTable("loginPageBranding", { export const loginPageBranding = pgTable("loginPageBranding", {
loginPageBrandingId: serial("loginPageBrandingId").primaryKey(), loginPageBrandingId: serial("loginPageBrandingId").primaryKey(),
logoUrl: text("logoUrl").notNull(), logoUrl: text("logoUrl"),
logoWidth: integer("logoWidth").notNull(), logoWidth: integer("logoWidth").notNull(),
logoHeight: integer("logoHeight").notNull(), logoHeight: integer("logoHeight").notNull(),
primaryColor: text("primaryColor"), primaryColor: text("primaryColor"),

View File

@@ -206,7 +206,7 @@ export const loginPageBranding = sqliteTable("loginPageBranding", {
loginPageBrandingId: integer("loginPageBrandingId").primaryKey({ loginPageBrandingId: integer("loginPageBrandingId").primaryKey({
autoIncrement: true autoIncrement: true
}), }),
logoUrl: text("logoUrl").notNull(), logoUrl: text("logoUrl"),
logoWidth: integer("logoWidth").notNull(), logoWidth: integer("logoWidth").notNull(),
logoHeight: integer("logoHeight").notNull(), logoHeight: integer("logoHeight").notNull(),
primaryColor: text("primaryColor"), primaryColor: text("primaryColor"),

View File

@@ -35,7 +35,29 @@ const paramsSchema = z.strictObject({
}); });
const bodySchema = z.strictObject({ const bodySchema = z.strictObject({
logoUrl: z.url(), 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;
}
},
{
error: "Invalid logo URL, must be a valid image URL"
}
)
])
.optional(),
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(),
@@ -95,6 +117,10 @@ export async function upsertLoginPageBranding(
typeof loginPageBranding typeof loginPageBranding
>; >;
if ((updateData.logoUrl ?? "").trim().length === 0) {
updateData.logoUrl = undefined;
}
if ( if (
build !== "saas" && build !== "saas" &&
!config.getRawPrivateConfig().flags.use_org_only_idp !config.getRawPrivateConfig().flags.use_org_only_idp

View File

@@ -11,6 +11,7 @@ import {
GetLoginPageResponse GetLoginPageResponse
} from "@server/routers/loginPage/types"; } from "@server/routers/loginPage/types";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
export interface AuthPageProps { export interface AuthPageProps {
params: Promise<{ orgId: string }>; params: Promise<{ orgId: string }>;
@@ -18,6 +19,12 @@ export interface AuthPageProps {
export default async function AuthPage(props: AuthPageProps) { export default async function AuthPage(props: AuthPageProps) {
const orgId = (await props.params).orgId; const orgId = (await props.params).orgId;
// custom auth branding is only available in enterprise and saas
if (build === "oss") {
redirect(`/${orgId}/settings/general/`);
}
let subscriptionStatus: GetOrgTierResponse | null = null; let subscriptionStatus: GetOrgTierResponse | null = null;
try { try {
const subRes = await getCachedSubscription(orgId); const subRes = await getCachedSubscription(orgId);

View File

@@ -42,24 +42,27 @@ export type AuthPageCustomizationProps = {
}; };
const AuthPageFormSchema = z.object({ const AuthPageFormSchema = z.object({
logoUrl: z.url().refine( logoUrl: z.union([
async (url) => { z.string().length(0),
try { z.url().refine(
const response = await fetch(url); async (url) => {
return ( try {
response.status === 200 && const response = await fetch(url);
(response.headers.get("content-type") ?? "").startsWith( return (
"image/" response.status === 200 &&
) (response.headers.get("content-type") ?? "").startsWith(
); "image/"
} catch (error) { )
return false; );
} catch (error) {
return false;
}
},
{
error: "Invalid logo URL, must be a valid image URL"
} }
}, )
{ ]),
error: "Invalid logo URL, must be a valid image URL"
}
),
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),
orgTitle: z.string().optional(), orgTitle: z.string().optional(),
@@ -90,7 +93,6 @@ export default function AuthPageBrandingForm({
deleteBranding, deleteBranding,
null null
); );
const [setIsDeleteModalOpen] = useState(false);
const t = useTranslations(); const t = useTranslations();

View File

@@ -7,7 +7,7 @@ import Image from "next/image";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
type BrandingLogoProps = { type BrandingLogoProps = {
logoPath?: string; logoPath?: string | null;
width: number; width: number;
height: number; height: number;
}; };

View File

@@ -88,7 +88,7 @@ type ResourceAuthPortalProps = {
idps?: LoginFormIDP[]; idps?: LoginFormIDP[];
orgId?: string; orgId?: string;
branding?: { branding?: {
logoUrl: string; logoUrl?: string | null;
logoWidth: number; logoWidth: number;
logoHeight: number; logoHeight: number;
primaryColor: string | null; primaryColor: string | null;