mirror of
https://github.com/danbulant/Portfolio
synced 2026-06-24 17:11:49 +00:00
feat: limit n of sessions in db
save client's ip address
This commit is contained in:
parent
dd7386882b
commit
137039df44
4 changed files with 38 additions and 16 deletions
|
|
@ -1,6 +1,8 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate rocket;
|
extern crate rocket;
|
||||||
|
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
use portfolio_core::error::ServiceError;
|
use portfolio_core::error::ServiceError;
|
||||||
use portfolio_core::services::candidate_service::CandidateService;
|
use portfolio_core::services::candidate_service::CandidateService;
|
||||||
use requests::LoginRequest;
|
use requests::LoginRequest;
|
||||||
|
|
@ -59,13 +61,14 @@ async fn validate(conn: Connection<'_, Db>, uuid_cookie: Result<UUIDCookie, Stat
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/login", data = "<login_form>")]
|
#[post("/login", data = "<login_form>")]
|
||||||
async fn login(conn: Connection<'_, Db>, login_form: Json<LoginRequest>) -> Result<String, Custom<String>> {
|
async fn login(conn: Connection<'_, Db>, login_form: Json<LoginRequest>, ip_addr: SocketAddr) -> Result<String, Custom<String>> {
|
||||||
let db = conn.into_inner();
|
let db = conn.into_inner();
|
||||||
println!("{} {}", login_form.application_id, login_form.password);
|
println!("{} {}", login_form.application_id, login_form.password);
|
||||||
|
|
||||||
let session_token = CandidateService::new_session(db,
|
let session_token = CandidateService::new_session(db,
|
||||||
login_form.application_id,
|
login_form.application_id,
|
||||||
login_form.password.to_string()
|
login_form.password.to_string(),
|
||||||
|
ip_addr.ip().to_string()
|
||||||
).await;
|
).await;
|
||||||
|
|
||||||
if session_token.is_ok() {
|
if session_token.is_ok() {
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,12 @@ impl Mutation {
|
||||||
db: &DbConn,
|
db: &DbConn,
|
||||||
user_id: i32,
|
user_id: i32,
|
||||||
random_uuid: Uuid,
|
random_uuid: Uuid,
|
||||||
|
ip_addr: String,
|
||||||
) -> Result<session::Model, DbErr> {
|
) -> Result<session::Model, DbErr> {
|
||||||
session::ActiveModel {
|
session::ActiveModel {
|
||||||
id: Set(random_uuid),
|
id: Set(random_uuid),
|
||||||
user_id: Set(user_id),
|
user_id: Set(user_id),
|
||||||
ip_address: Set("127.0.0.1".to_string()),
|
ip_address: Set(ip_addr),
|
||||||
created_at: Set(Utc::now().naive_local()),
|
created_at: Set(Utc::now().naive_local()),
|
||||||
expires_at: Set(Utc::now().naive_local().checked_add_signed(Duration::days(1)).unwrap()),
|
expires_at: Set(Utc::now().naive_local().checked_add_signed(Duration::days(1)).unwrap()),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,10 @@ impl Query {
|
||||||
}
|
}
|
||||||
|
|
||||||
// find session by user id
|
// find session by user id
|
||||||
pub async fn find_session_by_user_id(db: &DbConn, user_id: i32) -> Result<Option<session::Model>, DbErr> {
|
pub async fn find_sessions_by_user_id(db: &DbConn, user_id: i32) -> Result<Vec<session::Model>, DbErr> {
|
||||||
Session::find()
|
Session::find()
|
||||||
.filter(session::Column::UserId.eq(user_id))
|
.filter(session::Column::UserId.eq(user_id))
|
||||||
.one(db)
|
.all(db)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,27 @@
|
||||||
use chrono::Duration;
|
use std::cmp::min;
|
||||||
|
|
||||||
use entity::candidate;
|
use entity::candidate;
|
||||||
use sea_orm::{DatabaseConnection, prelude::Uuid, ModelTrait};
|
use sea_orm::{DatabaseConnection, prelude::Uuid, ModelTrait};
|
||||||
|
|
||||||
use crate::{crypto::{self}, Query, error::{ServiceError, USER_NOT_FOUND_ERROR, INVALID_CREDENTIALS_ERROR, DB_ERROR, USER_NOT_FOUND_BY_JWT_ID, USER_NOT_FOUND_BY_SESSION_ID}, Mutation};
|
use crate::{crypto::{self}, Query, error::{ServiceError, USER_NOT_FOUND_ERROR, INVALID_CREDENTIALS_ERROR, DB_ERROR, USER_NOT_FOUND_BY_JWT_ID, USER_NOT_FOUND_BY_SESSION_ID, EXPIRED_SESSION_ERROR}, Mutation};
|
||||||
|
|
||||||
pub struct CandidateService;
|
pub struct CandidateService;
|
||||||
|
|
||||||
impl CandidateService {
|
impl CandidateService {
|
||||||
pub async fn new_session(db: &DatabaseConnection, user_id: i32, password: String) -> Result<String, ServiceError> {
|
async fn delete_old_sessions(db: &DatabaseConnection, user_id: i32, keep_n_recent: usize) -> Result<(), ServiceError> {
|
||||||
|
let mut sessions = Query::find_sessions_by_user_id(db, user_id).await.unwrap();
|
||||||
|
|
||||||
|
sessions.sort_by_key(|s| s.created_at);
|
||||||
|
|
||||||
|
|
||||||
|
for session in sessions.iter().take(sessions.len() - min(sessions.len(), keep_n_recent)) {
|
||||||
|
Mutation::delete_session(db, session.id).await.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_session(db: &DatabaseConnection, user_id: i32, password: String, ip_addr: String) -> Result<String, ServiceError> {
|
||||||
let candidate = match Query::find_candidate_by_id(db, user_id).await {
|
let candidate = match Query::find_candidate_by_id(db, user_id).await {
|
||||||
Ok(candidate) => match candidate {
|
Ok(candidate) => match candidate {
|
||||||
Some(candidate) => candidate,
|
Some(candidate) => candidate,
|
||||||
|
|
@ -28,14 +42,18 @@ impl CandidateService {
|
||||||
|
|
||||||
// TODO delete old sessions?
|
// TODO delete old sessions?
|
||||||
|
|
||||||
|
|
||||||
// user is authenticated, generate a session
|
// user is authenticated, generate a session
|
||||||
let random_uuid: Uuid = Uuid::new_v4();
|
let random_uuid: Uuid = Uuid::new_v4();
|
||||||
|
|
||||||
let session = match Mutation::insert_session(db, user_id, random_uuid).await {
|
let session = match Mutation::insert_session(db, user_id, random_uuid, ip_addr).await {
|
||||||
Ok(session) => session,
|
Ok(session) => session,
|
||||||
Err(_) => return Err(DB_ERROR)
|
Err(_) => return Err(DB_ERROR)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// delete old sessions
|
||||||
|
CandidateService::delete_old_sessions(db, candidate.application, 3).await.ok(); // TODO move to dotenv
|
||||||
|
|
||||||
Ok(session.id.to_string())
|
Ok(session.id.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,13 +66,12 @@ impl CandidateService {
|
||||||
Err(_) => {return Err(DB_ERROR)}
|
Err(_) => {return Err(DB_ERROR)}
|
||||||
};
|
};
|
||||||
|
|
||||||
let limit = session.created_at.checked_add_signed(Duration::days(1)).unwrap();
|
|
||||||
let now = chrono::Utc::now().naive_utc();
|
let now = chrono::Utc::now().naive_utc();
|
||||||
// check if session is expired
|
// check if session is expired
|
||||||
if now > limit {
|
if now > session.expires_at {
|
||||||
// delete session
|
// delete session
|
||||||
Mutation::delete_session(db, session.id).await.unwrap();
|
Mutation::delete_session(db, session.id).await.unwrap();
|
||||||
return Err(USER_NOT_FOUND_BY_SESSION_ID)
|
return Err(EXPIRED_SESSION_ERROR)
|
||||||
}
|
}
|
||||||
|
|
||||||
let candidate = match session.find_related(candidate::Entity).one(db).await {
|
let candidate = match session.find_related(candidate::Entity).one(db).await {
|
||||||
|
|
@ -125,7 +142,8 @@ mod tests {
|
||||||
let session = CandidateService::new_session(
|
let session = CandidateService::new_session(
|
||||||
db,
|
db,
|
||||||
5555555,
|
5555555,
|
||||||
"Tajny_kod".to_string()
|
"Tajny_kod".to_string(),
|
||||||
|
"127.0.0.1".to_string(),
|
||||||
)
|
)
|
||||||
.await.ok().unwrap();
|
.await.ok().unwrap();
|
||||||
// println!("{}", session.err().unwrap().1);
|
// println!("{}", session.err().unwrap().1);
|
||||||
|
|
@ -149,7 +167,7 @@ mod tests {
|
||||||
|
|
||||||
// incorrect password
|
// incorrect password
|
||||||
assert!(
|
assert!(
|
||||||
CandidateService::new_session(db, candidate_form.application, "Spatny_kod".to_string()).await.is_err()
|
CandidateService::new_session(db, candidate_form.application, "Spatny_kod".to_string(), "127.0.0.1".to_string()).await.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue