parent
569e3753f0
commit
0ba58d90e3
@ -1,3 +1,4 @@
|
|||||||
pub mod company;
|
pub mod company;
|
||||||
pub mod in_process_transaction;
|
pub mod in_process_transaction;
|
||||||
pub mod transaction;
|
pub mod transaction;
|
||||||
|
pub mod user;
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
use crate::model::user::ActiveModel;
|
||||||
|
use sea_orm::{ActiveModelTrait, ConnectionTrait, DbErr, DeriveIntoActiveModel, IntoActiveModel};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::model;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Clone, DeriveIntoActiveModel, Serialize, Deserialize)]
|
||||||
|
pub struct NewUser {
|
||||||
|
pub email: String,
|
||||||
|
pub name: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewUser {
|
||||||
|
pub async fn create<C>(&self, db: &C) -> Result<model::user::Model, DbErr>
|
||||||
|
where
|
||||||
|
C: ConnectionTrait,
|
||||||
|
{
|
||||||
|
let res = self.clone().into_active_model().insert(db).await?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,133 @@
|
|||||||
|
use axum::{extract::State, Json};
|
||||||
|
use cookie::{time::Duration, Cookie, SameSite};
|
||||||
|
use jsonwebtoken::{encode, EncodingKey, Header};
|
||||||
|
use rand::RngCore;
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tower_cookies::Cookies;
|
||||||
|
|
||||||
|
use crate::{error::AppError, model, repo::user::NewUser, AppState, CONFIG};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UserLoginBody {
|
||||||
|
pub name: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct JWTClaim {
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
cookies: Cookies,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(payload): Json<UserLoginBody>,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let db = &state.db;
|
||||||
|
let user_opt = model::user::Entity::find()
|
||||||
|
.filter(model::user::Column::Name.eq(payload.name))
|
||||||
|
.one(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if user_opt.is_none() {
|
||||||
|
// To prevent timing attacks, we use the same verify function on a known password
|
||||||
|
argon2::verify_encoded("$argon2i$v=19$m=4096,t=3,p=1$CXr/AgSDawghR+GmOhM0wQ$4k2TCyoqkh/YaK9mh6uEa0eRZ/CIx3bfzJs5UnCcKjw", b"1234").unwrap();
|
||||||
|
return Err(AppError::NotFound("User not found".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = user_opt.unwrap();
|
||||||
|
|
||||||
|
let valid =
|
||||||
|
argon2::verify_encoded(&user.password, payload.password.as_bytes()).map_err(|e| {
|
||||||
|
error!("Error verifying the password for user {}: {}", user.name, e);
|
||||||
|
AppError::InternalServerError("There was an error verifying authentication".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return Err(AppError::Unauthorized);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a JWT and store it as a same site cookie
|
||||||
|
let claim = JWTClaim {
|
||||||
|
username: user.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
let token_str = encode(
|
||||||
|
&Header::default(),
|
||||||
|
&claim,
|
||||||
|
&EncodingKey::from_secret(b"some-secret"),
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to encode a JWT: {}", e);
|
||||||
|
AppError::InternalServerError("There was an error verifying authentication".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let cookie = Cookie::build("auth", token_str)
|
||||||
|
.domain(CONFIG.server_domain.clone())
|
||||||
|
.path("/")
|
||||||
|
.same_site(SameSite::Strict)
|
||||||
|
.secure(true)
|
||||||
|
.http_only(true)
|
||||||
|
.max_age(Duration::days(4))
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
cookies.add(cookie);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UserRegisterBody {
|
||||||
|
pub name: String,
|
||||||
|
pub email: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn register(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Json(payload): Json<UserRegisterBody>,
|
||||||
|
) -> Result<(), AppError> {
|
||||||
|
let db = &state.db;
|
||||||
|
let user_opt = model::user::Entity::find()
|
||||||
|
.filter(
|
||||||
|
model::user::Column::Name
|
||||||
|
.eq(&payload.name)
|
||||||
|
.or(model::user::Column::Email.eq(&payload.email)),
|
||||||
|
)
|
||||||
|
.one(db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if user_opt.is_some() {
|
||||||
|
return Err(AppError::Conflict(
|
||||||
|
"The username or email is already in use".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let salt = generate_salt();
|
||||||
|
let pass_hash =
|
||||||
|
argon2::hash_encoded(payload.password.as_ref(), &salt, &argon2::Config::default())
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to hash a password: {}", e);
|
||||||
|
AppError::InternalServerError(
|
||||||
|
"There was an error in the registration process".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let new_user = NewUser {
|
||||||
|
email: payload.email,
|
||||||
|
name: payload.name,
|
||||||
|
password: pass_hash,
|
||||||
|
};
|
||||||
|
|
||||||
|
new_user.create(db).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_salt() -> Vec<u8> {
|
||||||
|
let mut salt = [0u8; 16]; // 16 bytes salt length (adjust as needed)
|
||||||
|
rand::thread_rng().fill_bytes(&mut salt);
|
||||||
|
|
||||||
|
salt.into()
|
||||||
|
}
|
||||||
Loading…
Reference in new issue