feat: derive source names from Memos accounts

This commit is contained in:
2026-07-19 04:51:35 +08:00
parent 5425f93d1f
commit 15afc34a9c
4 changed files with 9 additions and 9 deletions
+3 -5
View File
@@ -14,10 +14,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
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 === "rename") {
const name = String(form.get("name") || "").trim(); if (!owner || !name || name.length > 80) throw new Error("Only the owner can rename a source");
db.prepare("UPDATE sources SET name=? WHERE id=?").run(name, id);
} else if (action === "set-enabled") {
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");
@@ -37,7 +34,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
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;
db.prepare("UPDATE sources SET last_connection_at=CURRENT_TIMESTAMP,last_connection_error=NULL,remote_display_name=?,remote_avatar_url=? WHERE id=?").run(identity.nickname || identity.username || identity.name, avatarUrl, id);
const name = 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");
+3 -2
View File
@@ -10,10 +10,10 @@ import { requireSameOrigin } from "@/lib/security";
export async function POST(req: Request) {
try {
requireSameOrigin(req); 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();
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");
if (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) {
@@ -21,6 +21,7 @@ export async function POST(req: Request) {
}
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 name = identity.nickname || identity.username || identity.name;
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);
+1 -1
View File
@@ -20,7 +20,7 @@ export default async function Dashboard({ searchParams }: { searchParams: Promis
{query.source === "shared" ? <p> Memos </p> : query.source === "updated" ? <p></p> : query.source && <p></p>}
{query.sync === "queued" && <p></p>}{query.sync === "already-queued" && <p className="muted"></p>}
<section className="card"><h2> Memos</h2>{publishSources.length ? <PublishForm sources={publishSources} /> : <p className="muted"> Memos </p>}</section>
<section className="card"><h2> Memos</h2><form action="/api/sources" method="post"><label><input name="name" required placeholder="我的 Memos" /></label><label>Memos <input name="baseUrl" type="url" required placeholder="https://memos.example.com" /></label><label>Personal Access Token<input name="token" type="password" required /></label><button></button></form><p className="muted">Token 使 Memos </p></section>
<section className="card"><h2> Memos</h2><form action="/api/sources" method="post"><label>Memos <input name="baseUrl" type="url" required placeholder="https://memos.example.com" /></label><label>Personal Access Token<input name="token" type="password" required /></label><button></button></form><p className="muted">使 API Key Memos Token 使 Memos </p></section>
<section><h2></h2>{sources.map((source) => <article className="card" key={source.id}>
<div className="space"><strong>{source.name}</strong><span className="tag">{source.is_enabled ? source.sync_status : "disabled"}</span></div>
<p className="meta"> ID{source.id}<br />{source.base_url}{source.remote_display_name && <><br />Memos {source.remote_avatar_url && <img className="avatar" src={source.remote_avatar_url} alt="" />} {source.remote_display_name}</>}<br />{source.members.map((member) => `${member.username}${member.role === "owner" ? "(建立者)" : ""}`).join("、")}<br />{source.last_synced_at || "尚未完成"}<br />{source.attachment_storage_mode === "remote" ? "遠端連結" : source.attachment_storage_mode === "images" ? "只快取圖片" : "完整備份"} {Math.round(source.attachment_cache_limit_bytes / 1024 / 1024)} MiB<br />{source.last_connection_at ? `最近成功:${new Date(source.last_connection_at + "Z").toLocaleString("zh-TW")}` : "尚未測試"}<br />Webhook{source.webhook_secret_hash ? (source.last_webhook_at ? (Date.now() - new Date(source.last_webhook_at + "Z").getTime() > 7 * 24 * 60 * 60 * 1000 ? `警示:超過 7 天未收到(最近:${new Date(source.last_webhook_at + "Z").toLocaleString("zh-TW")}` : `健康(最近收到:${new Date(source.last_webhook_at + "Z").toLocaleString("zh-TW")}`) : "已建立 URL,尚未收到呼叫") : "尚未建立 URL"}{!source.is_enabled && <><br />{source.disabled_at ? new Date(source.disabled_at + "Z").toLocaleString("zh-TW") : "是"}</>}{source.last_error && <><br /><span className="error">{source.last_error}</span></>}{source.last_connection_error && <><br /><span className="error">{source.last_connection_error}</span></>}{source.attachment_cache_error && <><br /><span className="error">{source.attachment_cache_error}</span></>}</p>