diff --git a/crates/backend/src/controller.rs b/crates/backend/src/controller.rs new file mode 100644 index 0000000..7af3245 --- /dev/null +++ b/crates/backend/src/controller.rs @@ -0,0 +1,3 @@ +mod user; + +pub use user::UserController; diff --git a/crates/backend/src/controller/user.rs b/crates/backend/src/controller/user.rs new file mode 100644 index 0000000..e38e558 --- /dev/null +++ b/crates/backend/src/controller/user.rs @@ -0,0 +1,79 @@ +use actix_web::{error::ErrorInternalServerError, web, Responder}; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, +}; +use sea_orm::{ActiveModelTrait, ActiveValue, DatabaseConnection, EntityTrait}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::AppState; + +pub struct UserController; + +#[derive(Serialize)] +pub struct UserWithoutPassword { + id: Uuid, + name: String, + email: String, +} + +#[derive(Deserialize)] +pub struct CreateUserDto { + name: String, + email: String, + password: String, +} + +impl From for UserWithoutPassword { + fn from(value: entity::user::Model) -> Self { + Self { + id: value.id, + name: value.name, + email: value.email, + } + } +} + +impl UserController { + pub async fn list_users(state: web::Data) -> actix_web::Result { + let db = &state.db; + let users = entity::prelude::User::find() + .all(db) + .await + .map_err(ErrorInternalServerError)?; + Ok(web::Json( + users + .into_iter() + .map(UserWithoutPassword::from) + .collect::>(), + )) + } + + pub async fn create_user( + state: web::Data, + user: web::Json, + ) -> actix_web::Result { + let db = &state.db; + let user = user.into_inner(); + + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + + let password_hash = argon2 + .hash_password(user.password.as_bytes(), &salt) + .map_err(ErrorInternalServerError)?; + + let user = entity::user::ActiveModel { + id: ActiveValue::NotSet, + name: ActiveValue::Set(user.name), + email: ActiveValue::Set(user.email), + hash: ActiveValue::Set(password_hash.to_string()), + salt: ActiveValue::Set(salt.to_string()), + }; + + let result = user.insert(db).await.map_err(ErrorInternalServerError)?; + + Ok(web::Json(UserWithoutPassword::from(result))) + } +} diff --git a/crates/backend/src/main.rs b/crates/backend/src/main.rs index 4a91751..fc72f6f 100644 --- a/crates/backend/src/main.rs +++ b/crates/backend/src/main.rs @@ -1,17 +1,41 @@ use actix_web::{web, App, HttpResponse, HttpServer, Responder}; +use sea_orm::{Database, DatabaseConnection}; +use std::env; + +use routes::config; +mod controller; +mod routes; + +#[derive(Clone)] +struct AppState { + db: DatabaseConnection, +} #[actix_web::main] async fn main() -> std::io::Result<()> { #[cfg(debug_assertions)] println!("Running debug build -> enabling permissive CORS"); + dotenvy::dotenv().ok(); + + let db_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set"); + + let conn = Database::connect(&db_url) + .await + .expect("Connecting to Database failed"); + + let state = AppState { db: conn }; + HttpServer::new(move || { let cors = if cfg!(debug_assertions) { actix_cors::Cors::permissive() } else { actix_cors::Cors::default() }; - App::new().wrap(cors).route("/", web::get().to(index)) + App::new() + .wrap(cors) + .app_data(web::Data::new(state.clone())) + .configure(config) }) .bind(("127.0.0.1", 8080))? .run() diff --git a/crates/backend/src/routes.rs b/crates/backend/src/routes.rs new file mode 100644 index 0000000..0a20d5a --- /dev/null +++ b/crates/backend/src/routes.rs @@ -0,0 +1,14 @@ +use crate::controller::UserController; +use actix_web::web; + +pub fn config(cfg: &mut web::ServiceConfig) { + cfg.service( + web::scope("/api/v1") + .service( + web::resource("/users") + .get(UserController::list_users) + .post(UserController::create_user), + ) + .service(web::resource("/users/{user_id}")), + ); +}