11"use client" ;
22
3- import { Eye , Loader2 , LogIn , Trash2 } from "lucide-react" ;
3+ import {
4+ Eye ,
5+ Loader2 ,
6+ LogIn ,
7+ Pencil ,
8+ Plus ,
9+ Shield ,
10+ Trash2 ,
11+ } from "lucide-react" ;
412import { useEffect , useState } from "react" ;
513import { toast } from "sonner" ;
614import { DialogAction } from "@/components/shared/dialog-action" ;
@@ -21,6 +29,7 @@ import {
2129 DialogHeader ,
2230 DialogTitle ,
2331} from "@/components/ui/dialog" ;
32+ import { Input } from "@/components/ui/input" ;
2433import { api } from "@/utils/api" ;
2534import { RegisterOidcDialog } from "./register-oidc-dialog" ;
2635import { RegisterSamlDialog } from "./register-saml-dialog" ;
@@ -68,6 +77,10 @@ export const SSOSettings = () => {
6877 const [ detailsProvider , setDetailsProvider ] =
6978 useState < ProviderForDetails | null > ( null ) ;
7079 const [ baseURL , setBaseURL ] = useState ( "" ) ;
80+ const [ manageOriginsOpen , setManageOriginsOpen ] = useState ( false ) ;
81+ const [ editingOrigin , setEditingOrigin ] = useState < string | null > ( null ) ;
82+ const [ editingValue , setEditingValue ] = useState ( "" ) ;
83+ const [ newOriginInput , setNewOriginInput ] = useState ( "" ) ;
7184
7285 useEffect ( ( ) => {
7386 if ( typeof window !== "undefined" ) {
@@ -76,20 +89,101 @@ export const SSOSettings = () => {
7689 } , [ ] ) ;
7790
7891 const { data : providers , isLoading } = api . sso . listProviders . useQuery ( ) ;
92+ const { data : userData } = api . user . get . useQuery ( undefined , {
93+ enabled : manageOriginsOpen ,
94+ } ) ;
7995 const { mutateAsync : deleteProvider , isLoading : isDeleting } =
8096 api . sso . deleteProvider . useMutation ( ) ;
97+ const { mutateAsync : addTrustedOrigin , isLoading : isAddingOrigin } =
98+ api . sso . addTrustedOrigin . useMutation ( ) ;
99+ const { mutateAsync : removeTrustedOrigin , isLoading : isRemovingOrigin } =
100+ api . sso . removeTrustedOrigin . useMutation ( ) ;
101+ const { mutateAsync : updateTrustedOrigin , isLoading : isUpdatingOrigin } =
102+ api . sso . updateTrustedOrigin . useMutation ( ) ;
103+
104+ const trustedOrigins = userData ?. user ?. trustedOrigins ?? [ ] ;
105+
106+ const handleAddOrigin = async ( ) => {
107+ const value = newOriginInput . trim ( ) ;
108+ if ( ! value ) return ;
109+ try {
110+ await addTrustedOrigin ( { origin : value } ) ;
111+ toast . success ( "Trusted origin added" ) ;
112+ setNewOriginInput ( "" ) ;
113+ await utils . user . get . invalidate ( ) ;
114+ } catch ( err ) {
115+ toast . error (
116+ err instanceof Error ? err . message : "Failed to add trusted origin" ,
117+ ) ;
118+ }
119+ } ;
120+
121+ const handleRemoveOrigin = async ( origin : string ) => {
122+ try {
123+ await removeTrustedOrigin ( { origin } ) ;
124+ toast . success ( "Trusted origin removed" ) ;
125+ if ( editingOrigin === origin ) setEditingOrigin ( null ) ;
126+ await utils . user . get . invalidate ( ) ;
127+ } catch ( err ) {
128+ toast . error (
129+ err instanceof Error ? err . message : "Failed to remove trusted origin" ,
130+ ) ;
131+ }
132+ } ;
133+
134+ const handleStartEdit = ( origin : string ) => {
135+ setEditingOrigin ( origin ) ;
136+ setEditingValue ( origin ) ;
137+ } ;
138+
139+ const handleSaveEdit = async ( ) => {
140+ if ( editingOrigin == null || ! editingValue . trim ( ) ) {
141+ setEditingOrigin ( null ) ;
142+ return ;
143+ }
144+ try {
145+ await updateTrustedOrigin ( {
146+ oldOrigin : editingOrigin ,
147+ newOrigin : editingValue . trim ( ) ,
148+ } ) ;
149+ toast . success ( "Trusted origin updated" ) ;
150+ setEditingOrigin ( null ) ;
151+ setEditingValue ( "" ) ;
152+ await utils . user . get . invalidate ( ) ;
153+ } catch ( err ) {
154+ toast . error (
155+ err instanceof Error ? err . message : "Failed to update trusted origin" ,
156+ ) ;
157+ }
158+ } ;
159+
160+ const handleCancelEdit = ( ) => {
161+ setEditingOrigin ( null ) ;
162+ setEditingValue ( "" ) ;
163+ } ;
81164
82165 return (
83166 < div className = "flex flex-col gap-4 rounded-lg border p-4" >
84- < div className = "flex flex-col gap-2" >
85- < div className = "flex items-center gap-2" >
86- < LogIn className = "size-6 text-muted-foreground" />
87- < CardTitle className = "text-xl" > Single Sign-On (SSO)</ CardTitle >
167+ < div className = "flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between" >
168+ < div className = "flex flex-col gap-2" >
169+ < div className = "flex items-center gap-2" >
170+ < LogIn className = "size-6 text-muted-foreground" />
171+ < CardTitle className = "text-xl" > Single Sign-On (SSO)</ CardTitle >
172+ </ div >
173+ < CardDescription >
174+ Configure OIDC or SAML identity providers for enterprise sign-in.
175+ Users can sign in with their organization's IdP.
176+ </ CardDescription >
88177 </ div >
89- < CardDescription >
90- Configure OIDC or SAML identity providers for enterprise sign-in.
91- Users can sign in with their organization's IdP.
92- </ CardDescription >
178+ < Button
179+ variant = "outline"
180+ size = "sm"
181+ onClick = { ( ) => setManageOriginsOpen ( true ) }
182+ className = "shrink-0"
183+ >
184+ < Shield className = "mr-2 size-4" />
185+ Manage origins
186+ </ Button >
93187 </ div >
94188
95189 { isLoading ? (
@@ -366,6 +460,128 @@ export const SSOSettings = () => {
366460 ) }
367461 </ DialogContent >
368462 </ Dialog >
463+
464+ < Dialog open = { manageOriginsOpen } onOpenChange = { setManageOriginsOpen } >
465+ < DialogContent className = "sm:max-w-[480px]" >
466+ < DialogHeader >
467+ < DialogTitle className = "flex items-center gap-2" >
468+ < Shield className = "size-5" />
469+ Trusted origins
470+ </ DialogTitle >
471+ < DialogDescription >
472+ Manage allowed origins for SSO callbacks. Add, edit, or remove
473+ origins for your account.
474+ </ DialogDescription >
475+ </ DialogHeader >
476+ < div className = "space-y-4 py-2" >
477+ < div className = "space-y-2" >
478+ < span className = "text-sm font-medium" > Current origins</ span >
479+ { trustedOrigins . length === 0 ? (
480+ < p className = "rounded-md border border-dashed bg-muted/30 px-3 py-4 text-center text-sm text-muted-foreground" >
481+ No trusted origins yet. Add one below.
482+ </ p >
483+ ) : (
484+ < ul className = "flex flex-col gap-2" >
485+ { trustedOrigins . map ( ( origin ) => (
486+ < li
487+ key = { origin }
488+ className = "flex items-center gap-2 rounded-md border bg-muted/30 px-3 py-2"
489+ >
490+ { editingOrigin === origin ? (
491+ < >
492+ < Input
493+ value = { editingValue }
494+ onChange = { ( e ) => setEditingValue ( e . target . value ) }
495+ placeholder = "https://..."
496+ className = "flex-1 font-mono text-sm"
497+ autoFocus
498+ />
499+ < Button
500+ size = "sm"
501+ onClick = { handleSaveEdit }
502+ disabled = { ! editingValue . trim ( ) || isUpdatingOrigin }
503+ >
504+ Save
505+ </ Button >
506+ < Button
507+ size = "sm"
508+ variant = "ghost"
509+ onClick = { handleCancelEdit }
510+ >
511+ Cancel
512+ </ Button >
513+ </ >
514+ ) : (
515+ < >
516+ < span className = "flex-1 break-all font-mono text-sm" >
517+ { origin }
518+ </ span >
519+ < Button
520+ variant = "ghost"
521+ size = "icon"
522+ className = "size-8 shrink-0"
523+ onClick = { ( ) => handleStartEdit ( origin ) }
524+ >
525+ < Pencil className = "size-3.5" />
526+ </ Button >
527+ < DialogAction
528+ title = "Remove trusted origin"
529+ description = { `Remove "${ origin } " from trusted origins?` }
530+ type = "destructive"
531+ onClick = { async ( ) => handleRemoveOrigin ( origin ) }
532+ >
533+ < Button
534+ variant = "ghost"
535+ size = "icon"
536+ className = "size-8 shrink-0 text-destructive hover:text-destructive"
537+ disabled = { isRemovingOrigin }
538+ >
539+ < Trash2 className = "size-3.5" />
540+ </ Button >
541+ </ DialogAction >
542+ </ >
543+ ) }
544+ </ li >
545+ ) ) }
546+ </ ul >
547+ ) }
548+ </ div >
549+ < div className = "space-y-2" >
550+ < span className = "text-sm font-medium" > Add trusted origin</ span >
551+ < div className = "flex gap-2" >
552+ < Input
553+ value = { newOriginInput }
554+ onChange = { ( e ) => setNewOriginInput ( e . target . value ) }
555+ placeholder = "https://example.com"
556+ className = "font-mono text-sm"
557+ onKeyDown = { ( e ) => {
558+ if ( e . key === "Enter" ) {
559+ e . preventDefault ( ) ;
560+ void handleAddOrigin ( ) ;
561+ }
562+ } }
563+ />
564+ < Button
565+ size = "sm"
566+ onClick = { handleAddOrigin }
567+ disabled = { ! newOriginInput . trim ( ) || isAddingOrigin }
568+ >
569+ < Plus className = "mr-1 size-4" />
570+ Add
571+ </ Button >
572+ </ div >
573+ </ div >
574+ </ div >
575+ < DialogFooter >
576+ < Button
577+ variant = "outline"
578+ onClick = { ( ) => setManageOriginsOpen ( false ) }
579+ >
580+ Close
581+ </ Button >
582+ </ DialogFooter >
583+ </ DialogContent >
584+ </ Dialog >
369585 </ div >
370586 ) ;
371587} ;
0 commit comments