import { Application, Router } from "@oak/oak"; import { addWebhookSubscription, deleteWebhooksByDid, getNotes, getNotesByDid, getNotesByDids, type WebhookVerb, } from "./src/data/db.ts"; import { authenticateRequest } from "./src/auth/verify.ts"; import { log } from "./src/log.ts"; const router = new Router(); const PAGINATION = 20; const GITHUB_CLIENT_ID = Deno.env.get("GITHUB_CLIENT_ID") ?? ""; const GITHUB_CLIENT_SECRET = Deno.env.get("GITHUB_CLIENT_SECRET") ?? ""; router.get("/", (ctx) => { ctx.response.body = "Hello world"; }); router.get("/auth/github", async (ctx) => { const code = ctx.request.url.searchParams.get("code"); const type = ctx.request.url.searchParams.get("type"); if (!code) { ctx.response.status = 400; ctx.response.body = { error: "code is required" }; return; } const params: Record = { client_id: GITHUB_CLIENT_ID, client_secret: GITHUB_CLIENT_SECRET, }; if (type === "refresh") { params.grant_type = "refresh_token"; params.refresh_token = code; } else { params.code = code; } const response = await fetch( "https://github.com/login/oauth/access_token", { method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json", }, body: JSON.stringify(params), }, ); ctx.response.status = response.status; ctx.response.body = await response.json(); }); router.get("/health", (ctx) => { ctx.response.body = { status: "ok" }; }); router.get("/notes", (ctx) => { const cursor = ctx.request.url.searchParams.get("cursor") ?? undefined; const limit = Number(ctx.request.url.searchParams.get("limit")) || PAGINATION; ctx.response.body = getNotes(cursor, limit); }); router.get("/:did/notes", (ctx) => { const { did } = ctx.params; const cursor = ctx.request.url.searchParams.get("cursor") ?? undefined; const limit = Number(ctx.request.url.searchParams.get("limit")) || PAGINATION; ctx.response.body = getNotesByDid(did, cursor, limit); }); router.post("/notes/feed", async (ctx) => { const body = await ctx.request.body.json(); const { dids, cursor, limit } = body ?? {}; if (!Array.isArray(dids) || dids.length === 0) { ctx.response.status = 400; ctx.response.body = { error: "dids must be a non-empty array" }; return; } ctx.response.body = getNotesByDids(dids, cursor, Number(limit) || PAGINATION); }); const ALLOWED_VERBS = ["create", "delete", "bulk-create"] as const; router.post("/:did/webhooks", async (ctx) => { const { did } = ctx.params; let verifiedDid: string; try { verifiedDid = await authenticateRequest( ctx.request.headers.get("Authorization"), ); } catch { ctx.response.status = 401; ctx.response.body = { error: "Unauthorized" }; return; } if (verifiedDid !== did) { ctx.response.status = 403; ctx.response.body = { error: "You can only manage your own webhooks" }; return; } const body = await ctx.request.body.json(); const { method, url, token, verb } = body ?? {}; if (!method || !url) { ctx.response.status = 400; ctx.response.body = { error: "method and url are required" }; return; } if (verb !== undefined && !ALLOWED_VERBS.includes(verb)) { ctx.response.status = 400; ctx.response.body = { error: `verb must be one of ${ALLOWED_VERBS.join(", ")}`, }; return; } const verbsToInsert: WebhookVerb[] = verb ? [verb] : ["create", "delete"]; const subscriptions = verbsToInsert.map((v) => addWebhookSubscription({ did, method, url, token, verb: v }) ); ctx.response.status = 201; ctx.response.body = subscriptions; }); router.delete("/:did/webhooks", async (ctx) => { const { did } = ctx.params; let verifiedDid: string; try { verifiedDid = await authenticateRequest( ctx.request.headers.get("Authorization"), ); } catch { ctx.response.status = 401; ctx.response.body = { error: "Unauthorized" }; return; } if (verifiedDid !== did) { ctx.response.status = 403; ctx.response.body = { error: "You can only manage your own webhooks" }; return; } deleteWebhooksByDid(did); ctx.response.status = 204; }); // router.delete("/:did/:rkey", async (ctx) => { // const { did, rkey } = ctx.params; // let verifiedDid: string; // try { // verifiedDid = await authenticateRequest( // ctx.request.headers.get("Authorization"), // ); // } catch { // ctx.response.status = 401; // ctx.response.body = { error: "Unauthorized" }; // return; // } // if (verifiedDid !== did) { // ctx.response.status = 403; // ctx.response.body = { error: "You can only delete your own notes" }; // return; // } // deleteNote({ did, rkey }); // ctx.response.status = 204; // }) const app = new Application(); app.use(async (ctx, next) => { ctx.response.headers.set("Access-Control-Allow-Origin", "*"); ctx.response.headers.set( "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS", ); ctx.response.headers.set( "Access-Control-Allow-Headers", "Content-Type, Authorization", ); if (ctx.request.method === "OPTIONS") { ctx.response.status = 204; return; } await next(); }); app.use(router.routes()); app.use(router.allowedMethods()); log("[server] listening on port 8080"); app.listen({ port: 8080 });