-
Notifications
You must be signed in to change notification settings - Fork 235
feat: improve OIDC connection behavior #2789
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 2 commits
0bad43c
02100d0
844accb
6a8076d
53ed845
26a6dd1
755e900
da3bd94
f833838
59cea5c
916fe2c
325c228
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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 ? ( | ||
|
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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 👍). If so, could we model it so it's explicit? Because I see we pass I think it would be much easier to reason about if we handled the open state in this component. We could model four states ( I see two further parts for refactoring:
The rendering logic in this component would then have a |
||
| 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> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trailing space in error message.
There's a trailing space at the end of the error details string.
Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents