58 lines
6.1 KiB
TypeScript
58 lines
6.1 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { requireUser } from "@/lib/auth";
|
|
import { decrypt } from "@/lib/crypto";
|
|
import { db } from "@/lib/db";
|
|
import { audit } from "@/lib/audit";
|
|
import { externalUrl } from "@/lib/http";
|
|
import { getMemosIdentity, verifyMemos } from "@/lib/memos";
|
|
import { queuePull } from "@/lib/sync";
|
|
import { requireSameOrigin } from "@/lib/security";
|
|
|
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
try {
|
|
requireSameOrigin(req); const user = await requireUser(); const { id: rawId } = await params; const id = Number(rawId); const form = await req.formData(); const action = String(form.get("action") || "");
|
|
const source = db.prepare("SELECT id,user_id,base_url,token_encrypted FROM sources WHERE id=?").get(id) as { id: number; user_id: number; base_url: string; token_encrypted: string } | undefined;
|
|
const member = db.prepare("SELECT role FROM source_members WHERE source_id=? AND user_id=?").get(id, user.id);
|
|
if (!source || !member) throw new Error("Source not found");
|
|
const owner = source.user_id === user.id;
|
|
if (action === "set-enabled") {
|
|
if (!owner) throw new Error("Only the owner can change source status"); const enabled = String(form.get("enabled")) === "1";
|
|
db.prepare("UPDATE sources SET is_enabled=?,disabled_at=CASE WHEN ? THEN NULL ELSE CURRENT_TIMESTAMP END,sync_status=CASE WHEN ? THEN 'pending' ELSE 'disabled' END WHERE id=?").run(enabled ? 1 : 0, enabled ? 1 : 0, enabled ? 1 : 0, id);
|
|
if (enabled) queuePull(id, "manual");
|
|
} else if (action === "set-sync-rules") {
|
|
if (!owner) throw new Error("Only the owner can change sync rules");
|
|
const tags = String(form.get("tags") || "").split(",").map((tag) => tag.trim().replace(/^#/, "")).filter(Boolean).slice(0, 20);
|
|
const from = String(form.get("from") || ""); const to = String(form.get("to") || ""); const attachmentMode = String(form.get("attachmentMode") || "all"); const batchSize = Number(form.get("batchSize") || 100); const maxPosts = Number(form.get("maxPosts") || 0);
|
|
if ((from && !/^\d{4}-\d{2}-\d{2}$/.test(from)) || (to && !/^\d{4}-\d{2}-\d{2}$/.test(to)) || (from && to && from > to) || !["all", "images", "none"].includes(attachmentMode) || !Number.isInteger(batchSize) || batchSize < 10 || batchSize > 100 || !Number.isInteger(maxPosts) || maxPosts < 0 || maxPosts > 100000) throw new Error("Invalid sync rules");
|
|
db.prepare("UPDATE sources SET sync_tags_json=?,sync_from=?,sync_to=?,sync_attachment_mode=?,sync_batch_size=?,sync_max_posts=?,sync_cursor=NULL,sync_imported_count=0,sync_run_id=NULL WHERE id=?").run(JSON.stringify(tags), from || null, to || null, attachmentMode, batchSize, maxPosts || null, id);
|
|
queuePull(id, "manual");
|
|
} else if (action === "set-attachment-storage") {
|
|
if (!owner) throw new Error("Only the owner can change attachment storage"); const mode = String(form.get("mode") || "remote"); const quotaMiB = Number(form.get("quotaMiB") || 100);
|
|
if (!["remote", "images", "all"].includes(mode) || !Number.isFinite(quotaMiB) || quotaMiB < 10 || quotaMiB > 10_240) throw new Error("Invalid attachment storage settings");
|
|
db.prepare("UPDATE sources SET attachment_storage_mode=?,attachment_cache_limit_bytes=?,attachment_cache_error=NULL WHERE id=?").run(mode, Math.round(quotaMiB * 1024 * 1024), id); queuePull(id, "manual");
|
|
} else if (action === "test-connection") {
|
|
if (!owner) throw new Error("Only the owner can test the connection");
|
|
try {
|
|
const token = decrypt(source.token_encrypted); await verifyMemos(source.base_url, token); const identity = await getMemosIdentity(source.base_url, token);
|
|
const avatar = identity.avatarUrl || identity.avatar || null; const avatarUrl = avatar?.startsWith("/") ? `${source.base_url.replace(/\/$/, "")}${avatar}` : avatar;
|
|
const name = identity.displayName || identity.nickname || identity.username || identity.name;
|
|
db.prepare("UPDATE sources SET name=?,last_connection_at=CURRENT_TIMESTAMP,last_connection_error=NULL,remote_display_name=?,remote_avatar_url=? WHERE id=?").run(name, name, avatarUrl, id);
|
|
} catch (connectionError) { const message = connectionError instanceof Error ? connectionError.message : "Connection failed"; db.prepare("UPDATE sources SET last_connection_error=? WHERE id=?").run(message, id); throw connectionError; }
|
|
} else if (action === "leave") {
|
|
if (owner) throw new Error("Transfer ownership or delete the source before leaving");
|
|
db.prepare("DELETE FROM source_members WHERE source_id=? AND user_id=?").run(id, user.id);
|
|
} else if (action === "transfer") {
|
|
if (!owner) throw new Error("Only the owner can transfer ownership"); const username = String(form.get("username") || "").trim();
|
|
const target = db.prepare("SELECT u.id FROM users u JOIN source_members sm ON sm.user_id=u.id WHERE sm.source_id=? AND u.username=?").get(id, username) as { id: number } | undefined;
|
|
if (!target || target.id === user.id) throw new Error("Choose another existing member");
|
|
const transfer = db.transaction(() => { db.prepare("UPDATE sources SET user_id=? WHERE id=?").run(target.id, id); db.prepare("UPDATE source_members SET role='member' WHERE source_id=? AND user_id=?").run(id, user.id); db.prepare("UPDATE source_members SET role='owner' WHERE source_id=? AND user_id=?").run(id, target.id); });
|
|
transfer();
|
|
} else if (action === "delete") {
|
|
if (!owner) throw new Error("Only the owner can delete a source");
|
|
const remove = db.transaction(() => { db.prepare("DELETE FROM posts WHERE source_id=? AND origin='memos'").run(id); db.prepare("UPDATE posts SET source_id=NULL WHERE source_id=? AND origin='hub'").run(id); db.prepare("DELETE FROM sources WHERE id=?").run(id); });
|
|
remove();
|
|
} else throw new Error("Unknown source action");
|
|
audit(user.id, `source.${action}`, "source", id); return NextResponse.redirect(externalUrl(req, "/dashboard?source=updated"));
|
|
} catch (error) { return NextResponse.redirect(externalUrl(req, "/dashboard?error=" + encodeURIComponent(error instanceof Error ? error.message : "source"))); }
|
|
}
|