fix: use presigned S3 URLs for avatar images instead of /uploads/ paths
All checks were successful
CI / ci (push) Successful in 1m11s
CI / e2e (push) Has been skipped
CI / deploy (push) Successful in 14s

Avatar images were rendered via /uploads/ which doesn't exist since
the S3 migration. Now the server enriches profile responses with
avatarImageUrl (presigned S3 URL) and the frontend uses it directly.
Also fixed the public profile page at /users/:id.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 23:02:45 +02:00
parent 62916a8397
commit b647e23f91
5 changed files with 37 additions and 12 deletions

View File

@@ -11,7 +11,8 @@ export function ProfileSection() {
const [displayName, setDisplayName] = useState("");
const [bio, setBio] = useState("");
const [avatarUrl, setAvatarUrl] = useState<string | null>(null);
const [avatarFilename, setAvatarFilename] = useState<string | null>(null);
const [avatarDisplayUrl, setAvatarDisplayUrl] = useState<string | null>(null);
const [dirty, setDirty] = useState(false);
const [message, setMessage] = useState<{
type: "success" | "error";
@@ -24,7 +25,8 @@ export function ProfileSection() {
if (profile && !dirty) {
setDisplayName(profile.displayName ?? "");
setBio(profile.bio ?? "");
setAvatarUrl(profile.avatarUrl ?? null);
setAvatarFilename(profile.avatarUrl ?? null);
setAvatarDisplayUrl(profile.avatarImageUrl ?? null);
}
}, [profile, dirty]);
@@ -34,7 +36,7 @@ export function ProfileSection() {
try {
await updateProfile.mutateAsync({
displayName: displayName.trim() || undefined,
avatarUrl,
avatarUrl: avatarFilename,
bio: bio.trim() || undefined,
});
setDirty(false);
@@ -63,13 +65,16 @@ export function ProfileSection() {
return;
}
const localPreview = URL.createObjectURL(file);
setAvatarDisplayUrl(localPreview);
setUploading(true);
setMessage(null);
try {
const result = await apiUpload<{ filename: string }>("/api/images", file);
setAvatarUrl(result.filename);
setAvatarFilename(result.filename);
setDirty(true);
} catch {
setAvatarDisplayUrl(null);
setMessage({ type: "error", text: "Avatar upload failed." });
} finally {
setUploading(false);
@@ -92,9 +97,9 @@ export function ProfileSection() {
onClick={() => fileInputRef.current?.click()}
className="relative w-16 h-16 rounded-full overflow-hidden cursor-pointer group shrink-0"
>
{avatarUrl ? (
{avatarDisplayUrl ? (
<img
src={`/uploads/${avatarUrl}`}
src={avatarDisplayUrl}
alt="Avatar"
className="w-full h-full object-cover"
/>
@@ -145,11 +150,12 @@ export function ProfileSection() {
>
{uploading ? "Uploading..." : "Change avatar"}
</button>
{avatarUrl && (
{avatarFilename && (
<button
type="button"
onClick={() => {
setAvatarUrl(null);
setAvatarFilename(null);
setAvatarDisplayUrl(null);
setDirty(true);
}}
className="block text-xs text-red-500 hover:text-red-700 mt-0.5"

View File

@@ -5,6 +5,7 @@ interface PublicProfile {
id: number;
displayName: string | null;
avatarUrl: string | null;
avatarImageUrl: string | null;
bio: string | null;
setups: { id: number; name: string; createdAt: string }[];
}

View File

@@ -52,9 +52,9 @@ function PublicProfilePage() {
<div className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-10">
{/* Profile header */}
<div className="flex items-center gap-5 mb-8">
{profile.avatarUrl ? (
{profile.avatarImageUrl ? (
<img
src={`/uploads/${profile.avatarUrl}`}
src={profile.avatarImageUrl}
alt={displayName}
className="w-20 h-20 rounded-full object-cover"
/>

View File

@@ -14,6 +14,7 @@ import {
listApiKeys,
} from "../services/auth.service.ts";
import { updateProfile } from "../services/profile.service.ts";
import { getImageUrl } from "../services/storage.service.ts";
type Env = { Variables: { db?: any; userId?: number } };
@@ -97,7 +98,12 @@ app.put(
const data = c.req.valid("json");
const updated = await updateProfile(db, userId, data);
if (!updated) return c.json({ error: "User not found" }, 404);
return c.json(updated);
return c.json({
...updated,
avatarImageUrl: updated.avatarUrl
? await getImageUrl(updated.avatarUrl)
: null,
});
},
);

View File

@@ -1,9 +1,21 @@
import { Hono } from "hono";
import { parseId } from "../lib/params.ts";
import { getPublicProfile } from "../services/profile.service.ts";
import { getImageUrl } from "../services/storage.service.ts";
type Env = { Variables: { db?: any; userId?: number } };
async function enrichAvatarUrl<T extends { avatarUrl: string | null }>(
record: T,
): Promise<T & { avatarImageUrl: string | null }> {
return {
...record,
avatarImageUrl: record.avatarUrl
? await getImageUrl(record.avatarUrl)
: null,
};
}
const app = new Hono<Env>();
// GET /:id/profile — Public profile (no auth required)
@@ -15,7 +27,7 @@ app.get("/:id/profile", async (c) => {
const profile = await getPublicProfile(db, id);
if (!profile) return c.json({ error: "User not found" }, 404);
return c.json(profile);
return c.json(await enrichAvatarUrl(profile));
});
export { app as profileRoutes };