Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 ? (
<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
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