Updates to account, ui, etc

This commit is contained in:
2025-05-13 22:57:29 -04:00
parent a273d4134b
commit abfa6b534c
38 changed files with 781 additions and 215 deletions

View File

@@ -34,13 +34,13 @@ impl FromRequest for Auth {
return Err(Error::new(401, "API Key does not exist".to_string()).into());
}
};
match User::select(&api_key.email).await {
match User::select(&api_key.id).await {
Some(user) => Ok(Auth {
session_id: None,
api_key: Some(key_id),
user,
}),
None => Err(Error::new(404, format!("User {} not found", api_key.email)).into()),
None => Err(Error::new(404, format!("User {} not found", api_key.id)).into()),
}
};
return Box::pin(fut);
@@ -79,13 +79,13 @@ impl FromRequest for Auth {
// Verify the session
let fut = async move {
match Session::verify(&session_id, &ip_address).await {
Ok(session) => match User::select(&session.email).await {
Ok(session) => match User::select(&session.id).await {
Some(user) => Ok(Auth {
session_id: Some(session_id),
api_key: None,
user,
}),
None => Err(Error::new(404, format!("User {} not found", session.email)).into()),
None => Err(Error::new(404, format!("User {} not found", session.id)).into()),
},
Err(err) => Err(err.into()),
}

View File

@@ -50,15 +50,16 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
let email = &request.email;
let ip_address = req.peer_addr().unwrap().ip().to_string();
let query_user = match User::select(&email).await {
let query_user = match User::select_by_email(&email).await {
Some(query_user) => query_user,
None => return HttpResponse::Unauthorized().finish(),
};
if verify_hash(&request.password, &query_user.password_hash) {
// Create a session
let session = Session::default(&email, &ip_address);
let session = Session::default(&query_user.id, &ip_address);
let session_cookie = session.cookie();
let session_exp_cookie = session.expiration_cookie();
// Save the session to the database
if let Err(err) = session.store().await {
log::error!(
@@ -77,6 +78,7 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
let user_response: UserResponse = query_user.into();
HttpResponse::Ok()
.cookie(session_cookie)
.cookie(session_exp_cookie)
.json(user_response)
} else {
log::error!(
@@ -84,7 +86,10 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
email,
ip_address
);
HttpResponse::Unauthorized().finish()
HttpResponse::Unauthorized()
.cookie(Session::empty_cookie())
.cookie(Session::empty_expiration_cookie())
.finish()
}
}
@@ -121,11 +126,68 @@ async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse {
email,
ip_address
);
HttpResponse::Ok().cookie(Session::empty_cookie()).finish()
HttpResponse::Ok()
.cookie(Session::empty_cookie())
.cookie(Session::empty_expiration_cookie())
.finish()
}
#[get("/profile")]
async fn get_profile(req: HttpRequest) -> HttpResponse {
let ip_address = req.peer_addr().unwrap().ip().to_string();
// Verify a session cookie exists
match req.cookie(SESSION_COOKIE_NAME) {
// Validate the session
Some(cookie) => {
let session_id = cookie.value().to_string();
let session = match Session::get(&session_id).await {
Ok(session) => session,
Err(_) => {
log::error!(
"Invalid profile attempt [Session: {}] [IP Address: {}]",
session_id,
ip_address
);
return HttpResponse::Unauthorized()
.cookie(Session::empty_cookie())
.cookie(Session::empty_expiration_cookie())
.finish();
}
};
let id = &session.id;
let query_user = match User::select(&id).await {
Some(query_user) => query_user,
None => {
return HttpResponse::Unauthorized()
.cookie(Session::empty_cookie())
.cookie(Session::empty_expiration_cookie())
.finish();
}
};
let user_response: UserResponse = query_user.into();
let session_cookie = session.cookie();
let session_exp_cookie = session.expiration_cookie();
log::info!(
"Successful profile attempt [ID: {}] [IP Address: {}]",
id,
ip_address
);
HttpResponse::Ok()
.cookie(session_cookie)
.cookie(session_exp_cookie)
.json(user_response)
}
None => HttpResponse::Unauthorized()
.cookie(Session::empty_cookie())
.cookie(Session::empty_expiration_cookie())
.finish(),
}
}
#[get("/session")]
async fn validate_session(req: HttpRequest) -> HttpResponse {
async fn session_refresh(req: HttpRequest) -> HttpResponse {
let ip_address = req.peer_addr().unwrap().ip().to_string();
// Verify a session cookie exists
match req.cookie(SESSION_COOKIE_NAME) {
@@ -142,33 +204,27 @@ async fn validate_session(req: HttpRequest) -> HttpResponse {
);
return HttpResponse::Unauthorized()
.cookie(Session::empty_cookie())
.cookie(Session::empty_expiration_cookie())
.finish();
}
};
let email = &session.email;
let query_user = match User::select(&email).await {
Some(query_user) => query_user,
None => {
return HttpResponse::Unauthorized()
.cookie(Session::empty_cookie())
.finish();
}
};
let user_response: UserResponse = query_user.into();
let id = &session.id;
let session_cookie = session.cookie();
let session_exp_cookie = session.expiration_cookie();
log::info!(
"Successful session validate attempt [Email: {}] [IP Address: {}]",
email,
"Successful session validate attempt [ID: {}] [IP Address: {}]",
id,
ip_address
);
HttpResponse::Ok()
.cookie(session_cookie)
.json(user_response)
.cookie(session_exp_cookie)
.finish()
}
None => HttpResponse::Unauthorized()
.cookie(Session::empty_cookie())
.cookie(Session::empty_expiration_cookie())
.finish(),
}
}
@@ -180,9 +236,9 @@ async fn change_password(
auth: Auth,
) -> HttpResponse {
let ip_address = req.peer_addr().unwrap().ip().to_string();
let email = auth.user.email;
let id = auth.user.id;
if let None = User::select(&email).await {
if let None = User::select(&id).await {
return HttpResponse::Unauthorized().finish();
};
@@ -196,20 +252,20 @@ async fn change_password(
avatar: None,
};
match update_user.update(&email).await {
match update_user.update(&id).await {
Ok(user) => {
let response: UserResponse = user.into();
log::info!(
"Successful password change attempt [Email: {}] [IP Address: {}]",
&email,
"Successful password change attempt [ID: {}] [IP Address: {}]",
&id,
ip_address
);
HttpResponse::Ok().json(response)
}
Err(err) => {
log::error!(
"Invalid password change attempt [Email: {}] [IP Address: {}]: {}",
&email,
"Invalid password change attempt [ID: {}] [IP Address: {}]: {}",
&id,
ip_address,
err
);
@@ -231,6 +287,7 @@ pub fn init_routes(config: &mut web::ServiceConfig) {
.service(login)
.service(logout)
.service(change_password)
.service(validate_session),
.service(get_profile)
.service(session_refresh),
);
}

View File

@@ -3,6 +3,7 @@ use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use redis::{AsyncCommands, RedisResult};
use tokio::task;
use uuid::Uuid;
use crate::{
db::redis_async_connection,
error::{Error, ApiResult},
@@ -11,26 +12,27 @@ use super::{csprng, hash, verify_hash};
const DEFAULT_SESSION_TTL: i64 = 86400; // (In seconds) 24 hours
pub const SESSION_COOKIE_NAME: &str = "session";
pub const SESSION_EXPIRATION_COOKIE_NAME: &str = "session_expiration";
#[derive(Debug, Serialize, Deserialize)]
pub struct Session {
pub session_id: String,
pub email: String,
pub id: Uuid,
pub ip_address: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires_at: Option<DateTime<Utc>>,
}
impl Session {
pub fn default(email: &str, ip_address: &str) -> Self {
Self::new(64, email, ip_address, Some(DEFAULT_SESSION_TTL))
pub fn default(id: &Uuid, ip_address: &str) -> Self {
Self::new(64, id, ip_address, Some(DEFAULT_SESSION_TTL))
}
pub fn new(take: usize, email: &str, ip_address: &str, ttl: Option<i64>) -> Self {
pub fn new(take: usize, id: &Uuid, ip_address: &str, ttl: Option<i64>) -> Self {
let now = Utc::now();
Self {
session_id: csprng(take),
email: email.to_string(),
id: id.clone(),
ip_address: hash(&ip_address).unwrap(),
expires_at: match ttl {
Some(ttl) => Some(now + chrono::Duration::seconds(ttl)),
@@ -77,7 +79,7 @@ impl Session {
);
};
});
session = Session::default(&session.email, ip_address);
session = Session::default(&session.id, ip_address);
session.store().await?;
Ok(session)
}
@@ -118,8 +120,8 @@ impl Session {
if let Ok(environment) = std::env::var("ENVIRONMENT") {
if environment == "development" || environment == "dev" {
log::trace!(
"Development cookie [Email: {}]: {}",
self.email,
"Development cookie [ID: {}]: {}",
self.id,
self.session_id
);
cookie.set_secure(false);
@@ -130,6 +132,28 @@ impl Session {
cookie
}
pub fn expiration_cookie(&self) -> Cookie {
let expires_at = match self.expires_at {
Some(expires_at) => expires_at.timestamp(),
None => DEFAULT_SESSION_TTL,
};
let ttl = expires_at - Utc::now().timestamp();
let mut cookie = Cookie::build(SESSION_EXPIRATION_COOKIE_NAME, expires_at.to_string())
.path("/")
.max_age(Duration::seconds(ttl))
.secure(true)
.http_only(false)
.finish();
if let Ok(environment) = std::env::var("ENVIRONMENT") {
if environment == "development" || environment == "dev" {
cookie.set_secure(false);
}
}
cookie
}
pub fn empty_cookie() -> Cookie<'static> {
let mut cookie = Cookie::build(SESSION_COOKIE_NAME, "")
.path("/")
@@ -147,4 +171,21 @@ impl Session {
cookie
}
pub fn empty_expiration_cookie() -> Cookie<'static> {
let mut cookie = Cookie::build(SESSION_EXPIRATION_COOKIE_NAME, "")
.path("/")
.max_age(Duration::seconds(-1))
.secure(true)
.http_only(false)
.finish();
if let Ok(environment) = std::env::var("ENVIRONMENT") {
if environment == "development" || environment == "dev" {
cookie.set_secure(false);
}
}
cookie
}
}