nomilo/src/models/user.rs

237 lines
7.1 KiB
Rust

use uuid::Uuid;
use diesel::prelude::*;
use diesel::result::Error as DieselError;
use diesel_derive_enum::DbEnum;
use rocket::request::{FromRequest, Request, Outcome};
use rocket::outcome::try_outcome;
use serde::{Deserialize};
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use argon2::password_hash::errors::Error as PasswordHashError;
use rand::rngs::OsRng;
use argon2::{Algorithm, Argon2, Version, Params};
use crate::schema::*;
use crate::DbConn;
use crate::models::errors::{UserError, ErrorResponse, make_500};
use crate::models::zone::Zone;
use crate::models::session::Session;
#[derive(Debug, DbEnum, Deserialize, Clone)]
#[serde(rename_all = "lowercase")]
pub enum Role {
#[db_rename = "admin"]
Admin,
#[db_rename = "zoneadmin"]
ZoneAdmin,
}
// TODO: Store Uuid instead of string??
#[derive(Debug, Queryable, Identifiable, Insertable)]
#[table_name = "user"]
pub struct User {
pub id: String,
}
#[derive(Debug, Queryable, Identifiable, Insertable)]
#[table_name = "localuser"]
#[primary_key(user_id)]
pub struct LocalUser {
pub user_id: String,
pub email: String,
pub password: String,
pub role: Role,
}
#[derive(Debug, Queryable, Identifiable, Insertable)]
#[table_name = "user_zone"]
#[primary_key(user_id, zone_id)]
pub struct UserZone {
pub user_id: String,
pub zone_id: String,
}
#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
pub password: String,
pub email: String,
pub role: Option<Role>
}
#[derive(Debug, Clone)]
pub struct UserInfo {
pub id: String,
pub role: Role,
pub username: String,
}
impl UserInfo {
pub fn is_admin(&self) -> bool {
matches!(self.role, Role::Admin)
}
pub fn check_admin(&self) -> Result<(), UserError> {
if self.is_admin() {
Ok(())
} else {
Err(UserError::PermissionDenied)
}
}
pub fn get_zone(&self, conn: &diesel::SqliteConnection, zone_name: &str) -> Result<Zone, UserError> {
use crate::schema::user_zone::dsl::*;
use crate::schema::zone::dsl::*;
let (res_zone, _): (Zone, UserZone) = zone.inner_join(user_zone)
.filter(name.eq(zone_name))
.filter(user_id.eq(&self.id))
.get_result(conn)
.map_err(|e| match e {
DieselError::NotFound => UserError::ZoneNotFound,
other => UserError::DbError(other)
})?;
Ok(res_zone)
}
pub fn get_zones(&self, conn: &diesel::SqliteConnection) -> Result<Vec<Zone>, UserError> {
use crate::schema::user_zone::dsl::*;
use crate::schema::zone::dsl::*;
let res: Vec<(Zone, UserZone)> = zone.inner_join(user_zone)
.filter(user_id.eq(&self.id))
.get_results(conn)
.map_err(UserError::DbError)?;
Ok(res.into_iter().map(|(z, _)| z).collect())
}
}
#[rocket::async_trait]
impl<'r> FromRequest<'r> for UserInfo {
type Error = ErrorResponse;
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
let session = try_outcome!(request.guard::<Session>().await);
let conn = try_outcome!(request.guard::<DbConn>().await.map_failure(make_500));
conn.run(move |c| {
match LocalUser::get_user_by_uuid(c, &session.user_id) {
Err(e) => ErrorResponse::from(e).into(),
Ok(d) => Outcome::Success(d),
}
}).await
}
}
impl LocalUser {
pub fn hash_password(password: &str) -> String {
let salt = SaltString::generate(&mut OsRng);
// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
let argon2 = Argon2::new(
Algorithm::Argon2id,
Version::V0x13, // v19
Params::new(15000, 2, 1, None).expect("password param error"),
);
argon2.hash_password(password.as_bytes(), &salt).expect("password hash failed").to_string()
}
pub fn verify_password(password: &str, password_hash: &str) -> Result<bool, PasswordHashError> {
let parsed_hash = PasswordHash::new(&password_hash)?;
Ok(Argon2::default().verify_password(password.as_bytes(), &parsed_hash).is_ok())
}
pub fn create_user(conn: &diesel::SqliteConnection, user_request: CreateUserRequest) -> Result<UserInfo, UserError> {
use crate::schema::localuser::dsl::*;
use crate::schema::user::dsl::*;
let new_user_id = Uuid::new_v4().to_simple().to_string();
let new_user = User {
id: new_user_id.clone(),
};
let new_localuser = LocalUser {
user_id: new_user_id,
email: user_request.email.clone(),
password: LocalUser::hash_password(&user_request.password),
role: if let Some(user_role) = user_request.role { user_role } else { Role::ZoneAdmin },
};
let res = UserInfo {
id: new_user.id.clone(),
role: new_localuser.role.clone(),
username: new_localuser.email.clone(),
};
conn.immediate_transaction(|| -> diesel::QueryResult<()> {
diesel::insert_into(user)
.values(new_user)
.execute(conn)?;
diesel::insert_into(localuser)
.values(new_localuser)
.execute(conn)?;
Ok(())
}).map_err(|e| match e {
DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => UserError::UserConflict,
other => UserError::DbError(other)
})?;
Ok(res)
}
pub fn get_user_by_creds(
conn: &diesel::SqliteConnection,
request_email: &str,
request_password: &str
) -> Result<UserInfo, UserError> {
use crate::schema::localuser::dsl::*;
use crate::schema::user::dsl::*;
let (client_user, client_localuser): (User, LocalUser) = user.inner_join(localuser)
.filter(email.eq(request_email))
.get_result(conn)
.map_err(|e| match e {
DieselError::NotFound => UserError::BadCreds,
other => UserError::DbError(other)
})?;
if !LocalUser::verify_password(&request_password, &client_localuser.password)? {
return Err(UserError::BadCreds);
}
Ok(UserInfo {
id: client_user.id,
role: client_localuser.role,
username: client_localuser.email,
})
}
pub fn get_user_by_uuid(conn: &diesel::SqliteConnection, request_user_id: &str) -> Result<UserInfo, UserError> {
use crate::schema::localuser::dsl::*;
use crate::schema::user::dsl::*;
let (client_user, client_localuser): (User, LocalUser) = user.inner_join(localuser)
.filter(id.eq(request_user_id))
.get_result(conn)
.map_err(|e| match e {
DieselError::NotFound => UserError::NotFound,
other => UserError::DbError(other)
})?;
Ok(UserInfo {
id: client_user.id,
role: client_localuser.role,
username: client_localuser.email,
})
}
}