Continued permissions and auth endpoint UNTESTED
This commit is contained in:
@@ -8,6 +8,7 @@ anyhow = "1.0.100"
|
|||||||
argon2 = "0.5.3"
|
argon2 = "0.5.3"
|
||||||
axum = "0.8.7"
|
axum = "0.8.7"
|
||||||
chrono = "0.4.42"
|
chrono = "0.4.42"
|
||||||
|
jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] }
|
||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
password-hash = "0.5.0"
|
password-hash = "0.5.0"
|
||||||
rand_core = { version = "0.6", features = ["getrandom"] }
|
rand_core = { version = "0.6", features = ["getrandom"] }
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
CREATE TABLE user_permissions (
|
CREATE TABLE users (
|
||||||
uuid UUID PRIMARY KEY,
|
uuid UUID PRIMARY KEY,
|
||||||
username VARCHAR NOT NULL UNIQUE,
|
username VARCHAR NOT NULL UNIQUE,
|
||||||
email VARCHAR UNIQUE,
|
email VARCHAR UNIQUE,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
CREATE TABLE users (
|
CREATE TABLE user_permissions(
|
||||||
uuid UUID PRIMARY KEY,
|
uuid UUID PRIMARY KEY,
|
||||||
root BOOL NOT NULL,
|
root BOOL NOT NULL,
|
||||||
manage_users BOOL NOT NULL,
|
manage_users BOOL NOT NULL,
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
|
use crate::prelude::*;
|
||||||
|
use std::i64;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
use argon2::{
|
use argon2::{
|
||||||
Argon2,
|
Argon2, PasswordHash, PasswordVerifier,
|
||||||
password_hash::{PasswordHasher, SaltString, rand_core::OsRng},
|
password_hash::{PasswordHasher, SaltString, rand_core::OsRng},
|
||||||
};
|
};
|
||||||
use tracing::debug;
|
use axum::{extract::State, http::StatusCode};
|
||||||
|
use chrono::{Duration, Utc};
|
||||||
|
use jsonwebtoken::{DecodingKey, EncodingKey, Header, TokenData, Validation, decode, encode};
|
||||||
|
|
||||||
|
use crate::domain::api::AuthClaims;
|
||||||
|
|
||||||
pub fn hash_password(password: &str) -> Result<String, password_hash::Error> {
|
pub fn hash_password(password: &str) -> Result<String, password_hash::Error> {
|
||||||
let salt = SaltString::generate(&mut OsRng);
|
let salt = SaltString::generate(&mut OsRng);
|
||||||
@@ -13,3 +21,47 @@ pub fn hash_password(password: &str) -> Result<String, password_hash::Error> {
|
|||||||
debug!("password hashed");
|
debug!("password hashed");
|
||||||
Ok(hash)
|
Ok(hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn verify_password(password: &str, password_hash: &str) -> Result<bool, password_hash::Error> {
|
||||||
|
let parsed_hash = PasswordHash::new(password_hash)?;
|
||||||
|
let argon2 = Argon2::default();
|
||||||
|
match argon2.verify_password(password.as_bytes(), &parsed_hash) {
|
||||||
|
Ok(_) => return Ok(true),
|
||||||
|
Err(password_hash::Error::Password) => return Ok(false),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gen_jwt(username: String) -> Result<String, StatusCode> {
|
||||||
|
// TODO: Update secret to .env file
|
||||||
|
let secret: String = "verysafestring".to_string();
|
||||||
|
let now = Utc::now();
|
||||||
|
let expire: chrono::TimeDelta = Duration::hours(24);
|
||||||
|
let exp = (now + expire).timestamp() as i64;
|
||||||
|
let iat = now.timestamp() as i64;
|
||||||
|
let claim = AuthClaims { iat, exp, username };
|
||||||
|
|
||||||
|
encode(
|
||||||
|
&Header::default(),
|
||||||
|
&claim,
|
||||||
|
&EncodingKey::from_secret(secret.as_ref()),
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to create JWT: {}", e);
|
||||||
|
return StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_jwt(token: String) -> Result<TokenData<AuthClaims>, StatusCode> {
|
||||||
|
let secret = "verysafestring".to_string();
|
||||||
|
let result: Result<TokenData<AuthClaims>, StatusCode> = decode(
|
||||||
|
&token,
|
||||||
|
&DecodingKey::from_secret(secret.as_ref()),
|
||||||
|
&Validation::default(),
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed to verify JWT: {}", e);
|
||||||
|
return StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
|
});
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
domain::{user::InternalUser, user_prems::UserPermissions},
|
auth::{gen_jwt, verify_password},
|
||||||
|
domain::{api::LoginData, user::InternalUser, user_prems::UserPermissions},
|
||||||
infra::db,
|
infra::db,
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::http::StatusCode;
|
use axum::{extract::State, http::StatusCode};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
@@ -16,6 +17,35 @@ use crate::{
|
|||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub async fn login(state: Arc<AppState>, login_data: LoginData) -> Result<String, StatusCode> {
|
||||||
|
let user = db::user::get_by_username(&state.db_pool, &login_data.username)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Failed fetching user during login: {}", e);
|
||||||
|
return StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
|
})?;
|
||||||
|
let user = match user {
|
||||||
|
Some(value) => value,
|
||||||
|
None => return Err(StatusCode::UNAUTHORIZED),
|
||||||
|
};
|
||||||
|
|
||||||
|
let verify = verify_password(&login_data.password, &user.password_hash).map_err(|e| {
|
||||||
|
error!("Failed to verify password hash: {}", e);
|
||||||
|
return StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !verify {
|
||||||
|
return Err(StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = gen_jwt(user.username.clone()).map_err(|e| {
|
||||||
|
error!("Failed to generate JWT: {}", e);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(token)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create(state: Arc<AppState>, new_user: NewUser) -> Result<User, StatusCode> {
|
pub async fn create(state: Arc<AppState>, new_user: NewUser) -> Result<User, StatusCode> {
|
||||||
debug!("create user started");
|
debug!("create user started");
|
||||||
|
|
||||||
@@ -29,13 +59,28 @@ pub async fn create(state: Arc<AppState>, new_user: NewUser) -> Result<User, Sta
|
|||||||
StatusCode::INTERNAL_SERVER_ERROR
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let created_user = db::user::create(&state.db_pool, internal)
|
let mut created_user = db::user::create(&state.db_pool, internal.clone())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(error = %e, "create user failed");
|
error!(error = %e, "create user failed");
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let perms = db::perms::create(
|
||||||
|
&state.db_pool,
|
||||||
|
created_user.uuid.clone(),
|
||||||
|
internal.permissions,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(error = %e, "create user permissions entry failed");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?;
|
||||||
|
|
||||||
|
created_user.attach_permissions(perms);
|
||||||
|
|
||||||
|
let created_user = created_user;
|
||||||
|
|
||||||
info!(user_uuid = %created_user.uuid, "user created");
|
info!(user_uuid = %created_user.uuid, "user created");
|
||||||
Ok(User::from(created_user))
|
Ok(User::from(created_user))
|
||||||
}
|
}
|
||||||
@@ -55,17 +100,40 @@ pub async fn get_safe_by_uuid(
|
|||||||
uuid: Uuid,
|
uuid: Uuid,
|
||||||
) -> Result<Option<User>, StatusCode> {
|
) -> Result<Option<User>, StatusCode> {
|
||||||
debug!(user_uuid = %uuid, "fetch user by uuid started");
|
debug!(user_uuid = %uuid, "fetch user by uuid started");
|
||||||
let user = db::user::get_safe_by_uuid(&state.db_pool, uuid.clone())
|
|
||||||
|
let mut user = match db::user::get_safe_by_uuid(&state.db_pool, uuid)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(error = %e, user_uuid = %uuid, "fetch user failed");
|
error!(error = %e, user_uuid = %uuid, "fetch user failed");
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})? {
|
||||||
|
Some(u) => u,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let perms = db::perms::get_by_uuid(&state.db_pool, uuid)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(error = %e, user_uuid = %uuid, "fetch permissions failed");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
})?;
|
})?;
|
||||||
Ok(user)
|
|
||||||
|
match perms {
|
||||||
|
Some(perms) => {
|
||||||
|
user.attach_permissions(perms);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
error!(user_uuid = %uuid, "permissions missing for existing user");
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_by_uuid(state: Arc<AppState>, uuid: Uuid) -> anyhow::Result<Option<InternalUser>> {
|
pub async fn get_by_uuid(state: Arc<AppState>, uuid: Uuid) -> anyhow::Result<Option<InternalUser>> {
|
||||||
debug!(user_uuid = %uuid, "fetch internal user started");
|
debug!(user_uuid = %uuid, "fetch internal user started");
|
||||||
|
|
||||||
let mut user = match db::user::get_by_uuid(&state.db_pool, uuid)
|
let mut user = match db::user::get_by_uuid(&state.db_pool, uuid)
|
||||||
.await
|
.await
|
||||||
.context("failed to fetch user by uuid")?
|
.context("failed to fetch user by uuid")?
|
||||||
@@ -74,48 +142,80 @@ pub async fn get_by_uuid(state: Arc<AppState>, uuid: Uuid) -> anyhow::Result<Opt
|
|||||||
None => return Ok(None),
|
None => return Ok(None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let perms_exist = db::perms::exists_by_uuid(&state.db_pool, user.uuid)
|
let perms = db::perms::get_by_uuid(&state.db_pool, user.uuid)
|
||||||
.await
|
.await
|
||||||
.context("failed to check if user permissions exist")?;
|
.context("failed to fetch user permissions")?;
|
||||||
|
|
||||||
if perms_exist {
|
match perms {
|
||||||
if let Some(perms) = db::perms::get_by_uuid(&state.db_pool, user.uuid)
|
Some(perms) => {
|
||||||
.await
|
|
||||||
.context("failed to fetch user permissions")?
|
|
||||||
{
|
|
||||||
user.attach_permissions(perms);
|
user.attach_permissions(perms);
|
||||||
}
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = user;
|
|
||||||
|
|
||||||
Ok(Some(user))
|
Ok(Some(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_perms(
|
pub async fn get_safe_by_username(
|
||||||
state: Arc<AppState>,
|
state: Arc<AppState>,
|
||||||
user_perms: UserPermissions,
|
username: &str,
|
||||||
) -> Result<(), StatusCode> {
|
) -> Result<Option<User>, StatusCode> {
|
||||||
debug!(user_uuid = %user_perms.uuid, "assign permissions started");
|
debug!(%username, "fetch user by username started");
|
||||||
let exists = db::user::exists_by_uuid(&state.db_pool, user_perms.uuid)
|
|
||||||
|
let mut user = match db::user::get_safe_by_username(&state.db_pool, username)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
error!(error = %e, user_uuid = %user_perms.uuid, "verify user exists failed");
|
error!(error = %e, %username, "fetch user by username failed");
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})? {
|
||||||
|
Some(u) => u,
|
||||||
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
|
let perms = db::perms::get_by_uuid(&state.db_pool, user.uuid)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!(error = %e, user_uuid = %user.uuid, "fetch permissions failed");
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !exists {
|
match perms {
|
||||||
warn!(user_uuid = %user_perms.uuid, "assign permissions skipped for missing user");
|
Some(perms) => {
|
||||||
return Err(StatusCode::BAD_REQUEST);
|
user.attach_permissions(perms);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
error!(user_uuid = %user.uuid, "permissions missing for existing user");
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db::perms::create(&state.db_pool, user_perms)
|
Ok(Some(user))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_by_username(
|
||||||
|
state: Arc<AppState>,
|
||||||
|
username: &str,
|
||||||
|
) -> anyhow::Result<Option<InternalUser>> {
|
||||||
|
debug!(%username, "fetch internal user by username started");
|
||||||
|
|
||||||
|
let mut user = match db::user::get_by_username(&state.db_pool, username)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.context("failed to fetch user by username")?
|
||||||
error!(error = %e, "create user permissions entry failed");
|
{
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
Some(u) => u,
|
||||||
})?;
|
None => return Ok(None),
|
||||||
|
};
|
||||||
|
|
||||||
info!("user permissions assigned");
|
let perms = db::perms::get_by_uuid(&state.db_pool, user.uuid)
|
||||||
Ok(())
|
.await
|
||||||
|
.context("failed to fetch user permissions")?;
|
||||||
|
|
||||||
|
match perms {
|
||||||
|
Some(perms) => {
|
||||||
|
user.attach_permissions(perms);
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(user))
|
||||||
}
|
}
|
||||||
|
|||||||
14
src/backend/src/domain/api.rs
Normal file
14
src/backend/src/domain/api.rs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
pub struct LoginData {
|
||||||
|
pub username: String,
|
||||||
|
pub password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct AuthClaims {
|
||||||
|
pub exp: i64,
|
||||||
|
pub iat: i64,
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod api;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod user_prems;
|
pub mod user_prems;
|
||||||
pub mod validation;
|
pub mod validation;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use sqlx::prelude::FromRow;
|
|||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
use crate::domain::user_prems::{MemberUserPermissions, UserPermissions};
|
use crate::domain::user_prems::UserPermissions;
|
||||||
use crate::domain::validation;
|
use crate::domain::validation;
|
||||||
|
|
||||||
use crate::auth;
|
use crate::auth;
|
||||||
@@ -25,7 +25,7 @@ pub struct NewUser {
|
|||||||
first_name: Option<String>,
|
first_name: Option<String>,
|
||||||
#[validate(length(min = 1, max = 64))]
|
#[validate(length(min = 1, max = 64))]
|
||||||
last_name: Option<String>,
|
last_name: Option<String>,
|
||||||
permissions: Option<MemberUserPermissions>,
|
permissions: UserPermissions,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
@@ -36,7 +36,7 @@ pub struct InternalNewUser {
|
|||||||
pub password_hash: String,
|
pub password_hash: String,
|
||||||
pub first_name: Option<String>,
|
pub first_name: Option<String>,
|
||||||
pub last_name: Option<String>,
|
pub last_name: Option<String>,
|
||||||
pub permissions: Option<MemberUserPermissions>,
|
pub permissions: UserPermissions,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Deserialize, FromRow)]
|
||||||
@@ -48,16 +48,18 @@ pub struct InternalUser {
|
|||||||
pub first_name: Option<String>,
|
pub first_name: Option<String>,
|
||||||
pub last_name: Option<String>,
|
pub last_name: Option<String>,
|
||||||
#[sqlx(skip)]
|
#[sqlx(skip)]
|
||||||
pub permissions: Option<MemberUserPermissions>,
|
pub permissions: UserPermissions,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
uuid: Uuid,
|
pub uuid: Uuid,
|
||||||
username: String,
|
pub username: String,
|
||||||
email: Option<String>,
|
pub email: Option<String>,
|
||||||
first_name: Option<String>,
|
pub first_name: Option<String>,
|
||||||
last_name: Option<String>,
|
pub last_name: Option<String>,
|
||||||
|
#[sqlx(skip)]
|
||||||
|
pub permissions: UserPermissions,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -91,6 +93,7 @@ impl From<InternalUser> for User {
|
|||||||
email: value.email,
|
email: value.email,
|
||||||
first_name: value.first_name,
|
first_name: value.first_name,
|
||||||
last_name: value.last_name,
|
last_name: value.last_name,
|
||||||
|
permissions: value.permissions.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,6 +108,11 @@ impl Display for UserConversionError {
|
|||||||
|
|
||||||
impl InternalUser {
|
impl InternalUser {
|
||||||
pub fn attach_permissions(&mut self, permissions: UserPermissions) {
|
pub fn attach_permissions(&mut self, permissions: UserPermissions) {
|
||||||
self.permissions = Some(MemberUserPermissions::from(permissions));
|
self.permissions = UserPermissions::from(permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl User {
|
||||||
|
pub fn attach_permissions(&mut self, permissions: UserPermissions) {
|
||||||
|
self.permissions = UserPermissions::from(permissions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::prelude::FromRow;
|
use sqlx::prelude::FromRow;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@@ -9,27 +9,19 @@ pub enum UserActions {
|
|||||||
Login,
|
Login,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, FromRow)]
|
#[derive(Debug, Clone, Deserialize, Serialize, FromRow)]
|
||||||
pub struct UserPermissions {
|
pub struct UserPermissions {
|
||||||
pub uuid: Uuid,
|
|
||||||
pub root: bool,
|
pub root: bool,
|
||||||
pub manage_users: bool,
|
pub manage_users: bool,
|
||||||
pub login: bool,
|
pub login: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, FromRow)]
|
impl Default for UserPermissions {
|
||||||
pub struct MemberUserPermissions {
|
fn default() -> Self {
|
||||||
pub root: bool,
|
|
||||||
pub manage_users: bool,
|
|
||||||
pub login: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<UserPermissions> for MemberUserPermissions {
|
|
||||||
fn from(value: UserPermissions) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
root: value.root,
|
root: false,
|
||||||
manage_users: value.manage_users,
|
manage_users: false,
|
||||||
login: value.login,
|
login: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,12 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::{domain::user_prems::UserPermissions, prelude::*};
|
use crate::{domain::user_prems::UserPermissions, prelude::*};
|
||||||
|
|
||||||
pub async fn create(pool: &PgPool, new_perms: UserPermissions) -> Result<UserPermissions> {
|
pub async fn create(
|
||||||
debug!(user_uuid = %new_perms.uuid, "insert user permissions started");
|
pool: &PgPool,
|
||||||
|
uuid: Uuid,
|
||||||
|
new_perms: UserPermissions,
|
||||||
|
) -> Result<UserPermissions> {
|
||||||
|
debug!(user_uuid = %uuid, "insert user permissions started");
|
||||||
let perms = sqlx::query_as::<_, UserPermissions>(
|
let perms = sqlx::query_as::<_, UserPermissions>(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO user_permissions (uuid, root, manage_users, login)
|
INSERT INTO user_permissions (uuid, root, manage_users, login)
|
||||||
@@ -13,14 +17,14 @@ pub async fn create(pool: &PgPool, new_perms: UserPermissions) -> Result<UserPer
|
|||||||
RETURNING uuid, root, manage_users, login
|
RETURNING uuid, root, manage_users, login
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(new_perms.uuid)
|
.bind(uuid)
|
||||||
.bind(new_perms.root)
|
.bind(new_perms.root)
|
||||||
.bind(new_perms.manage_users)
|
.bind(new_perms.manage_users)
|
||||||
.bind(new_perms.login)
|
.bind(new_perms.login)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
debug!(user_uuid = %perms.uuid, "insert user permissions completed");
|
debug!(user_uuid = %uuid, "insert user permissions completed");
|
||||||
Ok(perms)
|
Ok(perms)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,21 @@ pub async fn get_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<InternalUse
|
|||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_by_username(pool: &PgPool, username: &str) -> Result<Option<InternalUser>> {
|
||||||
|
debug!(username = %username, "fetch user by username started");
|
||||||
|
let user = sqlx::query_as::<_, InternalUser>(
|
||||||
|
r#"
|
||||||
|
SELECT uuid, username, email, password_hash, first_name, last_name FROM users WHERE username = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(username)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
debug!(username = %username, "fetch user by username completed");
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_safe_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<User>> {
|
pub async fn get_safe_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<User>> {
|
||||||
debug!(user_uuid = %uuid, "fetch safe user by uuid started");
|
debug!(user_uuid = %uuid, "fetch safe user by uuid started");
|
||||||
let user = sqlx::query_as::<_, User>(
|
let user = sqlx::query_as::<_, User>(
|
||||||
@@ -58,6 +73,21 @@ pub async fn get_safe_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<Option<User>>
|
|||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_safe_by_username(pool: &PgPool, username: &str) -> Result<Option<User>> {
|
||||||
|
debug!(username = %username, "fetch safe user by username started");
|
||||||
|
let user = sqlx::query_as::<_, User>(
|
||||||
|
r#"
|
||||||
|
SELECT uuid, username, email, first_name, last_name FROM users WHERE uuid = $1
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(username)
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
debug!(username = %username, "fetch safe user by username completed");
|
||||||
|
Ok(user)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_all(pool: &PgPool) -> Result<Vec<InternalUser>> {
|
pub async fn get_all(pool: &PgPool) -> Result<Vec<InternalUser>> {
|
||||||
debug!("fetch all internal users started");
|
debug!("fetch all internal users started");
|
||||||
let users = sqlx::query_as::<_, InternalUser>(
|
let users = sqlx::query_as::<_, InternalUser>(
|
||||||
@@ -108,3 +138,22 @@ pub async fn exists_by_uuid(pool: &PgPool, uuid: Uuid) -> Result<bool> {
|
|||||||
debug!(user_uuid = %uuid, "check user existence completed");
|
debug!(user_uuid = %uuid, "check user existence completed");
|
||||||
Ok(exists)
|
Ok(exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn exists_by_username(pool: &PgPool, username: &str) -> Result<bool> {
|
||||||
|
debug!(username = %username, "check user existence started");
|
||||||
|
let exists = sqlx::query_scalar::<_, bool>(
|
||||||
|
r#"
|
||||||
|
SELECT EXISTS(
|
||||||
|
SELECT 1
|
||||||
|
FROM users
|
||||||
|
WHERE uuid = $1
|
||||||
|
)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(username)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
debug!(username = %username, "check user existence completed");
|
||||||
|
Ok(exists)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,21 +1,63 @@
|
|||||||
|
use crate::{core::user_routines, prelude::*};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::Request,
|
extract::{Request, State},
|
||||||
http::{Method, header::AUTHORIZATION},
|
http::{self, Method, StatusCode, header::AUTHORIZATION},
|
||||||
middleware::Next,
|
middleware::{Next, from_fn_with_state},
|
||||||
response::IntoResponse,
|
response::Response,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tower_http::cors::{Any, CorsLayer};
|
use tower_http::cors::{Any, CorsLayer};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
pub async fn auth(request: Request, next: Next) -> impl IntoResponse {
|
use crate::{auth::verify_jwt, infra::db, state::AppState};
|
||||||
let method = request.method().clone();
|
|
||||||
let uri = request.uri().path().to_owned();
|
|
||||||
|
|
||||||
debug!(%method, uri, "auth middleware started");
|
pub async fn auth(
|
||||||
let response = next.run(request).await;
|
State(state): State<Arc<AppState>>,
|
||||||
let status = response.status();
|
mut req: Request,
|
||||||
debug!(%method, uri, %status, "auth middleware completed");
|
next: Next,
|
||||||
response
|
) -> Result<Response, StatusCode> {
|
||||||
|
// 1) Extract Authorization header
|
||||||
|
let auth_header = req
|
||||||
|
.headers()
|
||||||
|
.get(http::header::AUTHORIZATION)
|
||||||
|
.ok_or(StatusCode::FORBIDDEN)?; // no header at all
|
||||||
|
|
||||||
|
let auth_header = auth_header.to_str().map_err(|e| {
|
||||||
|
error!("Failed to parse Authorization header: {}", e);
|
||||||
|
StatusCode::FORBIDDEN
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// 2) Expect "Bearer <token>"
|
||||||
|
let mut parts = auth_header.split_whitespace();
|
||||||
|
let (scheme, token) = match (parts.next(), parts.next()) {
|
||||||
|
(Some(scheme), Some(token)) if scheme.eq_ignore_ascii_case("bearer") => (scheme, token),
|
||||||
|
_ => {
|
||||||
|
// either wrong scheme or missing token
|
||||||
|
return Err(StatusCode::UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 3) Verify JWT
|
||||||
|
let token_data = verify_jwt(token.to_string())?; // verify_jwt(&str) -> Result<TokenData<AuthClaims>, StatusCode>
|
||||||
|
let username = &token_data.claims.username;
|
||||||
|
|
||||||
|
// 4) Load current user from DB
|
||||||
|
let current_user = match user_routines::get_by_username(state, username)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Error when fetching user via routine: {}", e);
|
||||||
|
return StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
|
})? {
|
||||||
|
Some(user) => user,
|
||||||
|
None => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
};
|
||||||
|
// 5) Attach user to request extensions so handlers can grab it
|
||||||
|
req.extensions_mut().insert(current_user);
|
||||||
|
|
||||||
|
// 6) Continue down the stack
|
||||||
|
Ok(next.run(req).await)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cors() -> CorsLayer {
|
pub fn cors() -> CorsLayer {
|
||||||
|
|||||||
@@ -4,39 +4,54 @@ pub mod user_routes;
|
|||||||
use axum::{
|
use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
|
middleware::from_fn_with_state,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tower::ServiceBuilder;
|
use tower::{Layer, ServiceBuilder};
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
macro_rules! middleware {
|
||||||
|
(cors) => {
|
||||||
|
crate::router::middleware::cors()
|
||||||
|
};
|
||||||
|
(cors_auth, $state:expr) => {
|
||||||
|
(
|
||||||
|
crate::router::middleware::cors(),
|
||||||
|
axum::middleware::from_fn_with_state($state, crate::router::middleware::auth),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn init_router(app_state: Arc<AppState>) -> Router {
|
pub async fn init_router(app_state: Arc<AppState>) -> Router {
|
||||||
info!("router initialization started");
|
info!("router initialization started");
|
||||||
|
|
||||||
let router = Router::new()
|
let router = Router::new()
|
||||||
.route(
|
.route("/api/ping", get(ping).layer(middleware!(cors)))
|
||||||
"/api/ping",
|
|
||||||
get(ping).layer(
|
|
||||||
ServiceBuilder::new()
|
|
||||||
.layer(middleware::cors())
|
|
||||||
.layer(axum::middleware::from_fn(middleware::auth)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.route(
|
.route(
|
||||||
"/api/users",
|
"/api/users",
|
||||||
post(user_routes::create)
|
post(user_routes::create)
|
||||||
.layer(ServiceBuilder::new().layer(middleware::cors()))
|
.layer(middleware!(cors_auth, app_state.clone()))
|
||||||
.with_state(app_state.clone())
|
.with_state(app_state.clone())
|
||||||
.get(user_routes::get_all)
|
.get(user_routes::get_all)
|
||||||
.layer(ServiceBuilder::new().layer(middleware::cors()))
|
.layer(middleware!(cors_auth, app_state.clone()))
|
||||||
.with_state(app_state.clone()),
|
.with_state(app_state.clone()),
|
||||||
)
|
)
|
||||||
.route("/api/users/{uuid}", get(user_routes::get_uuid))
|
.route(
|
||||||
.layer(ServiceBuilder::new().layer(middleware::cors()))
|
"/api/users/{uuid}",
|
||||||
.with_state(app_state.clone());
|
get(user_routes::get_uuid)
|
||||||
|
.layer(middleware!(cors_auth, app_state.clone()))
|
||||||
|
.with_state(app_state.clone()),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/api/login",
|
||||||
|
post(user_routes::login)
|
||||||
|
.layer(middleware!(cors))
|
||||||
|
.with_state(app_state.clone()),
|
||||||
|
);
|
||||||
|
|
||||||
info!("router initialization completed");
|
info!("router initialization completed");
|
||||||
router
|
router
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use crate::prelude::*;
|
use crate::{domain::api::LoginData, prelude::*};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -40,3 +40,11 @@ pub async fn get_uuid(
|
|||||||
debug!("get user by uuid route completed");
|
debug!("get user by uuid route completed");
|
||||||
Ok(Json(user))
|
Ok(Json(user))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn login(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
Json(login_data): Json<LoginData>,
|
||||||
|
) -> Result<Json<String>, StatusCode> {
|
||||||
|
let result = core::user_routines::login(state, login_data).await?;
|
||||||
|
Ok(Json(result))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user