22
33import { zodResolver } from "@hookform/resolvers/zod" ;
44import { Plus , Trash2 } from "lucide-react" ;
5- import { useState } from "react" ;
5+ import { useEffect , useState } from "react" ;
66import type { FieldArrayPath } from "react-hook-form" ;
7- import { useFieldArray , useForm } from "react-hook-form" ;
7+ import { useFieldArray , useForm , useWatch } from "react-hook-form" ;
88import { toast } from "sonner" ;
99import { z } from "zod" ;
1010import { Button } from "@/components/ui/button" ;
@@ -28,6 +28,7 @@ import {
2828} from "@/components/ui/form" ;
2929import { Input } from "@/components/ui/input" ;
3030import { api } from "@/utils/api" ;
31+ import { useUrl } from "@/utils/hooks/use-url" ;
3132
3233const DEFAULT_SCOPES = [ "openid" , "email" , "profile" ] ;
3334
@@ -58,6 +59,7 @@ const oidcProviderSchema = z.object({
5859type OidcProviderForm = z . infer < typeof oidcProviderSchema > ;
5960
6061interface RegisterOidcDialogProps {
62+ providerId ?: string ;
6163 children : React . ReactNode ;
6264}
6365
@@ -70,16 +72,86 @@ const formDefaultValues = {
7072 scopes : [ ...DEFAULT_SCOPES ] ,
7173} ;
7274
73- export function RegisterOidcDialog ( { children } : RegisterOidcDialogProps ) {
75+ function parseOidcConfig ( oidcConfig : string | null ) : {
76+ clientId ?: string ;
77+ clientSecret ?: string ;
78+ scopes ?: string [ ] ;
79+ } | null {
80+ if ( ! oidcConfig ) return null ;
81+ try {
82+ const parsed = JSON . parse ( oidcConfig ) as {
83+ clientId ?: string ;
84+ clientSecret ?: string ;
85+ scopes ?: string [ ] ;
86+ } ;
87+ return {
88+ clientId : parsed . clientId ,
89+ clientSecret : parsed . clientSecret ,
90+ scopes : Array . isArray ( parsed . scopes ) ? parsed . scopes : undefined ,
91+ } ;
92+ } catch {
93+ return null ;
94+ }
95+ }
96+
97+ export function RegisterOidcDialog ( {
98+ providerId,
99+ children,
100+ } : RegisterOidcDialogProps ) {
74101 const utils = api . useUtils ( ) ;
75102 const [ open , setOpen ] = useState ( false ) ;
76- const { mutateAsync, isLoading } = api . sso . register . useMutation ( ) ;
103+
104+ const { data } = api . sso . one . useQuery (
105+ { providerId : providerId ?? "" } ,
106+ { enabled : ! ! providerId && open } ,
107+ ) ;
108+ const registerMutation = api . sso . register . useMutation ( ) ;
109+ const updateMutation = api . sso . update . useMutation ( ) ;
110+
111+ const isEdit = ! ! providerId ;
112+ const mutateAsync = isEdit
113+ ? updateMutation . mutateAsync
114+ : registerMutation . mutateAsync ;
115+ const isLoading = isEdit
116+ ? updateMutation . isLoading
117+ : registerMutation . isLoading ;
77118
78119 const form = useForm < OidcProviderForm > ( {
79120 resolver : zodResolver ( oidcProviderSchema ) ,
80121 defaultValues : formDefaultValues ,
81122 } ) ;
82123
124+ const watchedProviderId = useWatch ( {
125+ control : form . control ,
126+ name : "providerId" ,
127+ defaultValue : "" ,
128+ } ) ;
129+
130+ const baseURL = useUrl ( ) ;
131+
132+ useEffect ( ( ) => {
133+ if ( ! data || ! open ) return ;
134+ const domains = data . domain
135+ ? data . domain
136+ . split ( "," )
137+ . map ( ( d ) => d . trim ( ) )
138+ . filter ( Boolean )
139+ : [ "" ] ;
140+ if ( domains . length === 0 ) domains . push ( "" ) ;
141+ const oidc = parseOidcConfig ( data . oidcConfig ) ;
142+ form . reset ( {
143+ providerId : data . providerId ,
144+ issuer : data . issuer ,
145+ domains,
146+ clientId : oidc ?. clientId ?? "" ,
147+ clientSecret : oidc ?. clientSecret ?? "" ,
148+ scopes :
149+ oidc ?. scopes && oidc . scopes . length > 0
150+ ? oidc . scopes
151+ : [ ...DEFAULT_SCOPES ] ,
152+ } ) ;
153+ } , [ data , open , form ] ) ;
154+
83155 const { fields, append, remove } = useFieldArray ( {
84156 control : form . control ,
85157 name : "domains" as FieldArrayPath < OidcProviderForm > ,
@@ -130,7 +202,11 @@ export function RegisterOidcDialog({ children }: RegisterOidcDialogProps) {
130202 } ,
131203 } ) ;
132204
133- toast . success ( "OIDC provider registered successfully" ) ;
205+ toast . success (
206+ isEdit
207+ ? "OIDC provider updated successfully"
208+ : "OIDC provider registered successfully" ,
209+ ) ;
134210 form . reset ( formDefaultValues ) ;
135211 setOpen ( false ) ;
136212 await utils . sso . listProviders . invalidate ( ) ;
@@ -146,11 +222,13 @@ export function RegisterOidcDialog({ children }: RegisterOidcDialogProps) {
146222 < DialogTrigger asChild > { children } </ DialogTrigger >
147223 < DialogContent className = "sm:max-w-[500px]" >
148224 < DialogHeader >
149- < DialogTitle > Register OIDC provider</ DialogTitle >
225+ < DialogTitle >
226+ { isEdit ? "Update OIDC provider" : "Register OIDC provider" }
227+ </ DialogTitle >
150228 < DialogDescription >
151- Add any OIDC-compliant identity provider (e.g. Okta, Azure AD,
152- Google Workspace, Auth0, Keycloak). Discovery will fill endpoints
153- from the issuer URL when possible.
229+ { isEdit
230+ ? "Change issuer, domains, client settings or scopes. Provider ID cannot be changed."
231+ : "Add any OIDC-compliant identity provider (e.g. Okta, Azure AD, Google Workspace, Auth0, Keycloak). Discovery will fill endpoints from the issuer URL when possible." }
154232 </ DialogDescription >
155233 </ DialogHeader >
156234 < Form { ...form } >
@@ -162,11 +240,28 @@ export function RegisterOidcDialog({ children }: RegisterOidcDialogProps) {
162240 < FormItem >
163241 < FormLabel > Provider ID</ FormLabel >
164242 < FormControl >
165- < Input placeholder = "e.g. okta or my-idp" { ...field } />
243+ < Input
244+ placeholder = "e.g. okta or my-idp"
245+ { ...field }
246+ readOnly = { isEdit }
247+ className = { isEdit ? "bg-muted" : undefined }
248+ />
166249 </ FormControl >
167250 < FormDescription >
168251 Unique identifier; used in callback URL path.
252+ { isEdit && " Cannot be changed when editing." }
169253 </ FormDescription >
254+ { baseURL && (
255+ < div className = "rounded-md bg-muted px-3 py-2 text-xs" >
256+ < p className = "font-medium text-muted-foreground" >
257+ Callback URL (configure in your IdP)
258+ </ p >
259+ < p className = "mt-0.5 break-all font-mono" >
260+ { baseURL } /api/auth/sso/callback/
261+ { watchedProviderId ?. trim ( ) || "..." }
262+ </ p >
263+ </ div >
264+ ) }
170265 < FormMessage />
171266 </ FormItem >
172267 ) }
@@ -341,7 +436,7 @@ export function RegisterOidcDialog({ children }: RegisterOidcDialogProps) {
341436 Cancel
342437 </ Button >
343438 < Button type = "submit" isLoading = { isLoading } >
344- Register provider
439+ { isEdit ? "Update provider" : " Register provider" }
345440 </ Button >
346441 </ DialogFooter >
347442 </ form >
0 commit comments