| 1 | import type { Route } from "./+types/ticket"; |
| 2 | import { getViewer, roleIn } from "../../lib/session.server"; |
| 3 | import { issueTicket, socketPath } from "../../lib/socket-ticket"; |
| 4 | import { ticketSecret } from "../../lib/socket-ticket.server"; |
| 5 | import { bearerToken, socketUser } from "../../lib/website-token"; |
| 6 | |
| 7 | const PRIVATE = { "cache-control": "no-store", "x-robots-tag": "noindex" }; |
| 8 | |
| 9 | /** |
| 10 | * A socket ticket for a page opened with an access token: |
| 11 | * `GET /-/live/ticket?path=/<workspace>/-/chat/live` answers |
| 12 | * `{ ticket, expires_at }`, good for a minute on that socket alone |
| 13 | * (lib/socket-ticket.ts). Only a request signed in by a token gets one: a |
| 14 | * session's sockets carry its cookie and need none. A token let in by its |
| 15 | * `chat:read` scope rather than the website permission gets one for Chat's |
| 16 | * socket and the feed (`/-/live`) alone (lib/website-token.ts `socketUser`). |
| 17 | */ |
| 18 | export async function loader({ context, request }: Route.LoaderArgs) { |
| 19 | const viewer = getViewer(context); |
| 20 | const token = bearerToken(request); |
| 21 | if (!viewer || !token || !viewer.token) { |
| 22 | return Response.json( |
| 23 | { message: "Socket tickets are for pages opened with an access token; a signed-in browser's sockets use its session." }, |
| 24 | { status: viewer ? 400 : 401, headers: PRIVATE }, |
| 25 | ); |
| 26 | } |
| 27 | const socket = socketPath(new URL(request.url).searchParams.get("path") ?? ""); |
| 28 | if (!socket) return Response.json({ message: "Which socket? `path` is one of the site's live sockets." }, { status: 400, headers: PRIVATE }); |
| 29 | if (!socketUser(viewer, socket.path)) { |
| 30 | return Response.json({ message: "This access token can open Chat's sockets and your feed, not this one." }, { status: 403, headers: PRIVATE }); |
| 31 | } |
| 32 | if (socket.workspace && !roleIn(viewer, socket.workspace)) { |
| 33 | return Response.json({ message: "Not found" }, { status: 404, headers: PRIVATE }); |
| 34 | } |
| 35 | const issued = await issueTicket(ticketSecret(), { token, userId: viewer.id, path: socket.path }); |
| 36 | return Response.json(issued, { headers: PRIVATE }); |
| 37 | } |