Initial Mebbling hub implementation
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
import { NextResponse } from "next/server"; import bcrypt from "bcryptjs"; import { db } from "@/lib/db"; import { createSession } from "@/lib/auth"; import { externalUrl } from "@/lib/http";
|
||||
export async function POST(req: Request) { const form = await req.formData(); const username=String(form.get("username")||""); const password=String(form.get("password")||""); const user=db.prepare("SELECT id,username,password_hash,role,disabled FROM users WHERE username=?").get(username) as any;
|
||||
if (!user || user.disabled || !(await bcrypt.compare(password,user.password_hash))) return NextResponse.redirect(externalUrl(req,"/login?error=invalid")); await createSession({id:user.id,username:user.username,role:user.role}); return NextResponse.redirect(externalUrl(req,"/dashboard")); }
|
||||
@@ -0,0 +1 @@
|
||||
import { NextResponse } from "next/server"; import { clearSession } from "@/lib/auth"; import { externalUrl } from "@/lib/http"; export async function POST(req:Request){await clearSession();return NextResponse.redirect(externalUrl(req,"/"));}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { NextResponse } from "next/server"; import bcrypt from "bcryptjs"; import { db } from "@/lib/db"; import { createSession } from "@/lib/auth"; import { externalUrl } from "@/lib/http";
|
||||
export async function POST(req: Request) { const form = await req.formData(); const username = String(form.get("username") || "").trim(); const password = String(form.get("password") || "");
|
||||
if (!/^[A-Za-z0-9_-]{3,32}$/.test(username) || password.length < 10) return NextResponse.redirect(externalUrl(req,"/register?error=invalid"));
|
||||
try { const out = db.prepare("INSERT INTO users(username,password_hash) VALUES (?,?)").run(username, await bcrypt.hash(password, 12)); await createSession({ id: Number(out.lastInsertRowid), username, role: "user" }); return NextResponse.redirect(externalUrl(req,"/dashboard")); } catch { return NextResponse.redirect(externalUrl(req,"/register?error=taken")); }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { NextResponse } from "next/server"; import { requireUser } from "@/lib/auth"; import { db } from "@/lib/db"; import { externalUrl } from "@/lib/http";
|
||||
export async function POST(req:Request){try{const user=await requireUser();const f=await req.formData();const postId=Number(f.get('postId'));const content=String(f.get('content')||'').trim();if(!postId||!content||content.length>5000)throw new Error('Invalid comment');db.prepare('INSERT INTO comments(post_id,author_id,content) VALUES(?,?,?)').run(postId,user.id,content);return NextResponse.redirect(externalUrl(req,`/posts/${postId}`));}catch{return NextResponse.redirect(externalUrl(req,'/'));}}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { NextResponse } from "next/server"; import { requireUser } from "@/lib/auth"; import { db } from "@/lib/db"; import { externalUrl } from "@/lib/http"; import { mkdir, writeFile } from "node:fs/promises"; import { randomUUID } from "node:crypto"; import { extname, join } from "node:path";
|
||||
export async function POST(req:Request){const json=req.headers.get("accept")?.includes("application/json");try{const user=await requireUser();const f=await req.formData();const content=String(f.get("content")||"").trim();const visibility=String(f.get("visibility")||"PUBLIC");const sourceId=Number(f.get("sourceId"));const tags=String(f.get("tags")||"").split(/\s*,\s*/).filter(Boolean).map(t=>t.replace(/^#/,""));if(!content||!['PRIVATE','PROTECTED','PUBLIC'].includes(visibility)||!sourceId)throw new Error("Invalid post");const source=db.prepare("SELECT s.id FROM sources s JOIN source_members sm ON sm.source_id=s.id WHERE s.id=? AND sm.user_id=?").get(sourceId,user.id);if(!source)throw new Error("Source not found");const max=Number(process.env.UPLOAD_MAX_BYTES||10485760);const files=f.getAll('attachments').filter((x):x is File=>x instanceof File&&x.size>0);const attachments:any[]=[];await mkdir(join(process.cwd(),'public','uploads'),{recursive:true});for(const file of files){if(file.size>max)throw new Error(`${file.name} exceeds upload limit`);const id=randomUUID()+extname(file.name);await writeFile(join(process.cwd(),'public','uploads',id),Buffer.from(await file.arrayBuffer()));attachments.push({name:file.name,url:`/uploads/${id}`,type:file.type,size:file.size});}const out=db.prepare("INSERT INTO posts(source_id,author_id,content,visibility,tags_json,attachments_json,origin,sync_status) VALUES(?,?,?,?,?,?,'hub','queued')").run(sourceId,user.id,content,visibility,JSON.stringify(tags),JSON.stringify(attachments));db.prepare("INSERT INTO sync_jobs(source_id,kind,payload_json) VALUES(?, 'push', ?)").run(sourceId,JSON.stringify({postId:out.lastInsertRowid}));if(json)return NextResponse.json({id:Number(out.lastInsertRowid)},{status:201});return NextResponse.redirect(externalUrl(req,`/posts/${out.lastInsertRowid}`));}catch(e){const message=e instanceof Error?e.message:'post';if(json)return NextResponse.json({error:message},{status:400});return NextResponse.redirect(externalUrl(req,'/dashboard?error='+encodeURIComponent(message)));}}
|
||||
@@ -0,0 +1,2 @@
|
||||
import { NextResponse } from "next/server"; import { requireUser } from "@/lib/auth"; import { db } from "@/lib/db"; import { externalUrl } from "@/lib/http";
|
||||
const allowed=new Set(['👍','❤️','🎉','🤔']);export async function POST(req:Request){try{const user=await requireUser();const f=await req.formData();const postId=Number(f.get('postId'));const emoji=String(f.get('emoji'));if(!postId||!allowed.has(emoji))throw 0;const found=db.prepare('SELECT 1 FROM reactions WHERE post_id=? AND user_id=? AND emoji=?').get(postId,user.id,emoji);if(found)db.prepare('DELETE FROM reactions WHERE post_id=? AND user_id=? AND emoji=?').run(postId,user.id,emoji);else db.prepare('INSERT INTO reactions(post_id,user_id,emoji) VALUES(?,?,?)').run(postId,user.id,emoji);return NextResponse.redirect(externalUrl(req,`/posts/${postId}`));}catch{return NextResponse.redirect(externalUrl(req,'/'));}}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { requireUser } from "@/lib/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { createWebhookSecret, webhookSecretHash } from "@/lib/webhook";
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||
try {
|
||||
const user = await requireUser(); const { id: rawId } = await params; const id = Number(rawId);
|
||||
const source = db.prepare("SELECT id FROM sources WHERE id=? AND user_id=?").get(id, user.id);
|
||||
if (!source) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
const secret = createWebhookSecret();
|
||||
db.prepare("UPDATE sources SET webhook_secret_hash=? WHERE id=?").run(webhookSecretHash(secret), id);
|
||||
const publicOrigin = (process.env.NEXT_PUBLIC_APP_URL || new URL(request.url).origin).replace(/\/$/, "");
|
||||
return NextResponse.json({ url: `${publicOrigin}/api/sync/webhook/${id}/${secret}` });
|
||||
} catch { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
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"))); } }
|
||||
@@ -0,0 +1,2 @@
|
||||
import { NextResponse } from "next/server"; import { requireUser } from "@/lib/auth"; import { db } from "@/lib/db"; import { externalUrl } from "@/lib/http";
|
||||
export async function POST(req:Request){try{const user=await requireUser();const f=await req.formData();const sourceId=Number(f.get('sourceId'));const source=db.prepare('SELECT s.id FROM sources s JOIN source_members sm ON sm.source_id=s.id WHERE s.id=? AND sm.user_id=?').get(sourceId,user.id);if(!source)throw 0;db.prepare("INSERT INTO sync_jobs(source_id,kind) VALUES(?, 'pull')").run(sourceId);return NextResponse.redirect(externalUrl(req,'/dashboard'));}catch{return NextResponse.redirect(externalUrl(req,'/'));}}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { db } from "@/lib/db";
|
||||
import { withinRateLimit } from "@/lib/rate-limit";
|
||||
import { webhookSecretMatches } from "@/lib/webhook";
|
||||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ sourceId: string; secret: string }> }) {
|
||||
const { sourceId, secret } = await params;
|
||||
const id = Number(sourceId);
|
||||
const source = db.prepare("SELECT id, webhook_secret_hash FROM sources WHERE id=?").get(id) as { id: number; webhook_secret_hash: string | null } | undefined;
|
||||
if (!source || !webhookSecretMatches(secret, source.webhook_secret_hash)) return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||||
const forwarded = request.headers.get("x-forwarded-for")?.split(",")[0].trim() || "unknown";
|
||||
if (!withinRateLimit(`webhook:${id}:${forwarded}`)) return NextResponse.json({ error: "Too many requests" }, { status: 429 });
|
||||
let payload: unknown = {};
|
||||
try { payload = await request.json(); } catch { /* Memos payload is optional; a pull reconciles source state. */ }
|
||||
db.prepare("UPDATE sources SET last_webhook_at=CURRENT_TIMESTAMP WHERE id=?").run(id);
|
||||
db.prepare("INSERT INTO sync_jobs(source_id,kind,payload_json) VALUES(?, 'pull', ?)").run(id, JSON.stringify(payload));
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
Reference in New Issue
Block a user