fix: ignore profile picture cache after profile picture gets updated

This commit is contained in:
Elias Schneider
2025-04-09 15:51:58 +02:00
parent 658a9ca6dd
commit 4ba68938dd
5 changed files with 62 additions and 7 deletions

View File

@@ -254,7 +254,10 @@ func (uc *UserController) getUserProfilePictureHandler(c *gin.Context) {
defer picture.Close() defer picture.Close()
} }
c.Header("Cache-Control", "public, max-age=300") _, ok := c.GetQuery("skipCache")
if !ok {
c.Header("Cache-Control", "public, max-age=900")
}
c.DataFromReader(http.StatusOK, size, "image/png", picture, nil) c.DataFromReader(http.StatusOK, size, "image/png", picture, nil)
} }

View File

@@ -2,10 +2,11 @@
import FileInput from '$lib/components/form/file-input.svelte'; import FileInput from '$lib/components/form/file-input.svelte';
import * as Avatar from '$lib/components/ui/avatar'; import * as Avatar from '$lib/components/ui/avatar';
import Button from '$lib/components/ui/button/button.svelte'; import Button from '$lib/components/ui/button/button.svelte';
import { LucideLoader, LucideRefreshCw, LucideUpload } from 'lucide-svelte';
import { openConfirmDialog } from '../confirm-dialog';
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import type UserService from '$lib/services/user-service'; import { getProfilePictureUrl } from '$lib/utils/profile-picture-util';
import { LucideLoader, LucideRefreshCw, LucideUpload } from 'lucide-svelte';
import { onMount } from 'svelte';
import { openConfirmDialog } from '../confirm-dialog';
let { let {
userId, userId,
@@ -20,7 +21,12 @@
} = $props(); } = $props();
let isLoading = $state(false); let isLoading = $state(false);
let imageDataURL = $state(`/api/users/${userId}/profile-picture.png`); let imageDataURL = $state('');
onMount(() => {
// The "skipCache" query will only be added to the profile picture url on client-side
// because of that we need to set the imageDataURL after the component is mounted
imageDataURL = getProfilePictureUrl(userId);
});
async function onImageChange(e: Event) { async function onImageChange(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0] || null; const file = (e.target as HTMLInputElement).files?.[0] || null;
@@ -35,7 +41,7 @@
reader.readAsDataURL(file); reader.readAsDataURL(file);
await updateCallback(file).catch(() => { await updateCallback(file).catch(() => {
imageDataURL = `/api/users/${userId}/profile-picture.png`; imageDataURL = getProfilePictureUrl(userId);
}); });
isLoading = false; isLoading = false;
} }

View File

@@ -4,6 +4,7 @@
import { m } from '$lib/paraglide/messages'; import { m } from '$lib/paraglide/messages';
import WebAuthnService from '$lib/services/webauthn-service'; import WebAuthnService from '$lib/services/webauthn-service';
import userStore from '$lib/stores/user-store'; import userStore from '$lib/stores/user-store';
import { getProfilePictureUrl } from '$lib/utils/profile-picture-util';
import { LucideLogOut, LucideUser } from 'lucide-svelte'; import { LucideLogOut, LucideUser } from 'lucide-svelte';
const webauthnService = new WebAuthnService(); const webauthnService = new WebAuthnService();
@@ -17,7 +18,7 @@
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger <DropdownMenu.Trigger
><Avatar.Root class="h-9 w-9"> ><Avatar.Root class="h-9 w-9">
<Avatar.Image src="/api/users/{$userStore?.id}/profile-picture.png" /> <Avatar.Image src={getProfilePictureUrl($userStore?.id)} />
</Avatar.Root></DropdownMenu.Trigger </Avatar.Root></DropdownMenu.Trigger
> >
<DropdownMenu.Content class="min-w-40" align="start"> <DropdownMenu.Content class="min-w-40" align="start">

View File

@@ -1,6 +1,9 @@
import userStore from '$lib/stores/user-store';
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type'; import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
import type { UserGroup } from '$lib/types/user-group.type'; import type { UserGroup } from '$lib/types/user-group.type';
import type { User, UserCreate } from '$lib/types/user.type'; import type { User, UserCreate } from '$lib/types/user.type';
import { bustProfilePictureCache } from '$lib/utils/profile-picture-util';
import { get } from 'svelte/store';
import APIService from './api-service'; import APIService from './api-service';
export default class UserService extends APIService { export default class UserService extends APIService {
@@ -49,6 +52,7 @@ export default class UserService extends APIService {
const formData = new FormData(); const formData = new FormData();
formData.append('file', image!); formData.append('file', image!);
bustProfilePictureCache(userId);
await this.api.put(`/users/${userId}/profile-picture`, formData); await this.api.put(`/users/${userId}/profile-picture`, formData);
} }
@@ -56,14 +60,17 @@ export default class UserService extends APIService {
const formData = new FormData(); const formData = new FormData();
formData.append('file', image!); formData.append('file', image!);
bustProfilePictureCache(get(userStore)!.id);
await this.api.put('/users/me/profile-picture', formData); await this.api.put('/users/me/profile-picture', formData);
} }
async resetCurrentUserProfilePicture() { async resetCurrentUserProfilePicture() {
bustProfilePictureCache(get(userStore)!.id);
await this.api.delete(`/users/me/profile-picture`); await this.api.delete(`/users/me/profile-picture`);
} }
async resetProfilePicture(userId: string) { async resetProfilePicture(userId: string) {
bustProfilePictureCache(userId);
await this.api.delete(`/users/${userId}/profile-picture`); await this.api.delete(`/users/${userId}/profile-picture`);
} }

View File

@@ -0,0 +1,38 @@
import { browser } from '$app/environment';
type SkipCacheUntil = {
[key: string]: number;
};
export function getProfilePictureUrl(userId?: string) {
if (!userId) return '';
let url = `/api/users/${userId}/profile-picture.png`;
if (browser) {
const skipCacheUntil = getSkipCacheUntil(userId);
const skipCache = skipCacheUntil > Date.now();
if (skipCache) {
const skipCacheParam = new URLSearchParams();
skipCacheParam.append('skip-cache', skipCacheUntil.toString());
url += '?' + skipCacheParam.toString();
}
}
return url.toString();
}
function getSkipCacheUntil(userId: string) {
const skipCacheUntil: SkipCacheUntil = JSON.parse(
localStorage.getItem('skip-cache-until') ?? '{}'
);
return skipCacheUntil[userId] ?? 0;
}
export function bustProfilePictureCache(userId: string) {
const skipCacheUntil: SkipCacheUntil = JSON.parse(
localStorage.getItem('skip-cache-until') ?? '{}'
);
skipCacheUntil[userId] = Date.now() + 1000 * 60 * 15; // 15 minutes
localStorage.setItem('skip-cache-until', JSON.stringify(skipCacheUntil));
}