//// The HTTP dispatch table. Each route delegates to a handler module; the //// shared `Context` and request helpers live in `context`. import crate_server/context.{type Context, error_json} import crate_server/handlers/amend import crate_server/handlers/browse import crate_server/handlers/cover_proxy import crate_server/handlers/crate_overlap import crate_server/handlers/debug_index import crate_server/handlers/discogs import crate_server/handlers/edit_inbox import crate_server/handlers/feed import crate_server/handlers/graph import crate_server/handlers/health import crate_server/handlers/oauth import crate_server/handlers/shelf import crate_server/oauth/client_metadata import crate_server/oauth/keys import gleam/http.{Get, Post} import gleam/http/request import gleam/json.{type Json} import gleam/option import gleam/string import wisp.{type Request, type Response} pub fn handle_request(req: Request, ctx: Context) -> Response { use <- bounce_localhost(req, ctx) use <- wisp.serve_static( req, under: "/static", from: ctx.web.static_directory, ) case request.path_segments(req), req.method { ["healthz"], Get -> health.liveness() ["readyz"], Get -> health.readiness(ctx.readiness) ["debug", "index"], Get -> debug_index.status(req, ctx) ["client-metadata.json"], Get -> json_doc(client_metadata.document(ctx.web.base_url)) ["jwks.json"], Get -> json_doc(keys.jwks(ctx.oauth.key)) ["api", "oauth", "login"], Get -> oauth.login(req, ctx) ["api", "oauth", "callback"], Get -> oauth.callback(req, ctx) ["api", "discogs", "connect"], Get -> discogs.connect(req, ctx) ["api", "discogs", "callback"], Get -> discogs.callback(req, ctx) ["api", "cover", did, cid], Get -> cover_proxy.serve(did, cid, ctx) ["xrpc", nsid], method -> dispatch_xrpc(req, ctx, nsid, method) ["api", ..], _ -> error_json(404, "not found") ["xrpc", ..], _ -> method_not_implemented(req.path) _, _ -> serve_index(ctx) } } // One prefix match keeps the namespace in a single place for the eventual // production-NSID swap; the suffix arms stay greppable and mirror the lexicon // method ids. fn dispatch_xrpc( req: Request, ctx: Context, nsid: String, method: http.Method, ) -> Response { case nsid { "dev.mokkenstorm.crate." <> rest -> crate_xrpc(req, ctx, rest, method) _ -> method_not_implemented(nsid) } } fn crate_xrpc( req: Request, ctx: Context, rest: String, method: http.Method, ) -> Response { case rest, method { "server.getSession", Get -> oauth.whoami(req, ctx) "server.deleteSession", Post -> oauth.logout(req, ctx) "catalog.listReleases", Get -> browse.browse(req, ctx) "catalog.getRelease", Get -> browse.get_release(req, ctx) "catalog.adoptRelease", Post -> browse.add(req, ctx) "catalog.listEditProposals", Get -> edit_inbox.list_proposals(req, ctx) "catalog.applyEditProposal", Post -> edit_inbox.apply_proposal(req, ctx) "shelf.listEntries", Get -> shelf.list_shelf(req, ctx) "shelf.getEntry", Get -> shelf.get_entry(req, ctx) "shelf.addEntry", Post -> shelf.add_shelf_item(req, ctx) "shelf.appendEvent", Post -> shelf.append_event(req, ctx) "shelf.purgeEntry", Post -> shelf.purge_entry(req, ctx) "shelf.amendEntry", Post -> amend.amend_entry(req, ctx) "shelf.uploadCover", Post -> amend.upload_cover(req, ctx) "shelf.getCrateOverlap", Get -> crate_overlap.get_crate_overlap(req, ctx) "graph.followUser", Post -> graph.follow(req, ctx) "graph.unfollowUser", Post -> graph.unfollow(req, ctx) "feed.getFeedSkeleton", Get -> feed.get_feed_skeleton(req, ctx) "discogs.searchReleases", Get -> discogs.search(req, ctx) "discogs.searchArtists", Get -> discogs.search_artists(req, ctx) "discogs.resolveBarcode", Get -> discogs.scan(req, ctx) "discogs.getScanSeen", Get -> discogs.scan_seen(req, ctx) "discogs.getStatus", Get -> discogs.status(req, ctx) "discogs.disconnectAccount", Post -> discogs.disconnect(req, ctx) "discogs.importCollection", Post -> discogs.import_collection(req, ctx) "discogs.importWantlist", Post -> discogs.import_wantlist(req, ctx) _, _ -> method_not_implemented("dev.mokkenstorm.crate." <> rest) } } fn method_not_implemented(method_name: String) -> Response { json.object([ #("error", json.string("MethodNotImplemented")), #("message", json.string("unknown XRPC method: " <> method_name)), ]) |> json.to_string |> wisp.json_response(404) } // Host-specific cookies + loopback-IP callbacks make a "localhost" tab a trap (dev only; hosted deploys use an https BASE_URL). fn bounce_localhost( req: Request, ctx: Context, next: fn() -> Response, ) -> Response { case req.host == "localhost" && !string.starts_with(ctx.web.base_url, "https://") { True -> wisp.redirect( ctx.web.base_url <> req.path <> case req.query { option.Some(q) -> "?" <> q option.None -> "" }, ) False -> next() } } fn json_doc(doc: Json) -> Response { doc |> json.to_string |> wisp.json_response(200) } fn serve_index(ctx: Context) -> Response { wisp.response(200) |> wisp.set_header("content-type", "text/html; charset=utf-8") |> wisp.set_body(wisp.File( path: ctx.web.static_directory <> "/index.html", offset: 0, limit: option.None, )) }