2021-03-26 22:30:38 +00:00
|
|
|
use uuid::Uuid;
|
|
|
|
use diesel::prelude::*;
|
2021-03-27 05:45:59 +00:00
|
|
|
use diesel::result::Error as DieselError;
|
2021-03-26 22:30:38 +00:00
|
|
|
use diesel_derive_enum::DbEnum;
|
2021-04-02 17:33:59 +00:00
|
|
|
use rocket::{State, request::{FromRequest, Request, Outcome}};
|
2022-04-22 17:16:57 +00:00
|
|
|
use rocket::outcome::try_outcome;
|
2022-03-04 12:08:03 +00:00
|
|
|
use serde::{Deserialize};
|
2021-03-27 05:45:59 +00:00
|
|
|
// TODO: Maybe just use argon2 crate directly
|
2022-03-04 12:08:03 +00:00
|
|
|
use djangohashers::{make_password_with_algorithm, check_password, Algorithm};
|
2021-03-27 19:36:57 +00:00
|
|
|
use jsonwebtoken::{
|
|
|
|
errors::ErrorKind as JwtErrorKind
|
|
|
|
};
|
2021-03-26 22:30:38 +00:00
|
|
|
|
|
|
|
use crate::schema::*;
|
2021-03-27 05:45:59 +00:00
|
|
|
use crate::DbConn;
|
2021-03-27 19:36:57 +00:00
|
|
|
use crate::config::Config;
|
2022-03-04 12:08:03 +00:00
|
|
|
use crate::models::errors::{UserError, ErrorResponse, make_500};
|
|
|
|
use crate::models::zone::Zone;
|
|
|
|
use crate::models::auth::AuthClaims;
|
|
|
|
|
2021-03-27 19:36:57 +00:00
|
|
|
|
2021-04-05 01:05:39 +00:00
|
|
|
const BEARER: &str = "Bearer ";
|
2022-03-03 23:44:29 +00:00
|
|
|
const AUTH_HEADER: &str = "Authorization";
|
2021-03-26 22:30:38 +00:00
|
|
|
|
2021-03-27 17:23:19 +00:00
|
|
|
|
2021-04-03 06:16:54 +00:00
|
|
|
#[derive(Debug, DbEnum, Deserialize, Clone)]
|
2021-04-03 06:19:58 +00:00
|
|
|
#[serde(rename_all = "lowercase")]
|
2021-03-26 22:30:38 +00:00
|
|
|
pub enum Role {
|
2021-04-03 06:19:58 +00:00
|
|
|
#[db_rename = "admin"]
|
2021-03-26 22:30:38 +00:00
|
|
|
Admin,
|
2021-04-03 06:16:54 +00:00
|
|
|
#[db_rename = "zoneadmin"]
|
2021-03-26 22:30:38 +00:00
|
|
|
ZoneAdmin,
|
|
|
|
}
|
|
|
|
|
2021-03-27 05:45:59 +00:00
|
|
|
// TODO: Store Uuid instead of string??
|
|
|
|
#[derive(Debug, Queryable, Identifiable, Insertable)]
|
2021-03-26 22:30:38 +00:00
|
|
|
#[table_name = "user"]
|
|
|
|
pub struct User {
|
|
|
|
pub id: String,
|
|
|
|
}
|
|
|
|
|
2021-03-27 05:45:59 +00:00
|
|
|
#[derive(Debug, Queryable, Identifiable, Insertable)]
|
2021-03-26 22:30:38 +00:00
|
|
|
#[table_name = "localuser"]
|
|
|
|
#[primary_key(user_id)]
|
|
|
|
pub struct LocalUser {
|
|
|
|
pub user_id: String,
|
|
|
|
pub username: String,
|
|
|
|
pub password: String,
|
2021-04-05 22:56:15 +00:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
|
2021-03-27 05:45:59 +00:00
|
|
|
#[derive(Debug, Deserialize)]
|
|
|
|
pub struct CreateUserRequest {
|
|
|
|
pub username: String,
|
|
|
|
pub password: String,
|
|
|
|
pub email: String,
|
|
|
|
pub role: Option<Role>
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2021-03-26 22:30:38 +00:00
|
|
|
pub struct UserInfo {
|
2021-03-27 05:45:59 +00:00
|
|
|
pub id: String,
|
2021-04-03 06:16:54 +00:00
|
|
|
pub role: Role,
|
2021-03-27 05:45:59 +00:00
|
|
|
pub username: String,
|
2021-03-26 22:30:38 +00:00
|
|
|
}
|
|
|
|
|
2021-04-05 22:56:15 +00:00
|
|
|
impl UserInfo {
|
|
|
|
pub fn is_admin(&self) -> bool {
|
|
|
|
matches!(self.role, Role::Admin)
|
|
|
|
}
|
|
|
|
|
2021-04-06 02:39:56 +00:00
|
|
|
pub fn check_admin(&self) -> Result<(), UserError> {
|
|
|
|
if self.is_admin() {
|
|
|
|
Ok(())
|
|
|
|
} else {
|
|
|
|
Err(UserError::PermissionDenied)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-05 22:56:15 +00:00
|
|
|
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)
|
|
|
|
}
|
2021-04-06 02:39:56 +00:00
|
|
|
|
2021-05-02 13:56:42 +00:00
|
|
|
pub fn get_zones(&self, conn: &diesel::SqliteConnection) -> Result<Vec<Zone>, UserError> {
|
2021-04-06 02:39:56 +00:00
|
|
|
use crate::schema::user_zone::dsl::*;
|
|
|
|
use crate::schema::zone::dsl::*;
|
|
|
|
|
2021-05-02 13:56:42 +00:00
|
|
|
let res: Vec<(Zone, UserZone)> = zone.inner_join(user_zone)
|
|
|
|
.filter(user_id.eq(&self.id))
|
|
|
|
.get_results(conn)
|
|
|
|
.map_err(UserError::DbError)?;
|
2021-04-06 02:39:56 +00:00
|
|
|
|
2021-05-02 13:56:42 +00:00
|
|
|
Ok(res.into_iter().map(|(z, _)| z).collect())
|
2021-04-06 02:39:56 +00:00
|
|
|
}
|
2021-04-05 22:56:15 +00:00
|
|
|
}
|
|
|
|
|
2021-04-02 17:33:59 +00:00
|
|
|
#[rocket::async_trait]
|
|
|
|
impl<'r> FromRequest<'r> for UserInfo {
|
2021-04-02 21:12:29 +00:00
|
|
|
type Error = ErrorResponse;
|
2021-03-27 19:36:57 +00:00
|
|
|
|
2021-04-02 17:33:59 +00:00
|
|
|
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
2021-03-27 19:36:57 +00:00
|
|
|
let auth_header = match request.headers().get_one(AUTH_HEADER) {
|
|
|
|
None => return Outcome::Forward(()),
|
|
|
|
Some(auth_header) => auth_header,
|
|
|
|
};
|
|
|
|
|
|
|
|
let token = if auth_header.starts_with(BEARER) {
|
|
|
|
auth_header.trim_start_matches(BEARER)
|
|
|
|
} else {
|
2021-04-02 21:12:29 +00:00
|
|
|
return ErrorResponse::from(UserError::MalformedHeader).into()
|
2021-03-27 19:36:57 +00:00
|
|
|
};
|
|
|
|
|
2022-04-22 17:16:57 +00:00
|
|
|
let config = try_outcome!(request.guard::<&State<Config>>().await.map_failure(make_500));
|
2021-04-05 01:05:39 +00:00
|
|
|
let conn = try_outcome!(request.guard::<DbConn>().await.map_failure(make_500));
|
2021-03-26 22:30:38 +00:00
|
|
|
|
2021-03-27 19:36:57 +00:00
|
|
|
let token_data = AuthClaims::decode(
|
|
|
|
token, &config.web_app.secret
|
|
|
|
).map_err(|e| match e.into_kind() {
|
2021-04-02 21:12:29 +00:00
|
|
|
JwtErrorKind::ExpiredSignature => UserError::ExpiredToken,
|
|
|
|
_ => UserError::BadToken,
|
2021-03-27 19:36:57 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
let token_data = match token_data {
|
2021-04-02 21:12:29 +00:00
|
|
|
Err(e) => return ErrorResponse::from(e).into(),
|
2021-03-27 19:36:57 +00:00
|
|
|
Ok(data) => data
|
|
|
|
};
|
|
|
|
|
|
|
|
let user_id = token_data.sub;
|
|
|
|
|
2021-05-02 13:56:42 +00:00
|
|
|
conn.run(move |c| {
|
|
|
|
match LocalUser::get_user_by_uuid(c, &user_id) {
|
2021-04-02 21:12:29 +00:00
|
|
|
Err(UserError::NotFound) => ErrorResponse::from(UserError::NotFound).into(),
|
|
|
|
Err(e) => ErrorResponse::from(e).into(),
|
2021-04-02 17:33:59 +00:00
|
|
|
Ok(d) => Outcome::Success(d),
|
|
|
|
}
|
|
|
|
}).await
|
2021-03-26 22:30:38 +00:00
|
|
|
}
|
|
|
|
}
|
2021-03-27 05:45:59 +00:00
|
|
|
|
|
|
|
impl LocalUser {
|
2021-04-02 17:33:59 +00:00
|
|
|
pub fn create_user(conn: &diesel::SqliteConnection, user_request: CreateUserRequest) -> Result<UserInfo, UserError> {
|
2021-03-27 05:45:59 +00:00
|
|
|
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 {
|
2021-04-05 01:05:39 +00:00
|
|
|
user_id: new_user_id,
|
2021-03-27 05:45:59 +00:00
|
|
|
username: user_request.username.clone(),
|
|
|
|
password: make_password_with_algorithm(&user_request.password, Algorithm::Argon2),
|
2021-04-05 22:56:15 +00:00
|
|
|
// TODO: Use role from request
|
|
|
|
role: Role::ZoneAdmin,
|
2021-03-27 05:45:59 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let res = UserInfo {
|
|
|
|
id: new_user.id.clone(),
|
2021-04-05 22:56:15 +00:00
|
|
|
role: new_localuser.role.clone(),
|
2021-03-27 05:45:59 +00:00
|
|
|
username: new_localuser.username.clone(),
|
|
|
|
};
|
|
|
|
|
|
|
|
conn.immediate_transaction(|| -> diesel::QueryResult<()> {
|
|
|
|
diesel::insert_into(user)
|
|
|
|
.values(new_user)
|
2021-04-02 17:33:59 +00:00
|
|
|
.execute(conn)?;
|
2021-03-27 05:45:59 +00:00
|
|
|
|
|
|
|
diesel::insert_into(localuser)
|
|
|
|
.values(new_localuser)
|
2021-04-02 17:33:59 +00:00
|
|
|
.execute(conn)?;
|
2021-03-27 05:45:59 +00:00
|
|
|
|
|
|
|
Ok(())
|
2021-04-06 02:39:56 +00:00
|
|
|
}).map_err(|e| match e {
|
|
|
|
DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => UserError::UserConflict,
|
|
|
|
other => UserError::DbError(other)
|
2021-03-27 05:45:59 +00:00
|
|
|
})?;
|
|
|
|
|
|
|
|
Ok(res)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_user_by_creds(
|
2021-04-02 17:33:59 +00:00
|
|
|
conn: &diesel::SqliteConnection,
|
2021-03-27 05:45:59 +00:00
|
|
|
request_username: &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(username.eq(request_username))
|
2021-04-06 02:39:56 +00:00
|
|
|
.get_result(conn)
|
|
|
|
.map_err(|e| match e {
|
|
|
|
DieselError::NotFound => UserError::BadCreds,
|
|
|
|
other => UserError::DbError(other)
|
|
|
|
})?;
|
2021-03-27 05:45:59 +00:00
|
|
|
|
|
|
|
if !check_password(&request_password, &client_localuser.password)? {
|
2021-04-06 02:39:56 +00:00
|
|
|
return Err(UserError::BadCreds);
|
2021-03-27 05:45:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(UserInfo {
|
|
|
|
id: client_user.id,
|
2021-04-05 22:56:15 +00:00
|
|
|
role: client_localuser.role,
|
2021-03-27 05:45:59 +00:00
|
|
|
username: client_localuser.username,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-05-02 13:56:42 +00:00
|
|
|
pub fn get_user_by_uuid(conn: &diesel::SqliteConnection, request_user_id: &str) -> Result<UserInfo, UserError> {
|
2021-03-27 19:36:57 +00:00
|
|
|
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))
|
2021-04-06 02:39:56 +00:00
|
|
|
.get_result(conn)
|
|
|
|
.map_err(|e| match e {
|
|
|
|
DieselError::NotFound => UserError::NotFound,
|
|
|
|
other => UserError::DbError(other)
|
|
|
|
})?;
|
2021-03-27 19:36:57 +00:00
|
|
|
|
|
|
|
Ok(UserInfo {
|
|
|
|
id: client_user.id,
|
2021-04-05 22:56:15 +00:00
|
|
|
role: client_localuser.role,
|
2021-03-27 19:36:57 +00:00
|
|
|
username: client_localuser.username,
|
|
|
|
})
|
2021-03-27 05:45:59 +00:00
|
|
|
}
|
2022-03-04 12:08:03 +00:00
|
|
|
}
|