mirror of
https://github.com/danbulant/Portfolio
synced 2026-05-24 12:35:31 +00:00
feat: auth trait
This commit is contained in:
parent
2f12211aa7
commit
4cf408ab2f
10 changed files with 95 additions and 87 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
use entity::admin::Model as Admin;
|
use entity::admin::Model as Admin;
|
||||||
use log::info;
|
use log::info;
|
||||||
|
use portfolio_core::models::auth::AuthenticableTrait;
|
||||||
use portfolio_core::sea_orm::prelude::Uuid;
|
use portfolio_core::sea_orm::prelude::Uuid;
|
||||||
use portfolio_core::services::admin_service::AdminService;
|
use portfolio_core::services::admin_service::AdminService;
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
use entity::candidate::Model as Candidate;
|
use entity::candidate::Model as Candidate;
|
||||||
|
use portfolio_core::models::auth::AuthenticableTrait;
|
||||||
use portfolio_core::sea_orm::prelude::Uuid;
|
use portfolio_core::sea_orm::prelude::Uuid;
|
||||||
use portfolio_core::services::candidate_service::CandidateService;
|
use portfolio_core::services::candidate_service::CandidateService;
|
||||||
use rocket::http::Status;
|
use rocket::http::Status;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::net::{SocketAddr, IpAddr, Ipv4Addr};
|
||||||
|
|
||||||
use portfolio_core::{
|
use portfolio_core::{
|
||||||
crypto::random_12_char_string,
|
crypto::random_12_char_string,
|
||||||
services::{admin_service::AdminService, candidate_service::CandidateService, application_service::ApplicationService, portfolio_service::PortfolioService}, models::candidate::{BaseCandidateResponse, CreateCandidateResponse, ApplicationDetails}, sea_orm::prelude::Uuid, Query, error::ServiceError, utils::csv,
|
services::{admin_service::AdminService, candidate_service::CandidateService, application_service::ApplicationService, portfolio_service::PortfolioService}, models::{candidate::{BaseCandidateResponse, CreateCandidateResponse, ApplicationDetails}, auth::AuthenticableTrait}, sea_orm::prelude::Uuid, Query, error::ServiceError, utils::csv,
|
||||||
};
|
};
|
||||||
use requests::{AdminLoginRequest, RegisterRequest};
|
use requests::{AdminLoginRequest, RegisterRequest};
|
||||||
use rocket::http::{Cookie, Status, CookieJar};
|
use rocket::http::{Cookie, Status, CookieJar};
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||||
|
|
||||||
|
use portfolio_core::models::auth::AuthenticableTrait;
|
||||||
use portfolio_core::models::candidate::ApplicationDetails;
|
use portfolio_core::models::candidate::ApplicationDetails;
|
||||||
use portfolio_core::sea_orm::prelude::Uuid;
|
use portfolio_core::sea_orm::prelude::Uuid;
|
||||||
use portfolio_core::services::application_service::ApplicationService;
|
use portfolio_core::services::application_service::ApplicationService;
|
||||||
|
|
@ -227,7 +228,6 @@ pub async fn submit_portfolio(
|
||||||
|
|
||||||
#[post("/delete")]
|
#[post("/delete")]
|
||||||
pub async fn delete_portfolio(
|
pub async fn delete_portfolio(
|
||||||
conn: Connection<'_, Db>,
|
|
||||||
session: CandidateAuth,
|
session: CandidateAuth,
|
||||||
) -> Result<(), Custom<String>> {
|
) -> Result<(), Custom<String>> {
|
||||||
let candidate: entity::candidate::Model = session.into();
|
let candidate: entity::candidate::Model = session.into();
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,9 @@ serde = { version = "^1.0", features = ["derive"] }
|
||||||
# csv
|
# csv
|
||||||
csv = "^1.1"
|
csv = "^1.1"
|
||||||
|
|
||||||
# error
|
async-trait = "0.1.60"
|
||||||
|
|
||||||
|
# error
|
||||||
thiserror = "^1.0"
|
thiserror = "^1.0"
|
||||||
|
|
||||||
# env
|
# env
|
||||||
|
|
|
||||||
14
core/src/models/auth.rs
Normal file
14
core/src/models/auth.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use sea_orm::{prelude::Uuid, DbConn};
|
||||||
|
|
||||||
|
use crate::error::ServiceError;
|
||||||
|
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait AuthenticableTrait {
|
||||||
|
type User;
|
||||||
|
// fn password_valid(user: T);
|
||||||
|
async fn login(db: &DbConn, user: i32, password: String, ip_addr: String) -> Result<(String, String), ServiceError>;
|
||||||
|
async fn auth(db: &DbConn, session_id: Uuid) -> Result<Self::User, ServiceError>;
|
||||||
|
async fn logout(db: &DbConn, session_id: Uuid) -> Result<(), ServiceError>;
|
||||||
|
}
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
pub mod candidate_details;
|
pub mod candidate_details;
|
||||||
pub mod candidate;
|
pub mod candidate;
|
||||||
|
pub mod auth;
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
use entity::admin;
|
use entity::admin;
|
||||||
use sea_orm::{prelude::Uuid, DbConn};
|
use sea_orm::{prelude::Uuid, DbConn};
|
||||||
|
|
||||||
use crate::{crypto, error::ServiceError, Query, Mutation};
|
use crate::{crypto, error::ServiceError, Query, Mutation, models::auth::AuthenticableTrait};
|
||||||
|
|
||||||
use super::session_service::{AdminUser, SessionService};
|
use super::session_service::{AdminUser, SessionService};
|
||||||
|
|
||||||
|
|
@ -19,13 +20,20 @@ impl AdminService {
|
||||||
|
|
||||||
Ok(private_key)
|
Ok(private_key)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn login(
|
#[async_trait]
|
||||||
|
impl AuthenticableTrait for AdminService {
|
||||||
|
type User = admin::Model;
|
||||||
|
|
||||||
|
async fn login(
|
||||||
db: &DbConn,
|
db: &DbConn,
|
||||||
admin_id: i32,
|
admin_id: i32,
|
||||||
password: String,
|
password: String,
|
||||||
ip_addr: String,
|
ip_addr: String,
|
||||||
) -> Result<(String, String), ServiceError> {
|
) -> Result<(String, String), ServiceError> {
|
||||||
|
let admin = Query::find_admin_by_id(db, admin_id).await?.ok_or(ServiceError::InvalidCredentials)?;
|
||||||
|
|
||||||
let session_id = SessionService::new_session(db,
|
let session_id = SessionService::new_session(db,
|
||||||
None,
|
None,
|
||||||
Some(admin_id),
|
Some(admin_id),
|
||||||
|
|
@ -34,21 +42,21 @@ impl AdminService {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let private_key = Self::decrypt_private_key(db, admin_id, password).await?;
|
let private_key = Self::decrypt_private_key(db, admin.id, password).await?;
|
||||||
Ok((session_id, private_key))
|
Ok((session_id, private_key))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn logout(db: &DbConn, session_id: Uuid) -> Result<(), ServiceError> {
|
async fn auth(db: &DbConn, session_uuid: Uuid) -> Result<admin::Model, ServiceError> {
|
||||||
Mutation::delete_session(db, session_id).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn auth(db: &DbConn, session_uuid: Uuid) -> Result<admin::Model, ServiceError> {
|
|
||||||
match SessionService::auth_user_session(db, session_uuid).await? {
|
match SessionService::auth_user_session(db, session_uuid).await? {
|
||||||
AdminUser::Admin(admin) => Ok(admin),
|
AdminUser::Admin(admin) => Ok(admin),
|
||||||
AdminUser::Candidate(_) => Err(ServiceError::Unauthorized),
|
AdminUser::Candidate(_) => Err(ServiceError::Unauthorized),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn logout(db: &DbConn, session_id: Uuid) -> Result<(), ServiceError> {
|
||||||
|
Mutation::delete_session(db, session_id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -65,7 +73,7 @@ mod admin_tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_admin_login() -> Result<(), ServiceError> {
|
async fn test_admin_login() -> Result<(), ServiceError> {
|
||||||
let db = get_memory_sqlite_connection().await;
|
let db = get_memory_sqlite_connection().await;
|
||||||
let _ = admin::ActiveModel {
|
let admin = admin::ActiveModel {
|
||||||
id: Set(1),
|
id: Set(1),
|
||||||
name: Set("Admin".to_owned()),
|
name: Set("Admin".to_owned()),
|
||||||
public_key: Set("age1u889gp407hsz309wn09kxx9anl6uns30m27lfwnctfyq9tq4qpus8tzmq5".to_owned()),
|
public_key: Set("age1u889gp407hsz309wn09kxx9anl6uns30m27lfwnctfyq9tq4qpus8tzmq5".to_owned()),
|
||||||
|
|
@ -80,7 +88,7 @@ mod admin_tests {
|
||||||
.insert(&db)
|
.insert(&db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let (session_id, _private_key) = AdminService::login(&db, 1, "test".to_owned(), "127.0.0.1".to_owned()).await?;
|
let (session_id, _private_key) = AdminService::login(&db, admin.id, "test".to_owned(), "127.0.0.1".to_owned()).await?;
|
||||||
|
|
||||||
let logged_admin = AdminService::auth(&db, session_id.parse().unwrap()).await?;
|
let logged_admin = AdminService::auth(&db, session_id.parse().unwrap()).await?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
use entity::candidate;
|
use entity::candidate;
|
||||||
use sea_orm::{prelude::Uuid, DbConn};
|
use sea_orm::{prelude::Uuid, DbConn};
|
||||||
|
|
||||||
|
|
@ -5,7 +6,7 @@ use crate::{
|
||||||
models::{candidate_details::{EncryptedApplicationDetails, EncryptedString, EncryptedCandidateDetails}, candidate::CandidateDetails},
|
models::{candidate_details::{EncryptedApplicationDetails, EncryptedString, EncryptedCandidateDetails}, candidate::CandidateDetails},
|
||||||
crypto::{self, hash_password},
|
crypto::{self, hash_password},
|
||||||
error::ServiceError,
|
error::ServiceError,
|
||||||
Mutation, Query, models::candidate::{BaseCandidateResponse, CreateCandidateResponse}, utils::db::get_recipients,
|
Mutation, Query, models::{candidate::{BaseCandidateResponse, CreateCandidateResponse}, auth::AuthenticableTrait}, utils::db::get_recipients,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{session_service::{AdminUser, SessionService}, application_service::ApplicationService, portfolio_service::PortfolioService};
|
use super::{session_service::{AdminUser, SessionService}, application_service::ApplicationService, portfolio_service::PortfolioService};
|
||||||
|
|
@ -140,11 +141,6 @@ impl CandidateService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn logout(db: &DbConn, session_id: Uuid) -> Result<(), ServiceError> {
|
|
||||||
Mutation::delete_session(db, session_id).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn delete_candidate(db: &DbConn, candidate: candidate::Model) -> Result<(), ServiceError> {
|
pub async fn delete_candidate(db: &DbConn, candidate: candidate::Model) -> Result<(), ServiceError> {
|
||||||
PortfolioService::delete_candidate_root(candidate.application).await?;
|
PortfolioService::delete_candidate_root(candidate.application).await?;
|
||||||
|
|
||||||
|
|
@ -218,34 +214,6 @@ impl CandidateService {
|
||||||
Ok(private_key)
|
Ok(private_key)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn login(
|
|
||||||
db: &DbConn,
|
|
||||||
candidate_id: i32,
|
|
||||||
password: String,
|
|
||||||
ip_addr: String,
|
|
||||||
) -> Result<(String, String), ServiceError> {
|
|
||||||
let candidate = Query::find_candidate_by_id(db, candidate_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(ServiceError::CandidateNotFound)?;
|
|
||||||
|
|
||||||
let session_id =
|
|
||||||
SessionService::new_session(db, Some(candidate_id), None, password.clone(), ip_addr)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let private_key = Self::decrypt_private_key(candidate, password).await?;
|
|
||||||
Ok((session_id, private_key))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn auth(db: &DbConn, session_uuid: Uuid) -> Result<candidate::Model, ServiceError> {
|
|
||||||
match SessionService::auth_user_session(db, session_uuid).await {
|
|
||||||
Ok(user) => match user {
|
|
||||||
AdminUser::Candidate(candidate) => Ok(candidate),
|
|
||||||
AdminUser::Admin(_) => Err(ServiceError::Unauthorized),
|
|
||||||
},
|
|
||||||
Err(e) => Err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_application_id_valid(application_id: i32) -> bool {
|
fn is_application_id_valid(application_id: i32) -> bool {
|
||||||
let s = &application_id.to_string();
|
let s = &application_id.to_string();
|
||||||
if s.len() <= 3 {
|
if s.len() <= 3 {
|
||||||
|
|
@ -257,10 +225,47 @@ impl CandidateService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AuthenticableTrait for CandidateService {
|
||||||
|
type User = candidate::Model;
|
||||||
|
|
||||||
|
async fn login(
|
||||||
|
db: &DbConn,
|
||||||
|
application_id: i32,
|
||||||
|
password: String,
|
||||||
|
ip_addr: String,
|
||||||
|
) -> Result<(String, String), ServiceError> {
|
||||||
|
let candidate = Query::find_candidate_by_id(db, application_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(ServiceError::CandidateNotFound)?;
|
||||||
|
|
||||||
|
let session_id = SessionService::new_session(db, Some(application_id), None, password.clone(), ip_addr)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let private_key = Self::decrypt_private_key(candidate, password).await?;
|
||||||
|
Ok((session_id, private_key))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn auth(db: &DbConn, session_uuid: Uuid) -> Result<candidate::Model, ServiceError> {
|
||||||
|
match SessionService::auth_user_session(db, session_uuid).await {
|
||||||
|
Ok(user) => match user {
|
||||||
|
AdminUser::Candidate(candidate) => Ok(candidate),
|
||||||
|
AdminUser::Admin(_) => Err(ServiceError::Unauthorized),
|
||||||
|
},
|
||||||
|
Err(e) => Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async fn logout(db: &DbConn, session_id: Uuid) -> Result<(), ServiceError> {
|
||||||
|
Mutation::delete_session(db, session_id).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub mod tests {
|
pub mod tests {
|
||||||
use sea_orm::{DbConn};
|
use sea_orm::{DbConn};
|
||||||
|
|
||||||
|
use crate::models::auth::AuthenticableTrait;
|
||||||
use crate::models::candidate_details::tests::assert_all_application_details;
|
use crate::models::candidate_details::tests::assert_all_application_details;
|
||||||
use crate::utils::db::get_memory_sqlite_connection;
|
use crate::utils::db::get_memory_sqlite_connection;
|
||||||
use crate::{crypto, services::candidate_service::CandidateService, Mutation};
|
use crate::{crypto, services::candidate_service::CandidateService, Mutation};
|
||||||
|
|
|
||||||
|
|
@ -44,64 +44,41 @@ impl SessionService {
|
||||||
/// Authenticate user by application id and password and generate a new session
|
/// Authenticate user by application id and password and generate a new session
|
||||||
pub async fn new_session(
|
pub async fn new_session(
|
||||||
db: &DbConn,
|
db: &DbConn,
|
||||||
user_id: Option<i32>,
|
candidate_id: Option<i32>,
|
||||||
admin_id: Option<i32>,
|
admin_id: Option<i32>,
|
||||||
password: String,
|
password: String,
|
||||||
ip_addr: String,
|
ip_addr: String,
|
||||||
) -> Result<String, ServiceError> {
|
) -> Result<String, ServiceError> {
|
||||||
if user_id.is_none() && admin_id.is_none() {
|
if candidate_id.is_none() && admin_id.is_none() {
|
||||||
return Err(ServiceError::UserNotFoundBySessionId);
|
return Err(ServiceError::UserNotFoundBySessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if admin_id.is_none() {
|
if let Some(candidate_id) = candidate_id {
|
||||||
// unwrap is safe here
|
let candidate = Query::find_candidate_by_id(db, candidate_id).await?
|
||||||
let candidate = match Query::find_candidate_by_id(db, user_id.unwrap()).await {
|
.ok_or(ServiceError::CandidateNotFound)?;
|
||||||
Ok(candidate) => match candidate {
|
|
||||||
Some(candidate) => candidate,
|
|
||||||
None => return Err(ServiceError::CandidateNotFound),
|
|
||||||
},
|
|
||||||
Err(e) => return Err(ServiceError::DbError(e)),
|
|
||||||
};
|
|
||||||
|
|
||||||
// compare passwords
|
// compare passwords
|
||||||
match crypto::verify_password(password.clone(), candidate.code.clone()).await {
|
if !crypto::verify_password(password.clone(), candidate.code.clone()).await? {
|
||||||
Ok(valid) => {
|
|
||||||
if !valid {
|
|
||||||
return Err(ServiceError::InvalidCredentials);
|
return Err(ServiceError::InvalidCredentials);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => return Err(ServiceError::InvalidCredentials),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if user_id.is_none() {
|
if let Some(admin_id) = admin_id {
|
||||||
// unwrap is safe here
|
let admin = Query::find_admin_by_id(db, admin_id).await?
|
||||||
let admin = match Query::find_admin_by_id(db, admin_id.unwrap()).await {
|
.ok_or(ServiceError::InvalidCredentials)?;
|
||||||
Ok(admin) => match admin {
|
|
||||||
Some(admin) => admin,
|
|
||||||
None => return Err(ServiceError::CandidateNotFound),
|
|
||||||
},
|
|
||||||
Err(e) => return Err(ServiceError::DbError(e)),
|
|
||||||
};
|
|
||||||
|
|
||||||
// compare passwords
|
// compare passwords
|
||||||
match crypto::verify_password(password.clone(), admin.password.clone()).await {
|
if !crypto::verify_password(password.clone(), admin.password.clone()).await? {
|
||||||
Ok(valid) => {
|
|
||||||
if !valid {
|
|
||||||
return Err(ServiceError::InvalidCredentials);
|
return Err(ServiceError::InvalidCredentials);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => return Err(ServiceError::InvalidCredentials),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// user is authenticated, generate a new session
|
// user is authenticated, generate a new session
|
||||||
|
|
||||||
let random_uuid: Uuid = Uuid::new_v4();
|
let random_uuid: Uuid = Uuid::new_v4();
|
||||||
|
|
||||||
let session = Mutation::insert_session(db, user_id, admin_id, random_uuid, ip_addr).await?;
|
let session = Mutation::insert_session(db, candidate_id, admin_id, random_uuid, ip_addr).await?;
|
||||||
|
|
||||||
// delete old sessions
|
SessionService::delete_old_sessions(db, candidate_id, admin_id, 3)
|
||||||
SessionService::delete_old_sessions(db, user_id, admin_id, 3)
|
|
||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue