Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function deleteOIDCProvider(
return {
response: {
code: EnumStatusCode.ERR_NOT_FOUND,
details: `Organization ${authContext.organizationSlug} doesn't have an oidc identity provider `,
details: `Organization ${authContext.organizationSlug} doesn't have an OIDC provider `,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Trailing space in error message.

There's a trailing space at the end of the error details string.

Proposed fix
-          details: `Organization ${authContext.organizationSlug} doesn't have an OIDC provider `,
+          details: `Organization ${authContext.organizationSlug} doesn't have an OIDC provider`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
details: `Organization ${authContext.organizationSlug} doesn't have an OIDC provider `,
details: `Organization ${authContext.organizationSlug} doesn't have an OIDC provider`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controlplane/src/core/bufservices/sso/deleteOIDCProvider.ts` at line 51, In
deleteOIDCProvider.ts update the error details string that currently reads
`details: \`Organization ${authContext.organizationSlug} doesn't have an OIDC
provider \`,` to remove the trailing space—make the details value `Organization
${authContext.organizationSlug} doesn't have an OIDC provider` so the error
message has no trailing whitespace.

},
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,48 +20,12 @@ import { clsx } from 'clsx';
import { useCurrentOrganization } from '@/hooks/use-current-organization';
import { useWorkspace } from '@/hooks/use-workspace';
import Link from 'next/link';
import { absoluteUrlValidator } from '@/lib/zod';

export type SubgraphCheckExtensionsConfig = Omit<PlainMessage<ConfigureSubgraphCheckExtensionsRequest>, 'namespace'>;

const validationSchema = z.object({
endpoint: z
.string()
.trim()
.superRefine((val, ctx) => {
if (!val) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Must be a valid absolute URL starting with https://',
});
return;
}

try {
const url = new URL(val); // Ensure that the value is a valid absolute URL
if (url.hostname === 'localhost') {
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Must be a valid absolute URL starting with http:// or https://',
});
}

return;
}

if (url.protocol !== 'https:') {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Must be a valid absolute URL starting with https://',
});
}
} catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Must be a valid absolute URL starting with https://',
});
}
}),
endpoint: absoluteUrlValidator,
secretKey: z.string().trim().optional(),
includeComposedSdl: z.boolean(),
includeLintingIssues: z.boolean(),
Expand Down
118 changes: 118 additions & 0 deletions studio/src/components/oidc/connect-oidc-provider-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import { createOIDCProvider } from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery';
import { useState } from 'react';
import { useIsAdmin } from '@/hooks/use-is-admin';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import { docsBaseURL } from '@/lib/constants';
import { OIDCForm, OIDCProviderInput } from './oidc-form';
import { useMutation } from '@connectrpc/connect-query';
import { useToast } from '@/components/ui/use-toast';

export interface ConnectOIDCProviderDialogProps {
isProviderConnected: boolean;
refetch(): Promise<unknown>;
onProviderConnected(): void;
}

