1
0
mirror of https://github.com/pocket-id/pocket-id.git synced 2026-02-11 16:28:59 +00:00

feat: add support for dark mode oidc client icons (#1039)

Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
Kyle Mendell
2025-10-24 02:57:12 -05:00
committed by GitHub
parent eb3963d0fc
commit 028d1c858e
19 changed files with 381 additions and 119 deletions

View File

@@ -12,11 +12,13 @@
let {
label,
accept,
onchange
onchange,
id = 'file-input'
}: {
label: string;
accept?: string;
onchange: (file: File | string | null) => void;
id?: string;
} = $props();
let url = $state('');
@@ -47,7 +49,7 @@
<div class="flex">
<FileInput
id="logo"
{id}
variant="secondary"
{accept}
onchange={handleFileChange}
@@ -64,9 +66,9 @@
<LucideChevronDown class="size-4" /></Popover.Trigger
>
<Popover.Content class="w-80">
<Label for="file-url" class="text-xs">URL</Label>
<Label for="{id}-url" class="text-xs">URL</Label>
<Input
id="file-url"
id="{id}-url"
placeholder=""
value={url}
oninput={(e) => (url = e.currentTarget.value)}

View File

@@ -68,25 +68,31 @@ class OidcService extends APIService {
updateClient = async (id: string, client: OidcClientUpdate) =>
(await this.api.put(`/oidc/clients/${id}`, client)).data as OidcClient;
updateClientLogo = async (client: OidcClient, image: File | null) => {
if (client.hasLogo && !image) {
await this.removeClientLogo(client.id);
updateClientLogo = async (client: OidcClient, image: File | null, light: boolean = true) => {
const hasLogo = light ? client.hasLogo : client.hasDarkLogo;
if (hasLogo && !image) {
await this.removeClientLogo(client.id, light);
return;
}
if (!client.hasLogo && !image) {
if (!hasLogo && !image) {
return;
}
const formData = new FormData();
formData.append('file', image!);
await this.api.post(`/oidc/clients/${client.id}/logo`, formData);
cachedOidcClientLogo.bustCache(client.id);
await this.api.post(`/oidc/clients/${client.id}/logo`, formData, {
params: { light }
});
cachedOidcClientLogo.bustCache(client.id, light);
};
removeClientLogo = async (id: string) => {
await this.api.delete(`/oidc/clients/${id}/logo`);
cachedOidcClientLogo.bustCache(id);
removeClientLogo = async (id: string, light: boolean = true) => {
await this.api.delete(`/oidc/clients/${id}/logo`, {
params: { light }
});
cachedOidcClientLogo.bustCache(id, light);
};
createClientSecret = async (id: string) =>

View File

@@ -4,6 +4,7 @@ export type OidcClientMetaData = {
id: string;
name: string;
hasLogo: boolean;
hasDarkLogo: boolean;
requiresReauthentication: boolean;
launchURL?: string;
};
@@ -37,17 +38,20 @@ export type OidcClientWithAllowedUserGroupsCount = OidcClient & {
allowedUserGroupsCount: number;
};
export type OidcClientUpdate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo'>;
export type OidcClientUpdate = Omit<OidcClient, 'id' | 'logoURL' | 'hasLogo' | 'hasDarkLogo'>;
export type OidcClientCreate = OidcClientUpdate & {
id?: string;
};
export type OidcClientUpdateWithLogo = OidcClientUpdate & {
logo: File | null | undefined;
darkLogo: File | null | undefined;
};
export type OidcClientCreateWithLogo = OidcClientCreate & {
logo?: File | null;
logoUrl?: string;
darkLogo?: File | null;
darkLogoUrl?: string;
};
export type OidcDeviceCodeInfo = {

View File

@@ -9,73 +9,85 @@ type CachableImage = {
export const cachedApplicationLogo: CachableImage = {
getUrl: (light = true) => {
let url = '/api/application-images/logo';
if (!light) {
url += '?light=false';
}
const url = new URL('/api/application-images/logo', window.location.origin);
if (!light) url.searchParams.set('light', 'false');
return getCachedImageUrl(url);
},
bustCache: (light = true) => {
let url = '/api/application-images/logo';
if (!light) {
url += '?light=false';
}
const url = new URL('/api/application-images/logo', window.location.origin);
if (!light) url.searchParams.set('light', 'false');
bustImageCache(url);
}
};
export const cachedBackgroundImage: CachableImage = {
getUrl: () => getCachedImageUrl('/api/application-images/background'),
bustCache: () => bustImageCache('/api/application-images/background')
getUrl: () =>
getCachedImageUrl(new URL('/api/application-images/background', window.location.origin)),
bustCache: () =>
bustImageCache(new URL('/api/application-images/background', window.location.origin))
};
export const cachedProfilePicture: CachableImage = {
getUrl: (userId: string) => {
const url = `/api/users/${userId}/profile-picture.png`;
const url = new URL(`/api/users/${userId}/profile-picture.png`, window.location.origin);
return getCachedImageUrl(url);
},
bustCache: (userId: string) => {
const url = `/api/users/${userId}/profile-picture.png`;
const url = new URL(`/api/users/${userId}/profile-picture.png`, window.location.origin);
bustImageCache(url);
}
};
export const cachedOidcClientLogo: CachableImage = {
getUrl: (clientId: string) => {
const url = `/api/oidc/clients/${clientId}/logo`;
getUrl: (clientId: string, light = true) => {
const url = new URL(`/api/oidc/clients/${clientId}/logo`, window.location.origin);
if (!light) url.searchParams.set('light', 'false');
return getCachedImageUrl(url);
},
bustCache: (clientId: string) => {
const url = `/api/oidc/clients/${clientId}/logo`;
bustCache: (clientId: string, light = true) => {
const url = new URL(`/api/oidc/clients/${clientId}/logo`, window.location.origin);
if (!light) url.searchParams.set('light', 'false');
bustImageCache(url);
}
};
function getCachedImageUrl(url: string) {
const skipCacheUntil = getSkipCacheUntil(url);
function getCachedImageUrl(url: URL) {
const baseKey = normalizeUrlForKey(url);
const skipCacheUntil = getSkipCacheUntil(baseKey);
const skipCache = skipCacheUntil > Date.now();
const finalUrl = new URL(url.toString());
if (skipCache) {
const skipCacheParam = new URLSearchParams();
skipCacheParam.append('skip-cache', skipCacheUntil.toString());
url += '?' + skipCacheParam.toString();
finalUrl.searchParams.set('skip-cache', skipCacheUntil.toString());
}
return url.toString();
return finalUrl.pathname + (finalUrl.search ? `?${finalUrl.searchParams.toString()}` : '');
}
function bustImageCache(url: string) {
const skipCacheUntil: SkipCacheUntil = JSON.parse(
localStorage.getItem('skip-cache-until') ?? '{}'
);
skipCacheUntil[hashKey(url)] = Date.now() + 1000 * 60 * 15; // 15 minutes
localStorage.setItem('skip-cache-until', JSON.stringify(skipCacheUntil));
function bustImageCache(url: URL) {
const key = normalizeUrlForKey(url);
const expiresAt = Date.now() + 1000 * 60 * 15;
const store: SkipCacheUntil = JSON.parse(localStorage.getItem('skip-cache-until') ?? '{}');
store[key] = expiresAt;
localStorage.setItem('skip-cache-until', JSON.stringify(store));
}
function getSkipCacheUntil(url: string) {
const skipCacheUntil: SkipCacheUntil = JSON.parse(
localStorage.getItem('skip-cache-until') ?? '{}'
function getSkipCacheUntil(key: string): number {
const store: SkipCacheUntil = JSON.parse(localStorage.getItem('skip-cache-until') ?? '{}');
return store[key] ?? 0;
}
// Removes transient params and normalizes query order before hashing
function normalizeUrlForKey(url: URL) {
const u = new URL(url.toString());
u.searchParams.delete('skip-cache');
const sortedParams = new URLSearchParams(
[...u.searchParams.entries()].sort(([a], [b]) => a.localeCompare(b))
);
return skipCacheUntil[hashKey(url)] ?? 0;
const normalized = u.pathname + (sortedParams.toString() ? `?${sortedParams.toString()}` : '');
return hashKey(normalized);
}
function hashKey(key: string): string {
@@ -83,7 +95,7 @@ function hashKey(key: string): string {
for (let i = 0; i < key.length; i++) {
const char = key.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = hash & hash;
hash |= 0;
}
return Math.abs(hash).toString(36);
}