Read OAuth2 scopes from GraphQL on the connector frontend

Each component that initiates an OAuth2 flow now reads its scopes
from the colocated Relay fragment instead of a hardcoded TypeScript
map. The five live call sites pass scopes to the backend via the
new ?scope= query parameter.

Drop the dead CreateAccessSourceDialog React component and rename
the file to accessSourceMutations.ts since only the mutation export
was used.

Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
Aurélien Sibiril
2026-04-07 14:53:43 +02:00
parent 247cae14fa
commit 614b4a2384
9 changed files with 72 additions and 379 deletions

View File

@@ -21,6 +21,7 @@ import { GoogleWorkspaceConnector } from "./GoogleWorkspaceConnector";
const connectorListFragment = graphql` const connectorListFragment = graphql`
fragment ConnectorListFragment on Organization { fragment ConnectorListFragment on Organization {
googleWorkspaceOAuth2Scopes
scimConfiguration { scimConfiguration {
...GoogleWorkspaceConnectorFragment ...GoogleWorkspaceConnectorFragment
} }
@@ -40,7 +41,10 @@ export function ConnectorList(props: { fKey: ConnectorListFragment$key }) {
"Connect your identity provider to automatically sync users to your organization. Once connected, you don't need to configure SCIM manually.", "Connect your identity provider to automatically sync users to your organization. Once connected, you don't need to configure SCIM manually.",
)} )}
</p> </p>
<GoogleWorkspaceConnector fKey={data.scimConfiguration ?? null} /> <GoogleWorkspaceConnector
fKey={data.scimConfiguration ?? null}
oauth2Scopes={data.googleWorkspaceOAuth2Scopes}
/>
</div> </div>
); );
} }

View File

