@@ -34,14 +34,63 @@ import {
3434} from "@/components/ui/select" ;
3535import { api } from "@/utils/api" ;
3636
37- const addInvitation = z . object ( {
38- email : z
39- . string ( )
40- . min ( 1 , "Email is required" )
41- . email ( { message : "Invalid email" } ) ,
42- role : z . string ( ) . min ( 1 , "Role is required" ) ,
43- notificationId : z . string ( ) . optional ( ) ,
44- } ) ;
37+ const addInvitation = z
38+ . object ( {
39+ mode : z . enum ( [ "invitation" , "credentials" ] ) ,
40+ email : z
41+ . string ( )
42+ . min ( 1 , "Email is required" )
43+ . email ( { message : "Invalid email" } ) ,
44+ role : z . string ( ) . min ( 1 , "Role is required" ) ,
45+ notificationId : z . string ( ) . optional ( ) ,
46+ password : z . string ( ) . optional ( ) ,
47+ confirmPassword : z . string ( ) . optional ( ) ,
48+ } )
49+ . superRefine ( ( value , ctx ) => {
50+ if ( value . mode !== "credentials" ) {
51+ return ;
52+ }
53+
54+ if ( ! value . password ) {
55+ ctx . addIssue ( {
56+ code : z . ZodIssueCode . custom ,
57+ message : "Password is required" ,
58+ path : [ "password" ] ,
59+ } ) ;
60+ } else if ( value . password . length < 8 ) {
61+ ctx . addIssue ( {
62+ code : z . ZodIssueCode . custom ,
63+ message : "Password must be at least 8 characters" ,
64+ path : [ "password" ] ,
65+ } ) ;
66+ }
67+
68+ if ( ! value . confirmPassword ) {
69+ ctx . addIssue ( {
70+ code : z . ZodIssueCode . custom ,
71+ message : "Confirm password is required" ,
72+ path : [ "confirmPassword" ] ,
73+ } ) ;
74+ } else if ( value . confirmPassword . length < 8 ) {
75+ ctx . addIssue ( {
76+ code : z . ZodIssueCode . custom ,
77+ message : "Password must be at least 8 characters" ,
78+ path : [ "confirmPassword" ] ,
79+ } ) ;
80+ }
81+
82+ if (
83+ value . password &&
84+ value . confirmPassword &&
85+ value . password !== value . confirmPassword
86+ ) {
87+ ctx . addIssue ( {
88+ code : z . ZodIssueCode . custom ,
89+ message : "Passwords do not match" ,
90+ path : [ "confirmPassword" ] ,
91+ } ) ;
92+ }
93+ } ) ;
4594
4695type AddInvitation = z . infer < typeof addInvitation > ;
4796
@@ -54,50 +103,83 @@ export const AddInvitation = () => {
54103 const { mutateAsync : inviteMember , isPending : isInviting } =
55104 api . organization . inviteMember . useMutation ( ) ;
56105 const { mutateAsync : sendInvitation } = api . user . sendInvitation . useMutation ( ) ;
106+ const { mutateAsync : createUserWithCredentials , isPending : isCreating } =
107+ api . user . createUserWithCredentials . useMutation ( ) ;
57108 const { data : customRoles } = api . customRole . all . useQuery ( ) ;
58109 const [ error , setError ] = useState < string | null > ( null ) ;
59110
60111 const form = useForm < AddInvitation > ( {
61112 defaultValues : {
113+ mode : "invitation" ,
62114 email : "" ,
63115 role : "member" ,
64116 notificationId : "" ,
117+ password : "" ,
118+ confirmPassword : "" ,
65119 } ,
66120 resolver : zodResolver ( addInvitation ) ,
67121 } ) ;
122+
123+ const mode = form . watch ( "mode" ) ;
124+
68125 useEffect ( ( ) => {
69126 form . reset ( ) ;
70127 } , [ form , form . formState . isSubmitSuccessful , form . reset ] ) ;
71128
129+ useEffect ( ( ) => {
130+ if ( isCloud && form . getValues ( "mode" ) === "credentials" ) {
131+ form . setValue ( "mode" , "invitation" ) ;
132+ }
133+ } , [ form , isCloud ] ) ;
134+
72135 const onSubmit = async ( data : AddInvitation ) => {
136+ setError ( null ) ;
137+
73138 try {
74- const result = await inviteMember ( {
75- email : data . email . toLowerCase ( ) ,
76- role : data . role ,
77- } ) ;
139+ if ( data . mode === "credentials" ) {
140+ await createUserWithCredentials ( {
141+ email : data . email . toLowerCase ( ) ,
142+ password : data . password ! ,
143+ role : data . role ,
144+ } ) ;
145+ toast . success ( "User created with initial credentials" ) ;
146+ setOpen ( false ) ;
147+ } else {
148+ const result = await inviteMember ( {
149+ email : data . email . toLowerCase ( ) ,
150+ role : data . role ,
151+ } ) ;
78152
79- if ( ! isCloud && data . notificationId ) {
80- await sendInvitation ( {
81- invitationId : result ! . id ,
82- notificationId : data . notificationId || "" ,
83- } )
84- . then ( ( ) => {
85- toast . success ( "Invitation created and email sent" ) ;
153+ if ( ! isCloud && data . notificationId ) {
154+ await sendInvitation ( {
155+ invitationId : result ! . id ,
156+ notificationId : data . notificationId || "" ,
86157 } )
87- . catch ( ( error : any ) => {
88- toast . error ( error . message ) ;
89- } ) ;
90- } else {
91- toast . success ( "Invitation created" ) ;
158+ . then ( ( ) => {
159+ toast . success ( "Invitation created and email sent" ) ;
160+ } )
161+ . catch ( ( error : any ) => {
162+ toast . error ( error . message ) ;
163+ } ) ;
164+ } else {
165+ toast . success ( "Invitation created" ) ;
166+ }
167+
168+ setOpen ( false ) ;
92169 }
93- setError ( null ) ;
94- setOpen ( false ) ;
95- } catch ( error : any ) {
96- setError ( error . message || "Failed to create invitation" ) ;
170+ } catch ( error ) {
171+ const message =
172+ error instanceof Error ? error . message : "Failed to create user" ;
173+ setError ( message ) ;
174+ toast . error ( message ) ;
175+ } finally {
176+ await Promise . all ( [
177+ utils . organization . allInvitations . invalidate ( ) ,
178+ utils . user . all . invalidate ( ) ,
179+ ] ) ;
97180 }
98-
99- utils . organization . allInvitations . invalidate ( ) ;
100181 } ;
182+
101183 return (
102184 < Dialog open = { open } onOpenChange = { setOpen } >
103185 < DialogTrigger className = "" asChild >
@@ -108,7 +190,11 @@ export const AddInvitation = () => {
108190 < DialogContent className = "sm:max-w-2xl" >
109191 < DialogHeader >
110192 < DialogTitle > Add Invitation</ DialogTitle >
111- < DialogDescription > Invite a new user</ DialogDescription >
193+ < DialogDescription >
194+ { mode === "credentials"
195+ ? "Create a user with initial credentials"
196+ : "Invite a new user" }
197+ </ DialogDescription >
112198 </ DialogHeader >
113199 { error && < AlertBlock type = "error" > { error } </ AlertBlock > }
114200
@@ -118,6 +204,43 @@ export const AddInvitation = () => {
118204 onSubmit = { form . handleSubmit ( onSubmit ) }
119205 className = "grid w-full gap-4 "
120206 >
207+ { ! isCloud && (
208+ < FormField
209+ control = { form . control }
210+ name = "mode"
211+ render = { ( { field } ) => {
212+ return (
213+ < FormItem >
214+ < FormLabel > Invite Method</ FormLabel >
215+ < Select
216+ onValueChange = { field . onChange }
217+ defaultValue = { field . value }
218+ >
219+ < FormControl >
220+ < SelectTrigger >
221+ < SelectValue placeholder = "Select invite method" />
222+ </ SelectTrigger >
223+ </ FormControl >
224+ < SelectContent >
225+ < SelectItem value = "invitation" >
226+ Invitation Link
227+ </ SelectItem >
228+ < SelectItem value = "credentials" >
229+ Initial Credentials
230+ </ SelectItem >
231+ </ SelectContent >
232+ </ Select >
233+ < FormDescription >
234+ Choose between invitation link flow or direct
235+ credentials provisioning
236+ </ FormDescription >
237+ < FormMessage />
238+ </ FormItem >
239+ ) ;
240+ } }
241+ />
242+ ) }
243+
121244 < FormField
122245 control = { form . control }
123246 name = "email"
@@ -172,7 +295,7 @@ export const AddInvitation = () => {
172295 } }
173296 />
174297
175- { ! isCloud && (
298+ { ! isCloud && mode === "invitation" && (
176299 < FormField
177300 control = { form . control }
178301 name = "notificationId"
@@ -212,9 +335,57 @@ export const AddInvitation = () => {
212335 } }
213336 />
214337 ) }
338+
339+ { ! isCloud && mode === "credentials" && (
340+ < >
341+ < FormField
342+ control = { form . control }
343+ name = "password"
344+ render = { ( { field } ) => {
345+ return (
346+ < FormItem >
347+ < FormLabel > Password</ FormLabel >
348+ < FormControl >
349+ < Input
350+ type = "password"
351+ placeholder = "Enter initial password"
352+ { ...field }
353+ />
354+ </ FormControl >
355+ < FormDescription >
356+ The user can sign in with this password immediately
357+ </ FormDescription >
358+ < FormMessage />
359+ </ FormItem >
360+ ) ;
361+ } }
362+ />
363+
364+ < FormField
365+ control = { form . control }
366+ name = "confirmPassword"
367+ render = { ( { field } ) => {
368+ return (
369+ < FormItem >
370+ < FormLabel > Confirm Password</ FormLabel >
371+ < FormControl >
372+ < Input
373+ type = "password"
374+ placeholder = "Confirm initial password"
375+ { ...field }
376+ />
377+ </ FormControl >
378+ < FormMessage />
379+ </ FormItem >
380+ ) ;
381+ } }
382+ />
383+ </ >
384+ ) }
385+
215386 < DialogFooter className = "flex w-full flex-row" >
216387 < Button
217- isLoading = { isInviting }
388+ isLoading = { isInviting || isCreating }
218389 form = "hook-form-add-invitation"
219390 type = "submit"
220391 >
0 commit comments