| 1 | /** |
| 2 | * What Customize's pages do on the server besides reading: the connector |
| 3 | * actions every page with a connector shares (signing in and out, a key, |
| 4 | * listing tools again, where it reaches, removing it), and what follows |
| 5 | * adding one: the browser goes to sign in, or to the connector's page. |
| 6 | */ |
| 7 | import { type ShouldRevalidateFunctionArgs, redirect } from "react-router"; |
| 8 | |
| 9 | import type { ConnectorInput, User, WorkspaceConnector } from "@g1t/contracts"; |
| 10 | |
| 11 | import { addresses } from "./addresses.server"; |
| 12 | import { connectorCallbackPath, connectorPagePath, needsReload, splitTarget } from "./customize"; |
| 13 | import { workspaceAgents } from "./services.server"; |
| 14 | import type { ActionResult } from "../components/agents/dialogs"; |
| 15 | import { answer } from "../components/agents/actions.server"; |
| 16 | |
| 17 | /** Where a connector's sign-in comes back to: this g1t's own page. */ |
| 18 | export function callbackUri(slug: string): string { |
| 19 | return `${addresses().site}${connectorCallbackPath(slug)}`; |
| 20 | } |
| 21 | |
| 22 | /** A Customize page reads its data again only for another page, the other view, or a posted form (lib/customize.ts). */ |
| 23 | export function shouldRevalidate({ currentUrl, nextUrl, formMethod, defaultShouldRevalidate }: ShouldRevalidateFunctionArgs): boolean { |
| 24 | return needsReload(currentUrl, nextUrl, formMethod) ? defaultShouldRevalidate : false; |
| 25 | } |
| 26 | |
| 27 | /** The browser leaves for the connector's sign-in page, or lands on the connector's own page with what happened. */ |
| 28 | async function toSignIn(slug: string, viewer: User, id: string): Promise<Response> { |
| 29 | const started = await workspaceAgents.startConnectorSignIn(slug, viewer, id, callbackUri(slug)).catch(() => null); |
| 30 | if (started?.ok) return redirect(started.value.url); |
| 31 | const problem = started ? started.error.message : "The agents service didn't answer. Try again in a moment."; |
| 32 | return redirect(`${connectorPagePath(slug, id)}?problem=${encodeURIComponent(problem)}`); |
| 33 | } |
| 34 | |
| 35 | /** |
| 36 | * Adds a connector and takes the browser where it goes next: to sign in |
| 37 | * when the server asks for that, or to the connector's page otherwise. An |
| 38 | * answer (not a redirect) says why it couldn't be added. |
| 39 | */ |
| 40 | export async function addConnectorThen(slug: string, viewer: User, input: ConnectorInput): Promise<Response | ActionResult> { |
| 41 | const added = await workspaceAgents.addConnector(slug, viewer, input).catch(() => null); |
| 42 | if (!added) return { ok: false, intent: "connect", error: "The agents service didn't answer. Try again in a moment." }; |
| 43 | if (!added.ok) return { ok: false, intent: "connect", error: added.error.message, code: added.error.code }; |
| 44 | return afterAdd(slug, viewer, added.value); |
| 45 | } |
| 46 | |
| 47 | /** After a connector is added: the sign-in when it needs one, its page otherwise. */ |
| 48 | export function afterAdd(slug: string, viewer: User, connector: WorkspaceConnector): Promise<Response> | Response { |
| 49 | if (connector.state === "needs_sign_in") return toSignIn(slug, viewer, connector.id); |
| 50 | return redirect(`${connectorPagePath(slug, connector.id)}?added=1`); |
| 51 | } |
| 52 | |
| 53 | /** |
| 54 | * The actions a connector has wherever it is shown. `intent` names one; |
| 55 | * null says it isn't one of them, so the page's own action goes on. |
| 56 | * `afterRemove` is where the browser goes once it is removed. |
| 57 | */ |
| 58 | export async function connectorAction(intent: string, form: FormData, slug: string, viewer: User, afterRemove: string | null): Promise<Response | ActionResult | null> { |
| 59 | const id = String(form.get("connector") ?? ""); |
| 60 | switch (intent) { |
| 61 | case "sign_in": |
| 62 | return toSignIn(slug, viewer, id); |
| 63 | case "sign_out": |
| 64 | return answer(intent, workspaceAgents.signOutConnector(slug, viewer, id)); |
| 65 | case "key": |
| 66 | return answer(intent, workspaceAgents.setConnectorKey(slug, viewer, id, String(form.get("header_name") ?? "") || null, String(form.get("value") ?? ""))); |
| 67 | case "refresh": |
| 68 | return answer(intent, workspaceAgents.refreshConnector(slug, viewer, id)); |
| 69 | case "grant": { |
| 70 | const { scope, target } = splitTarget(String(form.get("where") ?? "")); |
| 71 | return answer(intent, workspaceAgents.grantConnector(slug, viewer, id, scope, target)); |
| 72 | } |
| 73 | case "revoke": |
| 74 | return answer(intent, workspaceAgents.revokeConnector(slug, viewer, id, String(form.get("grant") ?? ""))); |
| 75 | case "remove": { |
| 76 | const result = await answer(intent, workspaceAgents.removeConnector(slug, viewer, id)); |
| 77 | return result.ok && afterRemove ? redirect(afterRemove) : result; |
| 78 | } |
| 79 | default: |
| 80 | return null; |
| 81 | } |
| 82 | } |