backend/src/handler/user/create_user.rs
loading...
1200 "git2", 1201 "hex", 1202 "nutype", 1203 "rand 0.9.2", 1204 "serde", .. 1205 "sha2", 1206 "sqlx", 1207 "supabase-lib-rs", 1208 "thiserror 2.0.17", 1200 "git2", 1201 "hex", 1202 "nutype", 1203 "rand 0.9.2", 1204 "serde", 1205 " serde_json " , 1206 "sha2", 1207 "sqlx", 1208 "supabase-lib-rs", 1209 "thiserror 2.0.17", 65 AppError :: User ( e ) => { 66 let status_code = match e { 67 UserError :: NotFound ( _ ) => StatusCode :: NOT_FOUND , 68 UserError :: InvalidUserName ( _ ) => StatusCode :: BAD_REQUEST , 69 UserError :: NameTaken ( _ ) => StatusCode :: CONFLICT , 70 UserError :: EmailTaken ( _ ) => StatusCode :: CONFLICT , 71 UserError :: ReservedName ( _ ) => StatusCode :: CONFLICT , 72 UserError :: SupabaseError ( _ ) => StatusCode :: INTERNAL_SERVER_ERROR , 73 UserError :: DatabaseError ( _ ) => StatusCode :: INTERNAL_SERVER_ERROR , 74 }; 65 AppError :: User ( e ) => { 66 let status_code = match e { 67 UserError :: NotFound ( _ ) => StatusCode :: NOT_FOUND , 68 UserError :: InvalidUserName ( _ ) => StatusCode :: BAD_REQUEST , 69 UserError :: NameTaken ( _ ) => StatusCode :: CONFLICT , .. 70 UserError :: ReservedName 2 3 use gitdot_core :: dto :: CreateUserRequest ; 4 5 use crate :: app ::{ AppError , AppResponse , AppState }; 6 use crate :: dto ::{ CreateUserServerRequest , UserServerResponse } ;7 8 pub async fn create_user ( 9 State ( state ): State < AppState >, 10 Json ( request ): Json < CreateUserServerRequest >, 11 ) -> Result < AppResponse < UserServerResponse >, AppError > { 12 let create_request = CreateUserRequest :: new (& request .name, & request .email, & request .password)?; 13 state 14 .user_service 15 . create_user ( create_request ) 16 . await 17 . map_err ( AppError :: from ) 18 . map ( | user | AppResponse :: new ( StatusCode :: CREATED , user . into ())) 19 } 20 2 3 use gitdot_core :: dto :: CreateUserRequest ; 4 5 use crate :: app ::{ AppError , AppResponse , AppState }; 6 use crate :: dto :: CreateUserServerRequest ; 7 8 pub async fn create_user ( 9 State ( state ): State < AppState >, 10 Json ( request 10 hex = { workspace = true } 11 nutype = { workspace = true } 12 git2 = { workspace = true } 13 rand = { workspace = true } 14 serde = { workspace = true } .. 15 sha2 = { workspace = true } 16 sqlx = { workspace = true } 17 supabase-lib-rs = { workspace = true } 18 thiserror = { workspace = true } 10 hex = { workspace = true } 11 nutype = { workspace = true } 12 git2 = { workspace = true } 13 rand = { workspace = true } 14 serde = { workspace = true } 15 serde_json = { workspace = true } 16 1 use async_trait ::async_trait; 2 3 use supabase ::{ Client , Error , auth :: User }; 4 5 #[async_trait] 6 pub trait SupabaseClient : Send + Sync + Clone + ' static { 7 async fn create_user (& self , email : & str , password : & str ) -> Result < User , Error >; 8 } 9 10 #[derive( Debug , Clone )] 11 pub struct SupabaseClientImpl { 1 use async_trait ::async_trait; 2 3 use supabase ::{ Client , Error }; 4 5 #[async_trait] 6 pub trait SupabaseClient : Send + Sync + Clone + ' static { 7 async fn create_user (& self , name : & str , email : & str , password : & str ) -> Result
22 } 23 24 #[async_trait] 25 impl SupabaseClient for SupabaseClientImpl { 26 async fn create_user (& self , email : & str , password : & str ) -> Result < User , Error > { 27 let response = self 28 .client 29 9 NotFound ( String ), 10 11 #[error( " Name already taken: { 0 }" )] 12 NameTaken ( String ), 13 14 # [ error ( " Email already taken: { 0 }" ) ] 15 EmailTaken ( String ) , 16 17 #[error( " Reserved name: { 0 }" )] 18 ReservedName ( String ), 19 9 NotFound ( String ), 10 11 #[error( " Name already taken: { 0 }" )] 12 NameTaken ( String ), 13 .. .. 14 #[error( " Reserved name: { 0 }" )] 15 ReservedName ( String ), 16 17 #[error( " Database error: { 0 }" )] 4 5 use crate :: model :: User ; 6 7 #[async_trait] 8 pub trait UserRepository : Send + Sync + Clone + ' static { 9 async fn create ( & self , id : Uuid , name : & str , email : & str ) -> Result < User , Error > ; 10 11 async fn get (& self , user_name : & str ) -> Result < Option < User >, Error >; 12 13 async fn get_by_id (& self , id : Uuid ) -> Result < Option < User >, Error >; 4 5 use crate :: model :: User ; 6 7 #[async_trait] 8 pub trait UserRepository : Send + Sync + Clone + ' static { .. 9 async fn get (& self , user_name : & str ) -> Result < Option < User >, Error >; 10 11 async fn get_by_id
30 } 31 } 32 33 #[async_trait] 34 impl UserRepository for UserRepositoryImpl { 35 async fn create ( & self , id : Uuid , name : & str , email : & str 12 use crate :: util :: auth ::is_reserved_name; 13 14 #[async_trait] 15 pub trait UserService : Send + Sync + ' static { 16 async fn create_user (& self , request : CreateUserRequest ) -> Result < UserResponse , UserError >; 17 18 async fn get_current_user ( 19 & self , 20 request : GetCurrentUserRequest , 12 use crate :: util :: auth ::is_reserved_name; 13 14 #[async_trait] 15 pub trait UserService : Send + Sync + ' static { 16 async fn create_user (& self , request : CreateUserRequest ) -> Result <( ) , UserError >; 17 18 async fn get_current_user ( 19 & self , 20
60 U : UserRepository , 61 R : RepositoryRepository , 62 S : SupabaseClient , 63 { 64 async fn create_user (& self , request : CreateUserRequest ) -> Result < UserResponse , UserError > { 65 let name = request .name. to_string (); 66 67
71 if self .user_repo. is_name_taken (& name ). await ? { 72 return Err ( UserError :: NameTaken ( name )); 73 } 74 75 if self . user_repo . is_email_taken ( & request . email ) . await ? { 76 1 " use client " ; 2 3 import type { FormEvent } from " react " ; 4 import { signup } from " @/actions " ; .. 5 6 export default function SignupForm () { 7 async function handleSubmit ( event : FormEvent < HTMLFormElement > ) { 8 event . preventDefault ( ) ;.. .. .. .. 9 const formData = new FormData ( event . currentTarget ); 10 .. .. .. .. .. .. .. 11 await signup ( formData ); 12 } .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 13 14 return ( 15 <form className = " flex flex-col " onSubmit ={ handleSubmit } > 16 <p className = " pb - 4 " > Sign-up. </p> 17 18 <input type = " email " name = " email " placeholder = " Email " /> 19 <input type = " password " name = " password " placeholder = " Password " /> 20 1 " use client " ; 2 3 import { useActionState , useEffect , useState } from " react " ; 4 import { type AuthActionResult , signup } from " @/actions " ; 5 import { cn , validateEmail , validatePassword , validateName }
17 18 <input type = " email " name = " email " placeholder = " Email " /> 19 <input type = " password " name = " password " placeholder = " Password " /> 20 21 <div className = " flex flex - row left 6 createAnswer , 7 createAnswerComment , 8 createQuestion , 9 createQuestionComment , 10 createRepository , .. 11 getCurrentUser , 12 updateAnswer , 13 updateComment , 14 updateQuestion , 6 createAnswer , 7 createAnswerComment , 8 createQuestion , 9 createQuestionComment , 10 createRepository , 11 createUser , 12 getCurrentUser , 13 updateAnswer , 14 updateComment , 15 updateQuestion ,
34 | { success : true } 35 | { success : false ; error : string } ; 36 37 export async function signup ( formData : FormData ): Promise < AuthActionResult > { 38 const supabase = await createSupabaseClient (); 39 40 // todo: add validation 1 import " server-only " ; 2 3 import { .. 4 type UserRepositoriesResponse , 5 UserRepositoriesResponseSchema , 6 type UserResponse , 7 UserResponseSchema , 1 import " server-only " ; 2 3 import { 4 type CreateUserRequest , 5 type UserRepositoriesResponse , 6 UserRepositoriesResponseSchema , 7 type UserResponse , 8 UserResponseSchema ,
8 } from " ../dto " ; 9 import { getSession } from " ../supabase " ; 10 import { authFetch , GITDOT_SERVER_URL , handleResponse , NotFound } from " ./util " ; 11 12 export async function getUser ( username : string ): Promise < UserResponse | null > { 1 import { z } from " zod " ; 2 .. .. .. .. .. .. .. 3 export const UserResponseSchema = z . object ( { 4 id : z . uuid (), 5 name : z . string (), 6 email : z . string (), 1 import { z } from " zod " ; 2 3 export const CreateUserRequestSchema = z . object ( { 4 name : z . string ( ) , 5 email : z . string ( ) , 6 153 return false ; 154 } 155 return emailTester . test ( email ); 156 } 157 .. .. .. 158 export function validatePassword ( password : string ): boolean { 159 return !! password && password . length >= 8 ; 160 } 161 153 return false ; 154 } 155 return emailTester . test ( email ); 156 } 157 158 export function validateName ( username : string ) : boolean { 159 return ! ! username && username . length >= (
_
) =>
StatusCode
::
CONFLICT
,
71 UserError :: SupabaseError ( _ ) => StatusCode :: INTERNAL_SERVER_ERROR ,
72 UserError :: DatabaseError ( _ ) => StatusCode :: INTERNAL_SERVER_ERROR ,
73 };
):
Json
<
CreateUserServerRequest
>,
11 ) -> Result < AppResponse <( ) >, AppError > {
12 let create_request = CreateUserRequest :: new (& request .name, & request .email, & request .password)?;
13 state
14 .user_service
15 . create_user ( create_request )
16 . await
17 . map_err ( AppError :: from )
18 . map ( | _ | AppResponse :: new ( StatusCode :: CREATED , ()))
19 }
20
sha2
=
{
workspace
=
true
}
17 sqlx = { workspace = true }
18 supabase-lib-rs = { workspace = true }
19 thiserror = { workspace = true }
<( ) ,
Error
>;
8 }
9
10 #[derive( Debug , Clone )]
11 pub struct SupabaseClientImpl {
.
auth
()
30 . sign_up_with_email_and_password ( email , password )
..
..
..
..
..
31 . await ?;
32 response
33 . user
34 . ok_or_else ( | | Error :: auth ( " Failed to create user " ))
35 }
36 }
37
22 } 23 24 #[async_trait] 25 impl SupabaseClient for SupabaseClientImpl { 26 async fn create_user (& self , name : & str , email : & str , password : & str ) -> Result <( ) , Error > { 27 self .client .. 28 . auth () 29 . sign_up_with_email_password_and_data ( 30 email , 31 password , 32 Some ( serde_json :: json ! ( { " name " : name } ) ) , 33 Option :: None , 34 ) 35 . await ?; 36 .. 37 Ok (()) 38 } 39 } 40 (&
self
,
id
:
Uuid
) ->
Result
<
Option
<
User
>,
Error
>;
12
)
->
Result
<
User
,
Error
> {
36 let user = sqlx :: query_as :: < _ , User > (
37 " INSERT INTO users (id, name, email) VALUES ($1, $2, $3) RETURNING id, name, email, created_at " ,
38 )
39 . bind ( id )
40 . bind ( name )
41 . bind ( email )
42 . fetch_one ( & self . pool )
43 . await ? ;
44
45 Ok ( user )
46 }
47
48 async fn get (& self , user_name : & str ) -> Result < Option < User >, Error > {
49 let user = sqlx :: query_as ::< _ , User >(
50 " SELECT id, email, name, created_at FROM users WHERE name = $1 " ,
30 31 #[async_trait] 32 impl UserRepository for UserRepositoryImpl { 33 async fn get (& self , user_name : & str ) -> Result < Option < User >, Error > { 34 let user = sqlx :: query_as ::< _ , User >( .. .. .. .. .. .. .. .. .. .. .. .. 35 " SELECT id, email, name, created_at FROM users WHERE name = $1 " , 36 ) 37 . bind ( user_name ) 38 . fetch_optional (& self .pool) request
:
GetCurrentUserRequest
,
if
is_reserved_name
(&
name
) {
68 return Err ( UserError :: ReservedName ( name ));
60 U : UserRepository , 61 R : RepositoryRepository , 62 S : SupabaseClient , 63 { 64 async fn create_user (& self , request : CreateUserRequest ) -> Result <( ) , UserError > { 65 let name = request .name. to_string (); 66 67 if is_reserved_name (& name ) { 68 return Err ( UserError :: ReservedName ( name )); return
Err
(
UserError
::
EmailTaken
(
request
. email .
clone
( ) ) ) ;
75 if self . user_repo . is_email_taken ( & request . email ) . await ? {
76 return Err ( UserError :: EmailTaken ( request . email . clone ( ) ) ) ;
77 }
78
79 let supabase_user = self
80 .supabase_client
81 . create_user (& request .email, & request .password)
82 . await ?;
83
84 let user = self
85 . user_repo
86 . create ( supabase_user . id , & name , & request . email )
87 . await ? ;
88
89 Ok ( user . into ())
90 }
91
92 async fn get_current_user (
93 & self ,
71 if self .user_repo. is_name_taken (& name ). await ? { 72 return Err ( UserError :: NameTaken ( name )); 73 } 74 75 // Once Supabase user is created, the Postgres trigger will then 76 // create the corresponding user row in users table. 75 // Once Supabase user is created, the Postgres trigger will then 76 // create the corresponding user row in users table. .. .. 77 self .supabase_client .. 78 . create_user (& name , &request .email, & request .password) .. .. .. .. .. 79 . await ?; 80 81 Ok (()) 82 } 83 84 async fn get_current_user ( 85 & self , from
"
@/util
"
;
6
7 export default function SignupForm () {
8 const [ email , setEmail ] = useState ( " " ) ;
9 const [ name , setName ] = useState ( " " ) ;
10 const [ password , setPassword ] = useState ( " " ) ;
11 const [ debouncedPassword , setDebouncedPassword ] = useState ( " " ) ;
12
13 useEffect ( ( ) => {
14 const timer = setTimeout ( ( ) => {
15 setDebouncedPassword ( password ) ;
16 } , 200 );
17
18 return ( ) => clearTimeout ( timer ) ;
19 } , [ password ] ) ;
20
21 const [ state , formAction , isPending ] = useActionState (
22 async ( _prevState : AuthActionResult | null , formData : FormData ) => {
23 return await signup ( formData );
24 } ,
25 null ,
26 ) ;
27
28 const canSubmit =
29 validateEmail ( email ) &&
30 validateName ( name ) &&
31 validatePassword ( debouncedPassword ) &&
32 ! isPending ;
33
34 if ( state ?. success ) {
35 return (
36 < div className = " flex flex-col text-sm w-sm " >
37 < p className = " pb-2 " > Check your email. </ p >
38 < p className = " text-primary/60 " >
39 We sent a verification link to { email } . Click the link to verify your
40 account.
41 </ p >
42 </ div >
43 ) ;
44 }
45
46 return (
47 <form action ={ formAction } className = " flex flex-col text-sm w-sm " >
48 <p className = " pb - 2 " > Sign up. </p>
49
50 <input
51 type = " email "
52 name = " email "
-
align
pt
-
4
"
>
22 <button type = " submit " className = " font-bold " >
23 Submit
..
..
..
..
..
..
..
..
..
24 </button>
25 </div>
26 </form>
27 );
28 }
73 onChange = { ( e ) => setPassword ( e . target . value ) } 74 className = " border-border border-b ring-0 outline-0 focus:border-black transition-colors duration-150 " 75 /> 76 77 <div className = " flex flex - row mt - 2 w - full justify - end " > 78 <button 79 type = " submit " 80 className ={ cn ( 81 " underline transition-all duration-300 " , 82 canSubmit 83 ? " cursor-pointer decoration-current " 84 : " text-primary/60 cursor-not-allowed decoration-transparent " , 85 ) } 86 disabled = { isPending || email === " " || name === " " || password === " " } 87 > 88 { isPending ? " Submitting... " : " Submit. " } 89 </button> 90 </div> 91 92 { state && " error " in state && ( 93 < p className = " text-red-500 " > { state . error } </ p > 41 const email = formData . get ( " email " ) as string ;
42 const password = formData . get ( " password " ) as string ;
43
44 const { error } = await supabase . auth . signUp ( {
45 email ,
46 password ,
47 } );
48
49 if ( error ) {
50 return { success : false , error : error . message } ;
51 }
52 return { success : true } ;
53 }
54
35 | { success : true } 36 | { success : false ; error : string } ; 37 38 export async function signup ( formData : FormData ): Promise < AuthActionResult > { 39 const email = formData . get ( " email " ) as string ; .. .. 40 const name = formData . get ( " name " ) as string ; 41 const password = formData . get ( " password " ) as string ; 42 43 const result = await createUser ( { name , email , password } ); .. .. .. .. 44 if ( " error " in result ) { 45 return { success : false , error : result . error } ; 46 } 47 48 return { success : true } ; 49 } ..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
..
13 const response = await authFetch ( `${ GITDOT_SERVER_URL } /user/ ${ username }` );
14 return await handleResponse ( response , UserResponseSchema );
15 }
16
8 UserResponseSchema , 9 } from " ../dto " ; 10 import { getSession } from " ../supabase " ; 11 import { authFetch , GITDOT_SERVER_URL , handleResponse , NotFound } from " ./util " ; 12 13 export type CreateUserResult = { success : true } | { error : string } ; 14 15 export async function createUser ( 16 request : CreateUserRequest , 17 ) : Promise < CreateUserResult > { 18 const response = await fetch ( ` ${GITDOT_SERVER_URL } /user ` , { 19 method : " POST " , 20 headers : { " Content-Type " : " application/json " } , 21 body : JSON . stringify ( request ) , 22 } ) ; 23 24 if ( ! response . ok ) { 25 try { 26 const data = await response . json ( ) ; 27 return { error : data . message ?? " Failed to create user " } ; 28 } catch { 29 return { error : " Failed to create user " } ; 30 } 31 } 32 33 return { success : true } ; 34 } 35 36 export async function getUser ( username : string ): Promise < UserResponse | null > { 37 const response = await authFetch ( `${ GITDOT_SERVER_URL } /user/ ${ username }` ); 38 return await handleResponse ( response , UserResponseSchema ); password
:
z
.
string
( ) ,
7 } ) ;
8
9 export type CreateUserRequest = z . infer < typeof CreateUserRequestSchema > ;
10
11 export const UserResponseSchema = z . object ( {
12 id : z . uuid (),
13 name : z . string (),
2
;
160 }
161
162 export function validatePassword ( password : string ): boolean {
163 return !! password && password . length >= 8 ;
164 }