feat: derive source names from Memos accounts
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user