feat: complete v0.2 source and sync management

This commit is contained in:
2026-07-19 01:33:45 +08:00
parent 4e9a13981d
commit 5b5129fad5
21 changed files with 367 additions and 39 deletions
+29 -8
View File
@@ -1,8 +1,29 @@
import { NextResponse } from "next/server"; import { requireUser } from "@/lib/auth"; import { decrypt, encrypt } from "@/lib/crypto"; import { getMemosIdentity, verifyMemos } from "@/lib/memos"; import { db } from "@/lib/db"; import { externalUrl } from "@/lib/http";
export async function POST(req: Request) { try { const user=await requireUser(); const form=await req.formData(); const name=String(form.get("name")||"").trim(); const rawBaseUrl=String(form.get("baseUrl")||"").trim(); const token=String(form.get("token")||"").trim(); let baseUrl=""; try { const url=new URL(rawBaseUrl); if(!['http:','https:'].includes(url.protocol)) throw new Error(); baseUrl=`${url.origin}${url.pathname.replace(/\/+$/,"")}`; } catch { throw new Error("Invalid source URL"); } if(!name||token.length<20) throw new Error("Invalid source"); await verifyMemos(baseUrl,token); const identity=await getMemosIdentity(baseUrl,token);
const legacySources=db.prepare("SELECT id,token_encrypted FROM sources WHERE base_url=? AND remote_user IS NULL").all(baseUrl) as {id:number;token_encrypted:string}[];
for(const legacy of legacySources){try{const legacyIdentity=await getMemosIdentity(baseUrl,decrypt(legacy.token_encrypted));db.prepare("UPDATE sources SET remote_user=? WHERE id=? AND remote_user IS NULL").run(legacyIdentity.name,legacy.id);}catch{ /* Keep unavailable legacy sources unchanged. */ }}
const shared=db.prepare("SELECT id FROM sources WHERE base_url=? AND remote_user=?").get(baseUrl,identity.name) as {id:number}|undefined;
if(shared){db.prepare("INSERT OR IGNORE INTO source_members(source_id,user_id) VALUES(?,?)").run(shared.id,user.id);return NextResponse.redirect(externalUrl(req,"/dashboard?source=shared"));}
const out=db.prepare("INSERT INTO sources(user_id,name,base_url,token_encrypted,remote_user,sync_status) VALUES(?,?,?,?,?, 'queued')").run(user.id,name,baseUrl,encrypt(token),identity.name);const sourceId=Number(out.lastInsertRowid);db.prepare("INSERT INTO source_members(source_id,user_id,role) VALUES(?,?,'owner')").run(sourceId,user.id);db.prepare("INSERT INTO sync_jobs(source_id,kind) VALUES(?, 'pull')").run(sourceId);return NextResponse.redirect(externalUrl(req,"/dashboard?source=connected"));
} catch(e){ return NextResponse.redirect(externalUrl(req,"/dashboard?error="+encodeURIComponent(e instanceof Error?e.message:"source"))); } }
import { NextResponse } from "next/server";
import { requireUser } from "@/lib/auth";
import { decrypt, encrypt } from "@/lib/crypto";
import { db } from "@/lib/db";
import { externalUrl } from "@/lib/http";
import { getMemosIdentity, verifyMemos } from "@/lib/memos";
import { queuePull } from "@/lib/sync";
export async function POST(req: Request) {
try {
const user = await requireUser(); const form = await req.formData();
const name = String(form.get("name") || "").trim(); const rawBaseUrl = String(form.get("baseUrl") || "").trim(); const token = String(form.get("token") || "").trim();
let baseUrl = "";
try { const url = new URL(rawBaseUrl); if (!['http:', 'https:'].includes(url.protocol)) throw new Error(); baseUrl = `${url.origin}${url.pathname.replace(/\/+$/, "")}`; } catch { throw new Error("Invalid source URL"); }
if (!name || token.length < 20) throw new Error("Invalid source");
await verifyMemos(baseUrl, token); const identity = await getMemosIdentity(baseUrl, token);
const legacySources = db.prepare("SELECT id,token_encrypted FROM sources WHERE base_url=? AND remote_user IS NULL").all(baseUrl) as { id: number; token_encrypted: string }[];
for (const legacy of legacySources) {
try { const legacyIdentity = await getMemosIdentity(baseUrl, decrypt(legacy.token_encrypted)); db.prepare("UPDATE sources SET remote_user=? WHERE id=? AND remote_user IS NULL").run(legacyIdentity.name, legacy.id); } catch { /* Retry on a future connection. */ }
}
const shared = db.prepare("SELECT id FROM sources WHERE base_url=? AND remote_user=?").get(baseUrl, identity.name) as { id: number } | undefined;
if (shared) { db.prepare("INSERT OR IGNORE INTO source_members(source_id,user_id) VALUES(?,?)").run(shared.id, user.id); return NextResponse.redirect(externalUrl(req, "/dashboard?source=shared")); }
const out = db.prepare("INSERT INTO sources(user_id,name,base_url,token_encrypted,remote_user,sync_status) VALUES(?,?,?,?,?, 'queued')").run(user.id, name, baseUrl, encrypt(token), identity.name);
const sourceId = Number(out.lastInsertRowid);
db.prepare("INSERT INTO source_members(source_id,user_id,role) VALUES(?,?,'owner')").run(sourceId, user.id);
queuePull(sourceId, "source-created");
return NextResponse.redirect(externalUrl(req, "/dashboard?source=connected"));
} catch (error) { return NextResponse.redirect(externalUrl(req, "/dashboard?error=" + encodeURIComponent(error instanceof Error ? error.message : "source"))); }
}