export function ConnectOIDCProviderDialog({
isProviderConnected,
refetch,
onProviderConnected,
}: ConnectOIDCProviderDialogProps) {
const isAdmin = useIsAdmin();
const [open, setOpen] = useState(false);
const [isPending, setPending] = useState(false);
const [error, setError] = useState<string | null>(null);
const { toast } = useToast();
const { mutate } = useMutation(createOIDCProvider);

const onOpenChangeCallback = (open: boolean) => {
if (isPending && !open) {
return;
}

setPending(false);
setOpen(open);
setError(null);
};

const handleSubmit = (data: OIDCProviderInput) => {
if (!isAdmin || isPending) {
return;
}

setPending(true);
mutate(data, {
onSuccess(data) {
if (data.response?.code === EnumStatusCode.OK) {
refetch().finally(() => {
setOpen(false);
toast({
description: 'OIDC provider connected successfully.',
duration: 4000,
});

onProviderConnected();
});
} else {
setPending(false);
setError(
data.response?.details || 'Could not connect the OIDC provider to the organization. Please try again.',
);
}
},
onError() {
setPending(false);
setError('Could not connect the OIDC provider to the organization. Please try again.');
},
});
};

return (
<Dialog open={!isProviderConnected && isAdmin && open} onOpenChange={onOpenChangeCallback}>
{!isProviderConnected && (
<DialogTrigger asChild>
<Button className="md:ml-auto" variant="default">
Connect
</Button>
</DialogTrigger>
)}
<DialogContent>
<DialogHeader>
<DialogTitle>Connect OpenID Connect Provider</DialogTitle>
<DialogDescription className="flex flex-col gap-y-2">
<p>
Connecting an OIDC provider to this organization allows users to automatically log in and be part of this
organization.
</p>
<p>Use Okta, Auth0 or any other OAuth2 Open ID Connect compatible provider.</p>
<div>
<Link
href={docsBaseURL + '/studio/sso'}
className="text-sm text-primary"
target="_blank"
rel="noreferrer"
>
Click here{' '}
</Link>
for the step by step guide to configure your OIDC provider.
</div>
</DialogDescription>
</DialogHeader>

{error && <div className="mt-2 rounded bg-destructive p-2 text-destructive-foreground">{error}</div>}

<OIDCForm isPending={isPending} handleSubmit={handleSubmit} onCancel={() => setOpen(false)} />
</DialogContent>
</Dialog>
);
}
118 changes: 118 additions & 0 deletions studio/src/components/oidc/disconnect-oidc-provider-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
AlertDialog,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { EnumStatusCode } from '@wundergraph/cosmo-connect/dist/common/common_pb';
import { useState } from 'react';
import { useIsAdmin } from '@/hooks/use-is-admin';
import { Button, buttonVariants } from '@/components/ui/button';
import { deleteOIDCProvider } from '@wundergraph/cosmo-connect/dist/platform/v1/platform-PlatformService_connectquery';
import { useMutation } from '@connectrpc/connect-query';
import { useToast } from '@/components/ui/use-toast';

export interface DisconnectOIDCProviderDialogProps {
isProviderConnected: boolean;
refetch(): Promise<unknown>;
}