@@ -74,8 +74,9 @@ const updateSCIMBridgeMutation = graphql`
export function GoogleWorkspaceConnector(props: { export function GoogleWorkspaceConnector(props: {
fKey: GoogleWorkspaceConnectorFragment$key | null; fKey: GoogleWorkspaceConnectorFragment$key | null;
oauth2Scopes: readonly string[];
}) { }) {
const { fKey } = props; const { fKey, oauth2Scopes } = props;
const data = useFragment<GoogleWorkspaceConnectorFragment$key>(googleWorkspaceConnectorFragment, fKey); const data = useFragment<GoogleWorkspaceConnectorFragment$key>(googleWorkspaceConnectorFragment, fKey);
const bridge = data?.bridge; const bridge = data?.bridge;
const connector = bridge?.connector; const connector = bridge?.connector;
@@ -112,6 +113,9 @@ export function GoogleWorkspaceConnector(props: {
const url = new URL("/api/console/v1/connectors/initiate", baseUrl); const url = new URL("/api/console/v1/connectors/initiate", baseUrl);
url.searchParams.append("organization_id", organizationId); url.searchParams.append("organization_id", organizationId);
url.searchParams.append("provider", "GOOGLE_WORKSPACE"); url.searchParams.append("provider", "GOOGLE_WORKSPACE");
for (const scope of oauth2Scopes) {
url.searchParams.append("scope", scope);
}
const continueUrl = `/organizations/${organizationId}/settings/scim`; const continueUrl = `/organizations/${organizationId}/settings/scim`;
url.searchParams.append("continue", continueUrl); url.searchParams.append("continue", continueUrl);
window.location.href = url.toString(); window.location.href = url.toString();

View File

@@ -27,12 +27,12 @@ import { Link, useNavigate } from "react-router";
import { ConnectionHandler, graphql } from "relay-runtime"; import { ConnectionHandler, graphql } from "relay-runtime";
import { z } from "zod"; import { z } from "zod";
import type { CreateAccessSourceDialogMutation } from "#/__generated__/core/CreateAccessSourceDialogMutation.graphql"; import type { accessSourceMutationsCreateMutation } from "#/__generated__/core/accessSourceMutationsCreateMutation.graphql";
import type { CreateCsvAccessSourcePageQuery } from "#/__generated__/core/CreateCsvAccessSourcePageQuery.graphql"; import type { CreateCsvAccessSourcePageQuery } from "#/__generated__/core/CreateCsvAccessSourcePageQuery.graphql";
import { useFormWithSchema } from "#/hooks/useFormWithSchema"; import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
import { createAccessSourceMutation } from "./dialogs/CreateAccessSourceDialog"; import { createAccessSourceMutation } from "./dialogs/accessSourceMutations";
export const createCsvAccessSourcePageQuery = graphql` export const createCsvAccessSourcePageQuery = graphql`
query CreateCsvAccessSourcePageQuery($organizationId: ID!) { query CreateCsvAccessSourcePageQuery($organizationId: ID!) {
@@ -81,7 +81,7 @@ export default function CreateCsvAccessSourcePage({
); );
const [createAccessSource, isCreating] const [createAccessSource, isCreating]
= useMutation<CreateAccessSourceDialogMutation>( = useMutation<accessSourceMutationsCreateMutation>(
createAccessSourceMutation, createAccessSourceMutation,
); );

View File

@@ -45,6 +45,7 @@ const fragment = graphql`
connector { connector {
provider provider
} }
oauth2Scopes
connectionStatus connectionStatus
selectedOrganization selectedOrganization
needsConfiguration needsConfiguration
@@ -213,6 +214,9 @@ export function AccessSourceRow({ fKey, connectionId, organizationId }: Props) {
url.searchParams.append("organization_id", organizationId); url.searchParams.append("organization_id", organizationId);
url.searchParams.append("provider", provider); url.searchParams.append("provider", provider);
url.searchParams.append("connector_id", accessSource.connectorId); url.searchParams.append("connector_id", accessSource.connectorId);
for (const scope of accessSource.oauth2Scopes) {
url.searchParams.append("scope", scope);
}
url.searchParams.append( url.searchParams.append(
"continue", "continue",
`/organizations/${organizationId}/access-reviews/sources`, `/organizations/${organizationId}/access-reviews/sources`,

View File

@@ -40,9 +40,9 @@ import { graphql } from "relay-runtime";
import type { AddAccessSourceDialogConnectorProviderInfoFragment$data } from "#/__generated__/core/AddAccessSourceDialogConnectorProviderInfoFragment.graphql"; import type { AddAccessSourceDialogConnectorProviderInfoFragment$data } from "#/__generated__/core/AddAccessSourceDialogConnectorProviderInfoFragment.graphql";
import type { AddAccessSourceDialogCreateAPIKeyConnectorMutation } from "#/__generated__/core/AddAccessSourceDialogCreateAPIKeyConnectorMutation.graphql"; import type { AddAccessSourceDialogCreateAPIKeyConnectorMutation } from "#/__generated__/core/AddAccessSourceDialogCreateAPIKeyConnectorMutation.graphql";
import type { AddAccessSourceDialogCreateClientCredentialsConnectorMutation } from "#/__generated__/core/AddAccessSourceDialogCreateClientCredentialsConnectorMutation.graphql"; import type { AddAccessSourceDialogCreateClientCredentialsConnectorMutation } from "#/__generated__/core/AddAccessSourceDialogCreateClientCredentialsConnectorMutation.graphql";
import type { CreateAccessSourceDialogMutation } from "#/__generated__/core/CreateAccessSourceDialogMutation.graphql"; import type { accessSourceMutationsCreateMutation } from "#/__generated__/core/accessSourceMutationsCreateMutation.graphql";
import { createAccessSourceMutation } from "./CreateAccessSourceDialog"; import { createAccessSourceMutation } from "./accessSourceMutations";
export const addAccessSourceDialogConnectorProviderInfoFragment = graphql` export const addAccessSourceDialogConnectorProviderInfoFragment = graphql`
fragment AddAccessSourceDialogConnectorProviderInfoFragment on ConnectorProviderInfo @relay(plural: true) { fragment AddAccessSourceDialogConnectorProviderInfoFragment on ConnectorProviderInfo @relay(plural: true) {
@@ -51,6 +51,7 @@ export const addAccessSourceDialogConnectorProviderInfoFragment = graphql`
oauthConfigured oauthConfigured
apiKeySupported apiKeySupported
clientCredentialsSupported clientCredentialsSupported
oauth2Scopes
extraSettings { extraSettings {
key key
label label
@@ -185,7 +186,7 @@ export function AddAccessSourceDialog({
); );
const [createAccessSource] const [createAccessSource]
= useMutation<CreateAccessSourceDialogMutation>( = useMutation<accessSourceMutationsCreateMutation>(
createAccessSourceMutation, createAccessSourceMutation,
); );
const [createAPIKeyConnector] const [createAPIKeyConnector]
@@ -197,11 +198,14 @@ export function AddAccessSourceDialog({
createClientCredentialsConnectorMutation, createClientCredentialsConnectorMutation,
); );
const connectOAuthProvider = (provider: string) => { const connectOAuthProvider = (info: ProviderInfo) => {
const baseURL = import.meta.env.VITE_API_URL || window.location.origin; const baseURL = import.meta.env.VITE_API_URL || window.location.origin;
const url = new URL("/api/console/v1/connectors/initiate", baseURL); const url = new URL("/api/console/v1/connectors/initiate", baseURL);
url.searchParams.append("organization_id", organizationId); url.searchParams.append("organization_id", organizationId);
url.searchParams.append("provider", provider); url.searchParams.append("provider", info.provider);
for (const scope of info.oauth2Scopes) {
url.searchParams.append("scope", scope);
}
url.searchParams.append( url.searchParams.append(
"continue", "continue",
`/organizations/${organizationId}/access-reviews/sources`, `/organizations/${organizationId}/access-reviews/sources`,
@@ -419,7 +423,7 @@ export function AddAccessSourceDialog({
return ( return (
<Button <Button
variant="secondary" variant="secondary"
onClick={() => connectOAuthProvider(info.provider)} onClick={() => connectOAuthProvider(info)}
> >
{__("Connect")} {__("Connect")}
</Button> </Button>

View File

@@ -1,361 +0,0 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { formatError, type GraphQLError } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import {
Breadcrumb,
Button,
Dialog,
DialogContent,
DialogFooter,
Field,
Option,
Select,
useDialogRef,
useToast,
} from "@probo/ui";
import { type ReactNode, useEffect, useMemo } from "react";
import { Controller, useWatch } from "react-hook-form";
import { graphql, useMutation } from "react-relay";
import { useSearchParams } from "react-router";
import { z } from "zod";
import type { CreateAccessSourceDialogMutation } from "#/__generated__/core/CreateAccessSourceDialogMutation.graphql";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
export const createAccessSourceMutation = graphql`
mutation CreateAccessSourceDialogMutation(
$input: CreateAccessSourceInput!
$connections: [ID!]!
) {
createAccessSource(input: $input) {
accessSourceEdge @prependEdge(connections: $connections) {
node {
id
name
createdAt
...AccessSourceRowFragment
}
}
}
}
`;
type Props = {
children: ReactNode;
organizationId: string;
connectionId: string;
connectors: ReadonlyArray<{
readonly id: string;
readonly provider: "GOOGLE_WORKSPACE" | "LINEAR" | "SLACK";
readonly createdAt: string;
}>;
preselectedConnectorId: string | null;
};
const schema = z.object({
name: z.string().min(1),
sourceType: z.enum(["CSV", "OAUTH2"]),
provider: z.enum(["GOOGLE_WORKSPACE", "LINEAR", "SLACK"]).optional(),
connectorId: z.string().optional(),
csvData: z.string().optional(),
}).superRefine((data, ctx) => {
if (data.sourceType === "CSV" && !data.csvData?.trim()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["csvData"],
message: "CSV data is required for CSV sources.",
});
}
if (data.sourceType === "OAUTH2") {
if (!data.provider) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["provider"],
message: "Provider is required for OAuth2 sources.",
});
}
if (!data.connectorId) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["connectorId"],
message: "Connector is required for OAuth2 sources.",
});
}
}
});
function providerLabel(provider: "GOOGLE_WORKSPACE" | "LINEAR" | "SLACK") {
switch (provider) {
case "GOOGLE_WORKSPACE":
return "Google Workspace";
case "LINEAR":
return "Linear";
case "SLACK":
return "Slack";
default:
return provider;
}
}
export function CreateAccessSourceDialog({
children,
organizationId,
connectionId,
connectors,
preselectedConnectorId,
}: Props) {
const { __ } = useTranslate();
const { toast } = useToast();
const [searchParams, setSearchParams] = useSearchParams();
const preselectedConnector = useMemo(
() => connectors.find(connector => connector.id === preselectedConnectorId),
[connectors, preselectedConnectorId],
);
const { control, register, handleSubmit, reset, setValue }
= useFormWithSchema(
schema,
{
defaultValues: {
name: "",
sourceType: preselectedConnector ? "OAUTH2" : "CSV",
provider: preselectedConnector?.provider ?? "GOOGLE_WORKSPACE",
connectorId: preselectedConnector?.id,
csvData: "",
},
},
);
const sourceType = useWatch({ control, name: "sourceType" });
const provider = useWatch({ control, name: "provider" });
const connectorId = useWatch({ control, name: "connectorId" });
const ref = useDialogRef();
const providerConnectors = useMemo(
() => connectors,
[connectors],
);
const selectableConnectors = useMemo(
() =>
providerConnectors.filter(
connector => !provider || connector.provider === provider,
),
[provider, providerConnectors],
);
useEffect(() => {
if (!provider) {
setValue("connectorId", undefined);
return;
}
if (
connectorId
&& !selectableConnectors.some(connector => connector.id === connectorId)
) {
setValue("connectorId", undefined);
}
}, [provider, connectorId, selectableConnectors, setValue]);
useEffect(() => {
if (!preselectedConnector) return;
setValue("sourceType", "OAUTH2");
setValue("provider", preselectedConnector.provider);
setValue("connectorId", preselectedConnector.id);
}, [preselectedConnector, setValue]);
const [createAccessSource, isCreating]
= useMutation<CreateAccessSourceDialogMutation>(
createAccessSourceMutation,
);
const clearConnectorQueryParam = () => {
if (!searchParams.get("connector_id")) {
return;
}
setSearchParams((params) => {
params.delete("connector_id");
return params;
});
};
const startOAuthConnection = () => {
if (!provider) {
return;
}
const baseURL = import.meta.env.VITE_API_URL || window.location.origin;
const url = new URL("/api/console/v1/connectors/initiate", baseURL);
url.searchParams.append("organization_id", organizationId);
url.searchParams.append("provider", provider);
url.searchParams.append("continue", `/organizations/${organizationId}/access-reviews`);
window.location.href = url.toString();
};
const onSubmit = (data: z.infer<typeof schema>) => {
const isOAuth = data.sourceType === "OAUTH2";
createAccessSource({
variables: {
input: {
organizationId,
connectorId: isOAuth ? data.connectorId : null,
name: data.name,
csvData: isOAuth ? null : (data.csvData || null),
},
connections: [connectionId],
},
onCompleted(_, errors) {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(
__("Failed to create access source"),
errors as GraphQLError[],
),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Access source created successfully."),
variant: "success",
});
clearConnectorQueryParam();
reset();
ref.current?.close();
},
onError(error) {
toast({
title: __("Error"),
description: formatError(
__("Failed to create access source"),
error as GraphQLError,
),
variant: "error",
});
},
});
};
return (
<Dialog
ref={ref}
trigger={children}
title={(
<Breadcrumb
items={[
__("Access Reviews"),
__("New Access Source"),
]}
/>
)}
>
<form onSubmit={e => void handleSubmit(onSubmit)(e)}>
<DialogContent padded className="space-y-4">
<Field
label={__("Name")}
{...register("name")}
type="text"
required
/>
<Field label={__("Source type")}>
<Controller
control={control}
name="sourceType"
render={({ field }) => (
<Select value={field.value} onValueChange={field.onChange}>
<Option value="CSV">{__("CSV Upload")}</Option>
<Option value="OAUTH2">{__("OAuth2 Connector")}</Option>
</Select>
)}
/>
</Field>
{sourceType === "OAUTH2" && (
<>
<Field label={__("Provider")}>
<Controller
control={control}
name="provider"
render={({ field }) => (
<Select
value={field.value}
onValueChange={(value) => {
field.onChange(value);
setValue("connectorId", undefined);
}}
>
<Option value="GOOGLE_WORKSPACE">{__("Google Workspace")}</Option>
<Option value="LINEAR">{__("Linear")}</Option>
<Option value="SLACK">{__("Slack")}</Option>
</Select>
)}
/>
</Field>
<Field label={__("Connector")}>
<Controller
control={control}
name="connectorId"
render={({ field }) => (
<Select
value={field.value}
onValueChange={field.onChange}
>
{selectableConnectors.map(connector => (
<Option key={connector.id} value={connector.id}>
{`${providerLabel(connector.provider)} (${new Date(connector.createdAt).toLocaleDateString(undefined, { month: "short", year: "numeric" })})`}
</Option>
))}
</Select>
)}
/>
</Field>
<div className="flex items-center justify-between gap-3">
<p className="text-txt-secondary text-sm">
{__("Need a new connection? Connect your provider and come back to continue creating this source.")}
</p>
<Button
type="button"
variant="secondary"
onClick={startOAuthConnection}
disabled={!provider}
>
{__("Connect")}
</Button>
</div>
</>
)}
{sourceType === "CSV" && (
<>
<Field
label={__("CSV Data")}
{...register("csvData")}
type="textarea"
placeholder="email,full_name,role,job_title,is_admin,active,external_id"
/>
<p className="text-txt-secondary text-sm">
{__("Paste CSV content with a header row. Supported columns: email, full_name, role, job_title, is_admin, active, external_id.")}
</p>
</>
)}
</DialogContent>
<DialogFooter>
<Button disabled={isCreating} type="submit">
{__("Create")}
</Button>
</DialogFooter>
</form>
</Dialog>
);
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
import { graphql } from "relay-runtime";
export const createAccessSourceMutation = graphql`
mutation accessSourceMutationsCreateMutation(
$input: CreateAccessSourceInput!
$connections: [ID!]!
) {
createAccessSource(input: $input) {
accessSourceEdge @prependEdge(connections: $connections) {
node {
id
name
createdAt
...AccessSourceRowFragment
}
}
}
}
`;

View File

@@ -34,12 +34,12 @@ import type { AccessReviewSourcesTabFragment$key } from "#/__generated__/core/Ac
import type { AccessReviewSourcesTabPaginationQuery } from "#/__generated__/core/AccessReviewSourcesTabPaginationQuery.graphql"; import type { AccessReviewSourcesTabPaginationQuery } from "#/__generated__/core/AccessReviewSourcesTabPaginationQuery.graphql";
import type { AccessReviewSourcesTabQuery } from "#/__generated__/core/AccessReviewSourcesTabQuery.graphql"; import type { AccessReviewSourcesTabQuery } from "#/__generated__/core/AccessReviewSourcesTabQuery.graphql";
import type { AddAccessSourceDialogConnectorProviderInfoFragment$key } from "#/__generated__/core/AddAccessSourceDialogConnectorProviderInfoFragment.graphql"; import type { AddAccessSourceDialogConnectorProviderInfoFragment$key } from "#/__generated__/core/AddAccessSourceDialogConnectorProviderInfoFragment.graphql";
import type { CreateAccessSourceDialogMutation } from "#/__generated__/core/CreateAccessSourceDialogMutation.graphql"; import type { accessSourceMutationsCreateMutation } from "#/__generated__/core/accessSourceMutationsCreateMutation.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId"; import { useOrganizationId } from "#/hooks/useOrganizationId";
import { AccessSourceRow } from "../_components/AccessSourceRow"; import { AccessSourceRow } from "../_components/AccessSourceRow";
import { AddAccessSourceDialog, addAccessSourceDialogConnectorProviderInfoFragment } from "../dialogs/AddAccessSourceDialog"; import { AddAccessSourceDialog, addAccessSourceDialogConnectorProviderInfoFragment } from "../dialogs/AddAccessSourceDialog";
import { createAccessSourceMutation } from "../dialogs/CreateAccessSourceDialog"; import { createAccessSourceMutation } from "../dialogs/accessSourceMutations";
export const accessReviewSourcesTabQuery = graphql` export const accessReviewSourcesTabQuery = graphql`
query AccessReviewSourcesTabQuery($organizationId: ID!) { query AccessReviewSourcesTabQuery($organizationId: ID!) {
@@ -131,7 +131,7 @@ export default function AccessReviewSourcesTab({ queryRef }: Props) {
); );
const [createAccessSource, isCreatingSource] const [createAccessSource, isCreatingSource]
= useMutation<CreateAccessSourceDialogMutation>( = useMutation<accessSourceMutationsCreateMutation>(
createAccessSourceMutation, createAccessSourceMutation,
); );

View File

@@ -25,6 +25,7 @@ import { useOrganizationId } from "#/hooks/useOrganizationId";
const fragment = graphql` const fragment = graphql`
fragment CompliancePageSlackSectionFragment on Organization { fragment CompliancePageSlackSectionFragment on Organization {
canConnectSlack: permission(action: "core:connector:initiate") canConnectSlack: permission(action: "core:connector:initiate")
slackOAuth2Scopes
slackConnections(first: 100) { slackConnections(first: 100) {
__id __id
edges { edges {
@@ -144,7 +145,9 @@ export function CompliancePageSlackSection(props: { fragmentRef: CompliancePageS
</p> </p>
</div> </div>
<Button variant="secondary" asChild> <Button variant="secondary" asChild>
<a href={getSlackConnectionUrl(organizationId)}>{__("Connect")}</a> <a href={getSlackConnectionUrl(organizationId, organization.slackOAuth2Scopes)}>
{__("Connect")}
</a>
</Button> </Button>
</Card> </Card>
)} )}
@@ -153,13 +156,15 @@ export function CompliancePageSlackSection(props: { fragmentRef: CompliancePageS
); );
} }
function getSlackConnectionUrl(organizationId: string): string { function getSlackConnectionUrl(organizationId: string, scopes: readonly string[]): string {
const baseUrl = import.meta.env.VITE_API_URL || window.location.origin; const baseUrl = import.meta.env.VITE_API_URL || window.location.origin;
const url = new URL("/api/console/v1/connectors/initiate", baseUrl); const url = new URL("/api/console/v1/connectors/initiate", baseUrl);
url.searchParams.append("organization_id", organizationId); url.searchParams.append("organization_id", organizationId);
url.searchParams.append("provider", "SLACK"); url.searchParams.append("provider", "SLACK");
for (const scope of scopes) {
url.searchParams.append("scope", scope);
}
const redirectUrl = `/organizations/${organizationId}/compliance-page`; const redirectUrl = `/organizations/${organizationId}/compliance-page`;
url.searchParams.append("continue", redirectUrl); url.searchParams.append("continue", redirectUrl);
const finalUrl = url.toString(); return url.toString();
return finalUrl;
} }