chore: migrate to v3, Doppler secrets, clean up legacy code
Some checks failed
CI / v3 / Check ui (pull_request) Failing after 15s
CI / v3 / Test backend (pull_request) Failing after 16s
CI / v3 / Docker / backend (pull_request) Has been skipped
CI / v3 / Docker / runner (pull_request) Has been skipped
CI / v3 / Docker / ui (pull_request) Has been skipped
Some checks failed
CI / v3 / Check ui (pull_request) Failing after 15s
CI / v3 / Test backend (pull_request) Failing after 16s
CI / v3 / Docker / backend (pull_request) Has been skipped
CI / v3 / Docker / runner (pull_request) Has been skipped
CI / v3 / Docker / ui (pull_request) Has been skipped
- Remove all pre-v3 code: scraper, ui-v2, backend v1, ios v1+v2, legacy CI workflows - Flatten v3/ contents to repo root - Add Doppler secrets management (project=libnovel, config=prd) - Add justfile with doppler run wrappers for all docker compose commands - Strip hardcoded env fallbacks from docker-compose.yml - Add minimal README.md - Clean up .gitignore
This commit is contained in:
@@ -1,65 +1,56 @@
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { presignAvatarUploadUrl, presignAvatarUrl } from '$lib/server/minio';
|
||||
import { presignAvatarUrl } from '$lib/server/minio';
|
||||
import { updateUserAvatarUrl, getUserByUsername } from '$lib/server/pocketbase';
|
||||
import { backendFetch } from '$lib/server/scraper';
|
||||
|
||||
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
|
||||
|
||||
/**
|
||||
* POST /api/profile/avatar
|
||||
* Body: JSON { mime_type: "image/jpeg" | "image/png" | "image/webp" }
|
||||
* Body: raw image bytes (Content-Type: image/jpeg | image/png | image/webp)
|
||||
*
|
||||
* Returns a short-lived presigned PUT URL pointing at MinIO (public endpoint)
|
||||
* so the client can upload the image bytes directly, bypassing the server.
|
||||
* After the PUT completes, the client must call PATCH /api/profile/avatar
|
||||
* with the returned key to record it in PocketBase.
|
||||
*
|
||||
* Returns: { upload_url: string, key: string }
|
||||
*/
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
if (!locals.user) error(401, 'Not authenticated');
|
||||
|
||||
let mimeType = 'image/jpeg';
|
||||
try {
|
||||
const body = await request.json();
|
||||
if (body?.mime_type) mimeType = body.mime_type;
|
||||
} catch {
|
||||
// default to jpeg if body is missing/invalid
|
||||
}
|
||||
|
||||
if (!ALLOWED_TYPES.includes(mimeType)) {
|
||||
error(400, `Unsupported image type: ${mimeType}. Allowed: jpeg, png, webp`);
|
||||
}
|
||||
|
||||
const { uploadUrl, key } = await presignAvatarUploadUrl(locals.user.id, mimeType);
|
||||
return json({ upload_url: uploadUrl, key });
|
||||
};
|
||||
|
||||
/**
|
||||
* PATCH /api/profile/avatar
|
||||
* Body: JSON { key: string }
|
||||
*
|
||||
* Called after the client has successfully PUT the image to MinIO via the
|
||||
* presigned URL. Records the object key in PocketBase and returns a fresh
|
||||
* Uploads the image to MinIO via the Go backend (server-to-server, no browser
|
||||
* → MinIO direct upload), records the key in PocketBase, and returns a fresh
|
||||
* presigned GET URL for immediate display.
|
||||
*
|
||||
* Returns: { avatar_url: string | null }
|
||||
*/
|
||||
export const PATCH: RequestHandler = async ({ request, locals }) => {
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
if (!locals.user) error(401, 'Not authenticated');
|
||||
|
||||
let key: string | undefined;
|
||||
try {
|
||||
const body = await request.json();
|
||||
if (typeof body?.key === 'string') key = body.key;
|
||||
} catch {
|
||||
error(400, 'Invalid JSON body');
|
||||
const ct = request.headers.get('Content-Type') ?? '';
|
||||
// Strip parameters (e.g. "image/jpeg; charset=utf-8" → "image/jpeg")
|
||||
const mimeType = ct.split(';')[0].trim();
|
||||
|
||||
if (!ALLOWED_TYPES.includes(mimeType)) {
|
||||
error(400, `Unsupported image type. Allowed: image/jpeg, image/png, image/webp`);
|
||||
}
|
||||
|
||||
if (!key) error(400, 'Missing "key" field');
|
||||
// Read the raw body
|
||||
const blob = await request.arrayBuffer();
|
||||
if (blob.byteLength === 0) error(400, 'Empty image body');
|
||||
if (blob.byteLength > 5 * 1024 * 1024) error(413, 'Image too large (max 5 MiB)');
|
||||
|
||||
// Forward directly to Go backend — server-to-server, so internal MinIO is reachable.
|
||||
const uploadRes = await backendFetch(
|
||||
`/api/avatar-upload/${encodeURIComponent(locals.user.id)}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': mimeType },
|
||||
body: blob
|
||||
}
|
||||
);
|
||||
if (!uploadRes.ok) {
|
||||
const text = await uploadRes.text().catch(() => '');
|
||||
error(uploadRes.status as 400 | 500, `Upload failed: ${text || uploadRes.statusText}`);
|
||||
}
|
||||
const { key } = (await uploadRes.json()) as { key: string };
|
||||
|
||||
// Record object key in PocketBase.
|
||||
await updateUserAvatarUrl(locals.user.id, key);
|
||||
|
||||
// Return a fresh presigned GET URL for immediate display.
|
||||
const avatarUrl = await presignAvatarUrl(locals.user.id);
|
||||
return json({ avatar_url: avatarUrl });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user