backend/src/handler/user.rs
backend/src/handler/user/create_user.rs
1 use serde :: Deserialize ;
2
3 #[ derive ( Debug , Clone , Deserialize )]
4 pub struct CreateUserServerRequest {
5 pub name : String ,
6 pub email : String ,
7 pub password : String ,
8 }
9 1 use axum :: { Json , extract :: State , http :: StatusCode };
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 1 use crate :: dto :: OwnerName ;
2 use crate :: error :: UserError ;
3
4 #[ derive ( Debug , Clone )]
5 pub struct CreateUserRequest {
6 pub name : OwnerName ,
7 pub email : String ,
8 pub password : String ,
9 }
10
11 impl CreateUserRequest {
12 pub fn new ( name : & str , email : & str , password : & str ) -> Result < Self , UserError > {
13 Ok ( Self {
14 name : OwnerName :: try_new ( name )
15 . map_err ( | e | UserError :: InvalidUserName ( e . to_string ())) ? ,
16 email : email . to_string (),
17 password : password . to_string (),
18 })
19 }
20 }
21 3 #[derive( Debug , Clone , PartialEq , Eq )] 4 pub struct Settings { 5 pub port : String , 6 pub git_project_root : String , 7 pub database_url : String , .. .. 8 pub supabase_jwt_public_key : String , 9 pub oauth_device_verification_uri : String , 10 } 11 3 #[derive( Debug , Clone , PartialEq , Eq )] 4 pub struct Settings { 5 pub port : String , 6 pub git_project_root : String , 7 pub database_url : String , 8 pub supabase_url : String , 9 pub supabase_anon_key : String ,
16 git_project_root : env :: var ( " GIT_PROJECT_ROOT " ) 17 . unwrap_or_else ( | _ | " /srv/git " . to_string ()), 18 database_url : env :: var ( " DATABASE_URL " ). expect ( " DATABASE_URL must be set " ), 19 supabase_jwt_public_key : env :: var ( " SUPABASE_JWT_PUBLIC_KEY " ) .. .. .. .. .. .. .. .. .. 1 use chrono ::{ DateTime , Utc }; 2 use serde :: Serialize ; 3 use uuid :: Uuid ; 4 1 mod create_user ; 2 3 use chrono ::{ DateTime , Utc }; 4 use serde :: Serialize ; 5 use uuid :: Uuid ; 6 7 use gitdot_core :: dto :: UserResponse ; 8 9 pub use create_user :: CreateUserServerRequest ; 10 11 #[derive( Debug , Clone , PartialEq , Eq , Serialize )] 12 pub struct UserServerResponse { 13 pub id : Uuid , .. 1 mod get_current_user; 2 mod get_user; 3 mod list_user_repositories; 4 1 mod create_user ; 2 mod get_current_user; 3 mod get_user; 4 mod list_user_repositories; 5
11 use list_user_repositories ::list_user_repositories; 12 13 pub fn create_user_router () -> Router < AppState > { 14 Router :: new () 15 . route ( " /user " , get ( get_current_user )) 16 . route ( " /user/ { user_name }" , get ( get_user )) 17 . route ( 18 " /user/ { user_name } /repositories " , 19 get ( list_user_repositories ), 13 use list_user_repositories ::list_user_repositories; 14 15 pub fn create_user_router () -> Router < AppState > { 16 Router :: new () 17 . route ( " /user " , get ( get_current_user ). post ( create_user ) )18 . route ( " /user/ { user_name }" , get ( get_user )) 19 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 , email : & str , password : & str ) -> Result <( ) , Error >; 8 } 9 10 #[derive( Debug , Clone )] 11 pub struct SupabaseClientImpl { 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
12 client : Client , 13 } 14 15 impl SupabaseClientImpl { 16 pub fn new ( supabase_project_url : & str , supabase_anon_key : & str ) -> Result < Self , Error > {17 Ok ( Self { 18 client : Client :: .. 1 mod get_current_user; 2 mod get_user; 3 mod list_user_repositories; 4 1 mod create_user ; 2 mod get_current_user; 3 mod get_user; 4 mod list_user_repositories; 5
6 use uuid :: Uuid ; 7 8 use crate :: model :: User ; 9 10 pub use get_current_user :: GetCurrentUserRequest ; .. 11 pub use get_user :: GetUserRequest ; 12 pub use list_user_repositories :: ListUserRepositoriesRequest ; 13 14 #[derive( Debug , Clone )] 6 use chrono ::{ DateTime , Utc }; 7 use uuid :: Uuid ; 8 9 use crate :: model :: User ; 10 11 pub use create_user :: CreateUserRequest ; 12 pub use get_current_user :: GetCurrentUserRequest ; 13 pub use get_user :: GetUserRequest ; 14 6 InvalidUserName ( String ), 7 8 #[error( " User not found: { 0 }" )] 9 NotFound ( String ), 10 .. .. .. .. .. .. .. .. .. .. .. 11 #[error( " Database error: { 0 }" )] 12 DatabaseError (#[from] sqlx :: Error ), 6 InvalidUserName ( String ), 7 8 #[error( " User not found: { 0 }" )] 9 NotFound ( String ), 10 11 # [ error ( " Name already taken: { 0 }" ) ] 12 NameTaken ( String ) , 13 14 # 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 (& self , id : Uuid ) -> Result < Option < User >, Error >; 12 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
30 let user = sqlx :: query_as ::< _ , User >( 31 " SELECT id, email, name, created_at FROM users WHERE name = $1 " , 32 ) 33 . bind ( user_name ) 34 . fetch_optional (& self .pool) .. .. .. .. .. ..
.. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. 1 use async_trait ::async_trait; 2 3 use crate :: dto ::{ 4 GetCurrentUserRequest , GetUserRequest , ListUserRepositoriesRequest , RepositoryResponse , 5 UserResponse , 6 }; 7 use crate :: error :: UserError ; 8 use crate :: repository ::{ 2 3 use crate :: client :: { SupabaseClient , SupabaseClientImpl } ; 4 use crate :: dto ::{ 5 CreateUserRequest , GetCurrentUserRequest , GetUserRequest , ListUserRepositoriesRequest , 6 RepositoryResponse , UserResponse , 7 }; 8 use crate :: error :: UserError ; 9 use crate :: repository ::{
10 }; 11 12 #[async_trait] 13 pub trait UserService : Send + Sync + ' static { 14 async fn get_current_user ( .. 15 & self , 16 request : GetCurrentUserRequest , 17 ) -> Result < UserResponse , UserError >; 18
24 ) -> Result < Vec < RepositoryResponse >, UserError >; 25 } 26 27 #[derive( Debug , Clone )] 28 pub struct UserServiceImpl < U , R > .. .. 29 where 30 U : UserRepository , 31 R :
46 #[async_trait] 47 impl < U , R > UserService for UserServiceImpl < U , R > 48 where 49 U : UserRepository , 50 R : RepositoryRepository , .. 51 { 52 async fn get_current_user ( 53 & self ,
43 } 44 } 45 46 #[async_trait] 47 impl < U , R > UserService for UserServiceImpl < U , R > 48 where 49 U : UserRepository , 50 R : RepositoryRepository , 51 { 53 } 7 8 # Posgres direct connection URI 9 DATABASE_URL =postgresql://postgres:[PASSWORD_HERE]@db.[PROJECT_ID_HERE].supabase.co:5432/postgres 10 11 # Supabase JWT public key in PEM format 12 SUPABASE_JWT_PUBLIC_KEY ="-----BEGIN PUBLIC KEY----- 13 ... 14 ... 15 -----END PUBLIC KEY-----" 7 8 # Posgres direct connection URI 9 DATABASE_URL =postgresql://postgres:[PASSWORD_HERE]@db.[PROJECT_ID_HERE].supabase.co:5432/postgres 10 11 # Supabase variables 12 SUPABASE_URL = " https : / / your - project . supabase . co " 13 SUPABASE_ANON_KEY = " your - anon - key " 14 SUPABASE_JWT_PUBLIC_KEY ="-----BEGIN PUBLIC KEY----- 15 ... 2 3 use axum :: extract :: FromRef ; 4 use sqlx :: PgPool ; 5 6 use gitdot_core :: client ::{ Git2Client , GitHttpClientImpl }; 7 use gitdot_core :: repository ::{ 8 OAuthRepositoryImpl , OrganizationRepositoryImpl , QuestionRepositoryImpl , 9 RepositoryRepositoryImpl , UserRepositoryImpl , 10 }; 2 3 use axum :: extract :: FromRef ; 4 use sqlx :: PgPool ; 5 6 use gitdot_core :: client ::{ Git2Client , GitHttpClientImpl , SupabaseClientImpl }; 7 use gitdot_core :: repository ::{ 8 OAuthRepositoryImpl , OrganizationRepositoryImpl , QuestionRepositoryImpl , 9 RepositoryRepositoryImpl , UserRepositoryImpl , 10
30 31 impl AppState { 32 pub fn new ( settings : Arc < Settings >, pool : PgPool ) -> Self { 33 let git_client = Git2Client :: new ( settings .git_project_root. clone ()); 34 let git_http_client = GitHttpClientImpl :: new ( settings .git_project_root. clone ()); ..
47 )); 48 let user_service = Arc :: new ( UserServiceImpl :: new ( user_repo . clone (), repo_repo . clone ())); 49 let org_service = Arc :: new ( OrganizationServiceImpl :: new ( 50 org_repo . clone (), 51 user_repo . clone (), .. .. 64 } 65 AppError :: User ( e ) => { 66 let status_code = match e { 67 UserError :: NotFound ( _ ) => StatusCode :: NOT_FOUND , 68 UserError :: InvalidUserName ( _ ) => StatusCode :: BAD_REQUEST , .. .. .. 69 UserError :: DatabaseError ( _ ) => StatusCode :: INTERNAL_SERVER_ERROR , 70 }; 71 let response = AppResponse :: new ( 72 status_code , 64 } 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 };
..
35
36 let org_repo = OrganizationRepositoryImpl :: new ( pool . clone ());
37 let repo_repo = RepositoryRepositoryImpl :: new ( pool . clone ());
38 let user_repo = UserRepositoryImpl :: new ( pool . clone ());
30 31 impl AppState { 32 pub fn new ( settings : Arc < Settings >, pool : PgPool ) -> Self { 33 let git_client = Git2Client :: new ( settings .git_project_root. clone ()); 34 let git_http_client = GitHttpClientImpl :: new ( settings .git_project_root. clone ()); 35 let supabase_client = 36 SupabaseClientImpl :: new ( & settings . supabase_url , & settings . supabase_anon_key ) ; 37 38 let org_repo = OrganizationRepositoryImpl :: new ( pool . clone ()); 39 let repo_repo = RepositoryRepositoryImpl :: new ( pool . clone ()); 40 let user_repo = UserRepositoryImpl :: new ( pool . clone ()); 52 repo_repo . clone (),
53 ));
54 let repo_service = Arc :: new ( RepositoryServiceImpl :: new (
55 git_client . clone (),
47 question_repo . clone (), 48 user_repo . clone (), 49 )); 50 let user_service = Arc :: new ( UserServiceImpl :: new ( 51 user_repo . clone (), 52 repo_repo . clone (), 53 supabase_client . clone ( ) , 54 )); 55 let org_service = Arc :: new ( OrganizationServiceImpl :: new ( 56 org_repo . clone (), 57 user_repo . clone (), ,
70 UserError :: EmailTaken ( _ ) => StatusCode :: CONFLICT ,
71 UserError :: SupabaseError ( _ ) => StatusCode :: INTERNAL_SERVER_ERROR ,
72 UserError :: DatabaseError ( _ ) => StatusCode :: INTERNAL_SERVER_ERROR ,
73 };
74 let response = AppResponse :: new (
75 status_code ,
10 pub supabase_jwt_public_key : String ,
11 pub oauth_device_verification_uri : String ,
12 }
13
20 . expect ( " SUPABASE_JWT_PUBLIC_KEY must be set " ),
..
..
..
21 oauth_device_verification_uri : env :: var ( " OAUTH_DEVICE_VERIFICATION_URI " )
22 . expect ( " OAUTH_DEVICE_VERIFICATION_URI must be set " ),
23 })
24 }
16 Ok ( Self { 17 port : env :: var ( " PORT " ). unwrap_or_else ( | _ | " 8080 " . to_string ()), 18 git_project_root : env :: var ( " GIT_PROJECT_ROOT " ) 19 . unwrap_or_else ( | _ | " /srv/git " . to_string ()), 20 database_url : env :: var ( " DATABASE_URL " ). expect ( " DATABASE_URL must be set " ), 21 supabase_url : env :: var ( " SUPABASE_URL " ) . expect ( " SUPABASE_URL must be set " ) , 22 supabase_anon_key : env :: var ( " SUPABASE_ANON_KEY " ) 23 . expect ( " SUPABASE_ANON_KEY must be set " ) , 24 supabase_jwt_public_key : env :: var ( " SUPABASE_JWT_PUBLIC_KEY " ) 25 . expect ( " SUPABASE_JWT_PUBLIC_KEY must be set " ), 26 oauth_device_verification_uri : env :: var ( " OAUTH_DEVICE_VERIFICATION_URI " ) 27 . expect ( " OAUTH_DEVICE_VERIFICATION_URI must be set " ), .
route
(
20 " /user/ { user_name } /repositories " ,
21 get ( list_user_repositories ),
,
Error
>;
8 }
9
10 #[derive( Debug , Clone )]
11 pub struct SupabaseClientImpl {
new
(
supabase_project_url
,
supabase_anon_key
)? ,
..
19 })
20 }
21 }
22
23 #[async_trait]
24 impl SupabaseClient for SupabaseClientImpl {
25 async fn create_user (& self , email : & str , password : & str ) -> Result <( ) , Error > {
26 self .client
..
..
..
27 . auth ()
28 . sign_up_with_email_and_password ( email , password )
29 . await ?;
30 Ok (())
31 }
32 }
33
12 client : Client , 13 } 14 15 impl SupabaseClientImpl { 16 pub fn new ( supabase_project_url : & str , supabase_anon_key : & str ) -> Self { 17 Self { 18 client : Client :: new ( supabase_project_url , supabase_anon_key ) 19 . expect ( " Failed to create Supabase client " ) ,20 } 21 } 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 . 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 pub
use
list_user_repositories
::
ListUserRepositoriesRequest
;
15
[
error
(
"
Email already taken:
{
0
}"
) ]
15 EmailTaken ( String ) ,
16
17 #[error( " Database error: { 0 }" )]
18 DatabaseError (#[from] sqlx :: Error ),
19
20 # [ error ( " Supabase error: { 0 }" ) ]
21 SupabaseError ( # [ from ] supabase :: Error ) ,
22 }
23
:
&
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 >;
14
15 async fn get_by_emails (& self , emails : &[ String ]) -> Result < Vec < User >, Error >;
16
17 async fn is_name_taken ( & self , name : & str ) -> Result < bool , Error > ;
18
19 async fn is_email_taken ( & self , email : & str ) -> Result < bool , Error > ;
20 }
21
22 #[derive( Debug , Clone )]
23 pub struct UserRepositoryImpl {
..
..
..
..
..
..
35 . await ?;
36
37 Ok ( user )
38 }
30 } 31 } 32 33 #[async_trait] 34 impl UserRepository for UserRepositoryImpl { 35 async fn create ( & self , id : Uuid , name : & str , email : & str ) -> 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 " , ..
..
..
..
..
..
..
85 async fn is_name_taken ( & self , name : & str ) -> Result < bool , Error > { 86 let exists = sqlx :: query_scalar :: < _ , bool > ( 87 r #" 88 SELECT EXISTS( 89 SELECT 1 FROM users WHERE name = $1 90 UNION 91 SELECT 1 FROM organizations WHERE name = $1 92 ) 93 "#, 94 ) 95 . bind ( name ) 96 . fetch_one ( & self . pool ) 97 . await ? ; 98 99 Ok ( exists ) 100 } 101 102 async fn is_email_taken ( & self , email : & str ) -> Result < bool , Error > { 103 let exists = 104 sqlx :: query_scalar :: < _ , bool > ( " SELECT EXISTS(SELECT 1 FROM users WHERE email = $1) " ) 105 . bind ( email ) 106 . fetch_one ( & self . pool ) 107 . await ? ; 108 109 Ok ( exists ) 110 } 10 RepositoryRepository , RepositoryRepositoryImpl , UserRepository , UserRepositoryImpl , 11 }; 12 13 #[async_trait] 14 pub trait UserService : Send + Sync + ' static { 15 async fn create_user ( & self , request : CreateUserRequest ) -> Result < UserResponse , UserError > ; 16 17 async fn get_current_user ( 18 & self , 19 request : GetCurrentUserRequest , RepositoryRepository
,
32 {
33 user_repo : U ,
34 repo_repo : R ,
35 }
36
37 impl UserServiceImpl < UserRepositoryImpl , RepositoryRepositoryImpl > {
38 pub fn new ( user_repo : UserRepositoryImpl , repo_repo : RepositoryRepositoryImpl ) -> Self {
39 Self {
40 user_repo ,
41 repo_repo ,
27 ) -> Result < Vec < RepositoryResponse >, UserError >; 28 } 29 30 #[derive( Debug , Clone )] 31 pub struct UserServiceImpl < U , R , S > 32 where 33 U : UserRepository , 34 R : RepositoryRepository , 35 S : SupabaseClient , 36 { 37 user_repo : U , 38 repo_repo : R , 39 supabase_client : S , 40 } 41 42 impl UserServiceImpl < UserRepositoryImpl , RepositoryRepositoryImpl , SupabaseClientImpl > { 43 pub fn new ( 44 user_repo : UserRepositoryImpl , 45 repo_repo : RepositoryRepositoryImpl , 46 supabase_client : SupabaseClientImpl , 54
request
:
GetCurrentUserRequest
,
46 supabase_client : SupabaseClientImpl , 47 ) -> Self { 48 Self { 49 user_repo , 50 repo_repo , 51 supabase_client , 52 } 53 } 54 } 55 54 }
55
56 #[async_trait]
57 impl < U , R , S > UserService for UserServiceImpl < U , R , S >
58 where
59 U : UserRepository ,
60 R : RepositoryRepository ,
61 S : SupabaseClient ,