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

- 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:
Admin
2026-03-23 17:21:12 +05:00
parent 1118392811
commit 59e8cdb19a
522 changed files with 5259 additions and 80365 deletions

View File

@@ -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 });
};