fix: add upload/delete file endpoints and wire avatar update through profile

- Add POST /upload and DELETE /assets/:id routes to backend (session auth via session_token cookie)
- Add avatar arg to updateProfile GraphQL mutation and resolver
- Fix frontend to pass avatarId correctly on save, preserve existing avatar when unchanged
- Ignore 404 on file delete (already gone is fine)
- Remove broken folder lookup (getFolders is a stub, backend has no folder concept)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 18:22:22 +01:00
parent a050e886cb
commit 56b57486dc
4 changed files with 65 additions and 6 deletions

View File

@@ -45,6 +45,7 @@ builder.mutationField("updateProfile", (t) =>
artistName: t.arg.string(),
description: t.arg.string(),
tags: t.arg.stringList(),
avatar: t.arg.string(),
},
resolve: async (_root, args, ctx) => {
if (!ctx.currentUser) throw new GraphQLError("Unauthorized");
@@ -58,6 +59,7 @@ builder.mutationField("updateProfile", (t) =>
if (args.description !== undefined && args.description !== null)
updates.description = args.description;
if (args.tags !== undefined && args.tags !== null) updates.tags = args.tags;
if (args.avatar !== undefined) updates.avatar = args.avatar;
await ctx.db
.update(users)

View File

@@ -7,7 +7,8 @@ import { createYoga } from "graphql-yoga";
import { eq } from "drizzle-orm";
import { files } from "./db/schema/index";
import path from "path";
import { existsSync } from "fs";
import { existsSync, mkdirSync } from "fs";
import { writeFile, rm } from "fs/promises";
import sharp from "sharp";
import { schema } from "./graphql/index";
import { buildContext } from "./graphql/context";
@@ -120,6 +121,54 @@ async function main() {
return reply.sendFile(path.join(id, filename));
});
// Upload a file: POST /upload (multipart, requires session)
fastify.post("/upload", async (request, reply) => {
const token = request.cookies["session_token"];
if (!token) return reply.status(401).send({ error: "Unauthorized" });
const sessionData = await redis.get(`session:${token}`);
if (!sessionData) return reply.status(401).send({ error: "Unauthorized" });
const { id: userId } = JSON.parse(sessionData);
const data = await request.file();
if (!data) return reply.status(400).send({ error: "No file provided" });
const id = crypto.randomUUID();
const filename = data.filename;
const mime_type = data.mimetype;
const dir = path.join(UPLOAD_DIR, id);
mkdirSync(dir, { recursive: true });
const buffer = await data.toBuffer();
await writeFile(path.join(dir, filename), buffer);
const [file] = await db
.insert(files)
.values({ id, filename, mime_type, filesize: buffer.byteLength, uploaded_by: userId })
.returning();
return reply.status(201).send(file);
});
// Delete a file: DELETE /assets/:id (requires session)
fastify.delete("/assets/:id", async (request, reply) => {
const token = request.cookies["session_token"];
if (!token) return reply.status(401).send({ error: "Unauthorized" });
const sessionData = await redis.get(`session:${token}`);
if (!sessionData) return reply.status(401).send({ error: "Unauthorized" });
const { id } = request.params as { id: string };
const result = await db.select().from(files).where(eq(files.id, id)).limit(1);
if (!result[0]) return reply.status(404).send({ error: "File not found" });
await db.delete(files).where(eq(files.id, id));
const dir = path.join(UPLOAD_DIR, id);
await rm(dir, { recursive: true, force: true });
return reply.status(200).send({ ok: true });
});
fastify.get("/health", async (_request, reply) => {
return reply.send({ status: "ok", timestamp: new Date().toISOString() });
});