Skip to content
82 linesCodeBlameRaw
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 */
7import { type ShouldRevalidateFunctionArgs, redirect } from "react-router";
8
9import type { ConnectorInput, User, WorkspaceConnector } from "@g1t/contracts";
10
11import { addresses } from "./addresses.server";
12import { connectorCallbackPath, connectorPagePath, needsReload, splitTarget } from "./customize";
13import { workspaceAgents } from "./services.server";
14import type { ActionResult } from "../components/agents/dialogs";
15import { answer } from "../components/agents/actions.server";
16
17/** Where a connector's sign-in comes back to: this g1t's own page. */
18export 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). */
23export 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. */
28async 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 */
40export 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. */
48export 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 */
58export 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}