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 { createUserWebhook, getMemosIdentity, verifyMemos } from "@/lib/memos"; import { createWebhookSecret, webhookSecretHash } from "@/lib/webhook"; import { queuePull } from "@/lib/sync"; import { requireSameOrigin } from "@/lib/security"; export async function POST(req: Request) { try { requireSameOrigin(req); const user = await requireUser(); const form = await req.formData(); 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 (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 name = identity.displayName || identity.nickname || identity.username || identity.name; const existing = db.prepare("SELECT id FROM sources WHERE user_id=? AND base_url=?").get(user.id, baseUrl) as { id: number } | undefined; if (existing) { db.prepare("UPDATE sources SET name=?,token_encrypted=?,remote_user=?,sync_status='queued',last_connection_error=NULL WHERE id=?").run(name, encrypt(token), identity.name, existing.id); queuePull(existing.id, "source-created"); return NextResponse.redirect(externalUrl(req, "/dashboard?source=reconnected")); } 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); const pathSecret = createWebhookSecret(); const signingSecret = createWebhookSecret(); const publicOrigin = (process.env.NEXT_PUBLIC_APP_URL || new URL(req.url).origin).replace(/\/$/, ""); try { const remote = await createUserWebhook(baseUrl, token, identity.name, { url: `${publicOrigin}/api/sync/webhook/${sourceId}/${pathSecret}`, displayName: "Mebbling", signingSecret }); db.prepare("UPDATE sources SET webhook_secret_hash=?,webhook_mode='signed',webhook_remote_name=?,webhook_signing_secret_encrypted=? WHERE id=?").run(webhookSecretHash(pathSecret), remote.name, encrypt(signingSecret), sourceId); } catch (webhookError) { const message = webhookError instanceof Error ? webhookError.message : "Webhook unsupported"; db.prepare("UPDATE sources SET webhook_mode=? WHERE id=?").run(message.includes("Memos API 404") ? "manual" : "unavailable", sourceId); } 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"))); } }