export function DisconnectOIDCProviderDialog({ isProviderConnected, refetch }: DisconnectOIDCProviderDialogProps) {
const isAdmin = useIsAdmin();
const [open, setOpen] = useState(false);
const [isPending, setPending] = useState(false);

const { toast } = useToast();
const { mutate } = useMutation(deleteOIDCProvider);
const onDialogOpenChange = (open: boolean) => {
if (isPending && !open) {
return;
}

setOpen(isProviderConnected && open);
if (open) {
setPending(false);
}
};

const onSubmit = () => {
if (!isProviderConnected || !isAdmin || isPending) {
return;
}

setPending(true);
mutate(
{},
{
onSuccess(data) {
if (data.response?.code === EnumStatusCode.OK) {
refetch().finally(() => {
setOpen(false);

toast({
description: 'OIDC provider disconnected successfully.',
duration: 4000,
});
});
} else {
setPending(false);
toast({
description: data.response?.details || 'Could not disconnect the OIDC provider. Please try again.',
duration: 4000,
});
}
},
onError() {
setPending(false);
toast({
description: 'Could not disconnect the OIDC provider. Please try again.',
duration: 4000,
});
},
},
);
};

return (
<AlertDialog open={isProviderConnected && isAdmin && open} onOpenChange={onDialogOpenChange}>
{isProviderConnected && (
<AlertDialogTrigger asChild>
<Button className="md:ml-auto" variant="destructive">
Disconnect
</Button>
</AlertDialogTrigger>
)}

<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you sure you want to disconnect the OIDC provider?</AlertDialogTitle>
<AlertDialogDescription className="flex flex-col gap-y-1" asChild>
<div>
<p>
All members who are connected to the OIDC provider will be logged out and downgraded to the viewer role.
</p>
<p>Reconnecting will result in a new login url.</p>
<p>This action cannot be undone.</p>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isPending}>Cancel</AlertDialogCancel>
<Button
className={buttonVariants({ variant: 'destructive' })}
type="button"
disabled={!isAdmin || isPending}
isLoading={isPending}
onClick={onSubmit}
>
Disconnect
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
98 changes: 98 additions & 0 deletions studio/src/components/oidc/oidc-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { useFeature } from '@/hooks/use-feature';
import { cn } from '@/lib/utils';
import { Badge } from '@/components/ui/badge';
import Link from 'next/link';
import { calURL, docsBaseURL } from '@/lib/constants';
import { Button } from '@/components/ui/button';
import { CLI } from '@/components/ui/cli';
import { ConnectOIDCProviderDialog } from './connect-oidc-provider-dialog';
import { DisconnectOIDCProviderDialog } from './disconnect-oidc-provider-dialog';
import { UpdateMappersDialog } from './update-mappers-dialog';
import { OIDCInfoDialog } from './oidc-info-dialog';
import { GetOIDCProviderResponse } from '@wundergraph/cosmo-connect/dist/platform/v1/platform_pb';
import { useState } from 'react';

export interface OIDCCardProps {
className?: string;
providerData?: GetOIDCProviderResponse;
refetchOIDCProvider(): Promise<unknown>;
}

export function OIDCCard({ className, providerData, refetchOIDCProvider }: OIDCCardProps) {
const oidc = useFeature('oidc');
const [wasProviderJustConnected, setWasProviderJustConnected] = useState(false);
const [forceOpenMappersDialog, setForceOpenMappersDialog] = useState(false);

return (
<Card>
<CardHeader className={cn(className)}>
<div className="space-y-1.5">
<CardTitle className="flex items-center gap-x-2">
<span>Connect OIDC provider</span>
<Badge variant="outline">Enterprise feature</Badge>
</CardTitle>
<CardDescription>
Connecting an OIDC provider allows users to automatically log in and be a part of this organization.{' '}
<Link href={docsBaseURL + '/studio/sso'} className="text-sm text-primary" target="_blank" rel="noreferrer">
Learn more
</Link>
</CardDescription>
</div>
{!oidc ? (
Comment thread
wilsonrivera marked this conversation as resolved.
Outdated
<Button className="md:ml-auto" type="submit" variant="default" asChild>
<Link href={calURL} target="_blank" rel="noreferrer">
Contact us
</Link>
</Button>
) : (
<div className="ml-auto flex gap-x-3">
<OIDCInfoDialog
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This gets hard to read, as I open the individual component files . I assume the same solution was used previously, and you're just extracting the former huge component to more smaller components (which is good 👍).
The way I read it is “I have a card component but wait, four dialogs are rendered” 🤔 I assume that only single dialog out of these four can be rendered in single time?

If so, could we model it so it's explicit? Because I see we pass open prop in some cases, but in other we let the dialog component itself decide whether it is opened or not, e.g. with complicated conditions such as isProviderConnected && isAdmin && open.

I think it would be much easier to reason about if we handled the open state in this component. We could model four states (info-dialog-opened, update-mappers-dialog-opened, connect-oidc-dialog-opened, disconnect-oidc-dialog-opened) which would reflect states in which dialog should get opened. This can be solved by using useReducer.

I see two further parts for refactoring:

  1. Passing callback to dialog components which dispatches an event and stores the opened state
  2. Move logic from dialogs up the tree into this component (possibly wrapping it in hooks) or moving the logic into high order component above. I'd still use the reducer for clarity.

The rendering logic in this component would then have a switch statement and render just one dialog at a time instead of passing open prop. It'd make it obvious what needs to happen in order to see a dialog. I know it's a big ask and requires more refactoring, but I think it'd be worth it for maintainability.

open={wasProviderJustConnected}
providerData={providerData}
onClose={() => {
setWasProviderJustConnected(false);
setForceOpenMappersDialog(true);
}}
/>

<UpdateMappersDialog
forceOpen={forceOpenMappersDialog}
isProviderConnected={!!providerData?.name}
currentMappers={providerData?.mappers ?? []}
refetch={refetchOIDCProvider}
onClose={() => setForceOpenMappersDialog(false)}
/>

<ConnectOIDCProviderDialog
isProviderConnected={!!providerData?.name}
refetch={refetchOIDCProvider}
onProviderConnected={() => setWasProviderJustConnected(true)}
/>
<DisconnectOIDCProviderDialog isProviderConnected={!!providerData?.name} refetch={refetchOIDCProvider} />
</div>
)}
</CardHeader>
{providerData?.name && (
<CardContent className="flex flex-col gap-y-3">
<div className="flex flex-col gap-y-2">
<span className="px-1">OIDC provider</span>
<CLI command={`https://${providerData.endpoint}`} />
</div>
<div className="flex flex-col gap-y-2">
<span className="px-1">Sign in redirect URL</span>
<CLI command={providerData?.signInRedirectURL || ''} />
</div>
<div className="flex flex-col gap-y-2">
<span className="px-1">Sign out redirect URL</span>
<CLI command={providerData?.signOutRedirectURL || ''} />
</div>
<div className="flex flex-col gap-y-2">
<span className="px-1">Login URL</span>
<CLI command={providerData?.loginURL || ''} />
</div>
</CardContent>
)}
</Card>
);
}
Loading
Loading