diff --git a/bruno/user/Create user.bru b/bruno/user/Create user.bru new file mode 100644 index 0000000..4425574 --- /dev/null +++ b/bruno/user/Create user.bru @@ -0,0 +1,19 @@ +meta { + name: Create user + type: http + seq: 1 +} + +post { + url: {{api_base}}/user + body: json + auth: inherit +} + +body:json { + { + "username": "hure", + "name": "Dumme Nutte", + "password": "nüttchen" + } +} diff --git a/crates/backend/src/controller/auth.rs b/crates/backend/src/controller/auth.rs index 4a62ff8..a2e2af2 100644 --- a/crates/backend/src/controller/auth.rs +++ b/crates/backend/src/controller/auth.rs @@ -22,4 +22,7 @@ async fn login( login_request: web::Json, session: Session, ) -> Result { + let login_request = login_request.into_inner(); + + todo!() } diff --git a/crates/backend/src/controller/project.rs b/crates/backend/src/controller/project.rs index 53aa642..2a1c16b 100644 --- a/crates/backend/src/controller/project.rs +++ b/crates/backend/src/controller/project.rs @@ -1,11 +1,10 @@ -use std::path; - use actix_web::{Result, delete, get, post, put, web}; use sea_orm::prelude::Uuid; use validator::Validate; use crate::db::Database; use crate::db::project::CreateProject; +use crate::entity; use crate::error::ApiError; pub fn setup(cfg: &mut actix_web::web::ServiceConfig) { diff --git a/crates/backend/src/controller/user.rs b/crates/backend/src/controller/user.rs index 480a0de..73dc07b 100644 --- a/crates/backend/src/controller/user.rs +++ b/crates/backend/src/controller/user.rs @@ -1,4 +1,7 @@ -use actix_web::{Responder, delete, get, post, put}; +use crate::{Database, entity, error::ApiError}; +use actix_web::{Responder, delete, get, post, put, web}; +use serde::Deserialize; +use validator::Validate; pub fn setup(cfg: &mut actix_web::web::ServiceConfig) { cfg.service(get_users) @@ -7,6 +10,13 @@ pub fn setup(cfg: &mut actix_web::web::ServiceConfig) { .service(delete_user); } +#[derive(Deserialize, Validate)] +struct CreateUser { + username: String, + name: String, + password: String, +} + #[get("")] async fn get_users() -> impl Responder { "" @@ -18,8 +28,16 @@ async fn get_user() -> impl Responder { } #[post("")] -async fn create_user() -> impl Responder { - "" +async fn create_user( + db: web::Data, + user: web::Json, +) -> Result, ApiError> { + let user = user.into_inner(); + let result = db + .create_user(user.name, user.username, user.password) + .await?; + + Ok(web::Json(result)) } #[put("")] diff --git a/crates/backend/src/db/entity/local_auth.rs b/crates/backend/src/db/entity/local_auth.rs index 6d6dc30..08483e0 100644 --- a/crates/backend/src/db/entity/local_auth.rs +++ b/crates/backend/src/db/entity/local_auth.rs @@ -9,9 +9,25 @@ pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: Uuid, pub hash: String, + pub password_change_required: bool, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] -pub enum Relation {} +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::Id", + to = "super::user::Column::Id", + on_update = "Cascade", + on_delete = "Cascade" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/backend/src/db/entity/user.rs b/crates/backend/src/db/entity/user.rs index 57973c4..03a6b23 100644 --- a/crates/backend/src/db/entity/user.rs +++ b/crates/backend/src/db/entity/user.rs @@ -9,16 +9,24 @@ pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: Uuid, #[sea_orm(unique)] + pub username: String, pub name: String, - pub role: String, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { + #[sea_orm(has_one = "super::local_auth::Entity")] + LocalAuth, #[sea_orm(has_many = "super::user_group_project::Entity")] UserGroupProject, } +impl Related for Entity { + fn to() -> RelationDef { + Relation::LocalAuth.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::UserGroupProject.def() diff --git a/crates/backend/src/db/project.rs b/crates/backend/src/db/project.rs index dff12bd..b3e1c26 100644 --- a/crates/backend/src/db/project.rs +++ b/crates/backend/src/db/project.rs @@ -2,7 +2,7 @@ use super::Database; use crate::error::ApiError; use log::debug; -use entity::project; +use crate::entity::project; use sea_orm::ActiveValue::{NotSet, Set, Unchanged}; use sea_orm::prelude::Uuid; use sea_orm::{ActiveModelTrait, DeleteResult, EntityTrait}; diff --git a/crates/backend/src/db/user.rs b/crates/backend/src/db/user.rs index be20e42..ca44682 100644 --- a/crates/backend/src/db/user.rs +++ b/crates/backend/src/db/user.rs @@ -1,11 +1,90 @@ -use super::Database; +use crate::error::ApiError; +use argon2::{ + Argon2, PasswordHash, PasswordVerifier, + password_hash::{PasswordHasher, SaltString, rand_core::OsRng}, +}; +use sea_orm::{ + ActiveModelTrait, + ActiveValue::{NotSet, Set}, + ColumnTrait, DbErr, EntityTrait, ModelTrait, QueryFilter, TransactionTrait, + prelude::Uuid, +}; + +use crate::{Database, entity}; + +use super::entity::local_auth; impl Database { - async fn create_user() {} + pub async fn create_user( + &self, + name: String, + username: String, + password: String, + ) -> Result { + let argon2 = Argon2::default(); - async fn verify_local_user() {} + let salt = SaltString::generate(&mut OsRng); + let hash = argon2 + .hash_password(password.as_bytes(), &salt) + .map_err(|err| ApiError::Argon2Error(err.to_string()))? + .to_string(); - async fn verify_ldap_user() {} + let user = self + .conn + .transaction::<_, entity::user::Model, DbErr>(|txn| { + Box::pin(async move { + let user = entity::user::ActiveModel { + id: NotSet, + name: Set(name), + username: Set(username), + }; - async fn change_user_password() {} + let user: entity::user::Model = user.insert(txn).await?; + + let local_auth = entity::local_auth::ActiveModel { + id: Set(user.id), + hash: Set(hash), + password_change_required: NotSet, + }; + + local_auth.insert(txn).await?; + Ok(user) + }) + }) + .await?; + Ok(user) + } + + pub async fn verify_local_user( + &self, + username: &str, + password: &str, + ) -> Result { + let user = entity::user::Entity::find() + .filter(entity::user::Column::Username.eq(username)) + .one(&self.conn) + .await? + .ok_or(ApiError::Unauthorized)?; + + let local_auth = user + .find_related(entity::local_auth::Entity) + .one(&self.conn) + .await? + .ok_or(ApiError::Unauthorized)?; + + let argon2 = Argon2::default(); + + let password_hash = PasswordHash::new(&local_auth.hash) + .map_err(|err| ApiError::Argon2Error(err.to_string()))?; + + if let Err(_) = argon2.verify_password(password.as_bytes(), &password_hash) { + return Err(ApiError::Unauthorized); + } + + Ok(()) + } + + pub async fn verify_ldap_user() {} + + pub async fn change_user_password() {} } diff --git a/crates/backend/src/error.rs b/crates/backend/src/error.rs index dfb4c67..094d5d6 100644 --- a/crates/backend/src/error.rs +++ b/crates/backend/src/error.rs @@ -1,4 +1,5 @@ use actix_web::{HttpResponse, ResponseError, cookie::time::error, http::StatusCode}; +use sea_orm::TransactionError; use thiserror::Error; #[derive(Error, Debug)] @@ -13,6 +14,8 @@ pub enum ApiError { BadRequest(String), #[error("Validation Error: {0}")] ValidationError(#[from] validator::ValidationErrors), + #[error("Argon2 Error: {0}")] + Argon2Error(String), } impl ResponseError for ApiError { @@ -23,6 +26,7 @@ impl ResponseError for ApiError { ApiError::Unauthorized => StatusCode::UNAUTHORIZED, ApiError::BadRequest(..) => StatusCode::BAD_REQUEST, ApiError::ValidationError(..) => StatusCode::BAD_REQUEST, + ApiError::Argon2Error(..) => StatusCode::INTERNAL_SERVER_ERROR, } } @@ -30,3 +34,12 @@ impl ResponseError for ApiError { HttpResponse::build(self.status_code()).body(self.to_string()) } } + +impl From> for ApiError { + fn from(value: TransactionError) -> Self { + Self::Database(match value { + TransactionError::Connection(e) => e, + TransactionError::Transaction(e) => e, + }) + } +} diff --git a/crates/backend/src/main.rs b/crates/backend/src/main.rs index ec7c65c..fb70dff 100644 --- a/crates/backend/src/main.rs +++ b/crates/backend/src/main.rs @@ -1,14 +1,13 @@ use actix_files::NamedFile; use actix_session::{SessionMiddleware, storage::RedisSessionStore}; use actix_web::{App, HttpResponse, HttpServer, cookie::Key, middleware::Logger, web}; -use argon2::Argon2; -use db::Database; use log::debug; mod controller; mod db; mod error; +pub use db::Database; pub use db::entity; #[derive(Clone)] @@ -36,7 +35,6 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { let app = App::new() .app_data(web::Data::new(database.clone())) - .app_data(web::Data::new(Argon2::default())) .app_data(web::Data::new(app_config.clone())) .wrap(Logger::default()) .wrap(SessionMiddleware::new( diff --git a/crates/migration/src/baseline.rs b/crates/migration/src/baseline.rs index dbfe516..7741d30 100644 --- a/crates/migration/src/baseline.rs +++ b/crates/migration/src/baseline.rs @@ -46,8 +46,8 @@ impl MigrationTrait for Migration { .table(User::Table) .if_not_exists() .col(pk_uuid(User::Id).extra("DEFAULT gen_random_uuid()")) - .col(string_uniq(User::Name)) - .col(string(User::Role)) + .col(string_uniq(User::Username)) + .col(string(User::Name)) .to_owned(), ) .await?; @@ -101,6 +101,15 @@ impl MigrationTrait for Migration { .if_not_exists() .col(pk_uuid(LocalAuth::Id)) .col(string(LocalAuth::Hash)) + .col(boolean(LocalAuth::PasswordChangeRequired).default(true)) + .foreign_key( + ForeignKey::create() + .name("fk-localauth-user") + .from(LocalAuth::Table, LocalAuth::Id) + .to(User::Table, User::Id) + .on_update(ForeignKeyAction::Cascade) + .on_delete(ForeignKeyAction::Cascade), + ) .to_owned(), ) .await @@ -123,6 +132,10 @@ impl MigrationTrait for Migration { manager .drop_table(Table::drop().table(UserGroupProject::Table).to_owned()) + .await?; + + manager + .drop_table(Table::drop().table(LocalAuth::Table).to_owned()) .await } } @@ -146,8 +159,8 @@ enum Group { enum User { Table, Id, + Username, Name, - Role, } #[derive(DeriveIden)] @@ -163,4 +176,5 @@ enum LocalAuth { Table, Id, Hash, + PasswordChangeRequired, }