Working on session validation
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use actix_web::web::Json;
|
||||
use futures_util::try_join;
|
||||
use moka::future::Cache;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Execute, Postgres, QueryBuilder};
|
||||
use crate::airports::model::airport_category::AirportCategory;
|
||||
use crate::airports::{Frequency, FrequencyRow, Runway, RunwayRow, UpdateFrequency, UpdateRunway};
|
||||
use sqlx::{Postgres, QueryBuilder};
|
||||
use crate::airports::{
|
||||
AirportCategory, Frequency, FrequencyRow, Runway, RunwayRow, UpdateFrequency, UpdateRunway,
|
||||
};
|
||||
use crate::db;
|
||||
use crate::error::{ApiResult, Error};
|
||||
use crate::metars::Metar;
|
||||
@@ -516,7 +515,7 @@ impl Airport {
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub async fn update(icao: &str, airport: &UpdateAirport) -> ApiResult<()> {
|
||||
pub async fn update(_icao: &str, _airport: &UpdateAirport) -> ApiResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
use std::str::FromStr;
|
||||
use futures_util::stream::StreamExt as _;
|
||||
|
||||
use crate::{
|
||||
airports::{Airport, AirportCategory},
|
||||
airports::Airport,
|
||||
db::Paged,
|
||||
auth::{Auth, verify_role},
|
||||
};
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::airports::{AirportQuery, UpdateAirport};
|
||||
use crate::users::ADMIN_ROLE;
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ use argon2::{
|
||||
use rand::distr::Alphanumeric;
|
||||
use rand::prelude::*;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod model;
|
||||
mod routes;
|
||||
|
||||
@@ -27,9 +27,12 @@ impl FromRequest for Auth {
|
||||
Some(key_id) => {
|
||||
let fut = async move {
|
||||
// Check if the Session API key exists
|
||||
let api_key = match Session::get(&key_id).await? {
|
||||
Some(session) => session,
|
||||
None => return Err(Error::new(401, "API Key does not exist".to_string()).into()),
|
||||
let api_key = match Session::get(&key_id).await {
|
||||
Ok(session) => session,
|
||||
Err(err) => {
|
||||
log::error!("Invalid session auth attempt: {}", err);
|
||||
return Err(Error::new(401, "API Key does not exist".to_string()).into());
|
||||
}
|
||||
};
|
||||
match User::select(&api_key.email).await {
|
||||
Some(user) => Ok(Auth {
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
use std::sync::OnceLock;
|
||||
use actix_web::{
|
||||
post, web, HttpResponse, ResponseError,
|
||||
cookie::{Cookie, time::Duration},
|
||||
HttpRequest, put,
|
||||
};
|
||||
use actix_web::{post, web, HttpResponse, ResponseError, HttpRequest, put, get};
|
||||
use crate::{
|
||||
auth::{verify_hash, Session, SESSION_COOKIE_NAME},
|
||||
error::Error,
|
||||
users::{LoginRequest, RegisterRequest, User, UserResponse},
|
||||
};
|
||||
|
||||
use crate::auth::{hash, Auth, DEFAULT_SESSION_TTL};
|
||||
use crate::error::ApiResult;
|
||||
use crate::auth::Auth;
|
||||
use crate::users::UpdateUser;
|
||||
|
||||
#[post("/register")]
|
||||
@@ -19,20 +13,20 @@ async fn register(user: web::Json<RegisterRequest>, req: HttpRequest) -> HttpRes
|
||||
let register_user = user.into_inner();
|
||||
let email = register_user.email.clone();
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
let mut insert_user: User = match register_user.to_user() {
|
||||
let insert_user: User = match register_user.to_user() {
|
||||
Ok(user) => user,
|
||||
Err(err) => return ResponseError::error_response(&err),
|
||||
};
|
||||
|
||||
match insert_user.insert().await {
|
||||
Ok(user) => {
|
||||
let response: UserResponse = user.into();
|
||||
let user_response: UserResponse = user.into();
|
||||
log::info!(
|
||||
"Successful user registration [Email: {}] [IP Address: {}]",
|
||||
email,
|
||||
ip_address
|
||||
);
|
||||
HttpResponse::Created().json(response)
|
||||
HttpResponse::Created().json(user_response)
|
||||
}
|
||||
Err(err) => {
|
||||
// Obfuscate the service error message to prevent leaking database details
|
||||
@@ -63,8 +57,8 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
|
||||
|
||||
if verify_hash(&request.password, &query_user.password_hash) {
|
||||
// Create a session
|
||||
let session = Session::new(64, &email, &ip_address, Some(DEFAULT_SESSION_TTL));
|
||||
let session_cookie = session.to_cookie();
|
||||
let session = Session::default(&email, &ip_address);
|
||||
let session_cookie = session.cookie();
|
||||
// Save the session to the database
|
||||
if let Err(err) = session.store().await {
|
||||
log::error!(
|
||||
@@ -80,7 +74,10 @@ async fn login(request: web::Json<LoginRequest>, req: HttpRequest) -> HttpRespon
|
||||
email,
|
||||
ip_address
|
||||
);
|
||||
HttpResponse::Ok().cookie(session_cookie).finish()
|
||||
let user_response: UserResponse = query_user.into();
|
||||
HttpResponse::Ok()
|
||||
.cookie(session_cookie)
|
||||
.json(user_response)
|
||||
} else {
|
||||
log::error!(
|
||||
"Invalid login attempt [Email: {}] [IP Address: {}]",
|
||||
@@ -119,19 +116,59 @@ async fn logout(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
}
|
||||
}
|
||||
|
||||
let session_cookie = Cookie::build(SESSION_COOKIE_NAME, "")
|
||||
.path("/")
|
||||
.max_age(Duration::seconds(-1))
|
||||
.secure(true)
|
||||
.http_only(true)
|
||||
.finish();
|
||||
|
||||
log::info!(
|
||||
"Successful logout attempt [Email: {}] [IP Address: {}]",
|
||||
email,
|
||||
ip_address
|
||||
);
|
||||
HttpResponse::Ok().cookie(session_cookie).finish()
|
||||
HttpResponse::Ok().cookie(Session::empty_cookie()).finish()
|
||||
}
|
||||
|
||||
#[get("/session")]
|
||||
async fn validate_session(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::replace(&session_id, &ip_address).await {
|
||||
Ok(session) => session,
|
||||
Err(err) => {
|
||||
log::error!(
|
||||
"Invalid session validate attempt [Session: {}] [IP Address: {}]",
|
||||
session_id,
|
||||
ip_address
|
||||
);
|
||||
return ResponseError::error_response(&Error::new(500, err.to_string()));
|
||||
}
|
||||
};
|
||||
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 session_cookie = session.cookie();
|
||||
|
||||
log::info!(
|
||||
"Successful session validate attempt [Email: {}] [IP Address: {}]",
|
||||
email,
|
||||
ip_address
|
||||
);
|
||||
HttpResponse::Ok()
|
||||
.cookie(session_cookie)
|
||||
.json(user_response)
|
||||
}
|
||||
None => HttpResponse::Unauthorized()
|
||||
.cookie(Session::empty_cookie())
|
||||
.finish(),
|
||||
}
|
||||
}
|
||||
|
||||
#[put("/password")]
|
||||
@@ -178,8 +215,8 @@ async fn change_password(
|
||||
}
|
||||
|
||||
#[post("/password-reset")]
|
||||
async fn password_reset(req: HttpRequest, auth: Auth) -> HttpResponse {
|
||||
let ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
async fn password_reset(req: HttpRequest, _auth: Auth) -> HttpResponse {
|
||||
let _ip_address = req.peer_addr().unwrap().ip().to_string();
|
||||
HttpResponse::Ok().finish()
|
||||
}
|
||||
|
||||
@@ -189,6 +226,7 @@ pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||
.service(register)
|
||||
.service(login)
|
||||
.service(logout)
|
||||
.service(change_password),
|
||||
.service(change_password)
|
||||
.service(validate_session),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,15 +2,14 @@ use actix_web::cookie::{time::Duration, Cookie};
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use redis::{AsyncCommands, RedisResult};
|
||||
|
||||
use tokio::task;
|
||||
use crate::{
|
||||
db::redis_async_connection,
|
||||
error::{Error, ApiResult},
|
||||
};
|
||||
|
||||
use super::{csprng, hash, verify_hash};
|
||||
|
||||
pub const DEFAULT_SESSION_TTL: i64 = 86400; // (In seconds) 24 hours
|
||||
const DEFAULT_SESSION_TTL: i64 = 86400; // (In seconds) 24 hours
|
||||
pub const SESSION_COOKIE_NAME: &str = "session";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@@ -23,6 +22,10 @@ pub struct Session {
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn default(email: &str, ip_address: &str) -> Self {
|
||||
Self::new(64, email, ip_address, Some(DEFAULT_SESSION_TTL))
|
||||
}
|
||||
|
||||
pub fn new(take: usize, email: &str, ip_address: &str, ttl: Option<i64>) -> Self {
|
||||
let now = Utc::now();
|
||||
Self {
|
||||
@@ -53,16 +56,32 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get(session_id: &str) -> ApiResult<Option<Self>> {
|
||||
pub async fn get(session_id: &str) -> ApiResult<Self> {
|
||||
let mut conn = redis_async_connection().await?;
|
||||
let result: RedisResult<Option<String>> = conn.get(session_id).await;
|
||||
match result {
|
||||
Ok(Some(value)) => Ok(Some(serde_json::from_str(&value)?)),
|
||||
Ok(None) => Ok(None),
|
||||
Ok(Some(value)) => Ok(serde_json::from_str(&value)?),
|
||||
Ok(None) => Err(Error::new(401, format!("Missing session {}", session_id))),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn replace(session_id: &str, ip_address: &str) -> ApiResult<Self> {
|
||||
let mut session = Self::verify(session_id, ip_address).await?;
|
||||
let session_id_owned = session_id.to_owned();
|
||||
task::spawn(async move {
|
||||
if let Err(err) = Self::delete(&session_id_owned).await {
|
||||
log::error!(
|
||||
"Error deleting old session in replace session call: {}",
|
||||
err
|
||||
);
|
||||
};
|
||||
});
|
||||
session = Session::default(&session.email, ip_address);
|
||||
session.store().await?;
|
||||
Ok(session)
|
||||
}
|
||||
|
||||
pub async fn delete(session_id: &str) -> ApiResult<()> {
|
||||
let mut conn = redis_async_connection().await?;
|
||||
let result: RedisResult<()> = conn.del(session_id).await;
|
||||
@@ -73,11 +92,7 @@ impl Session {
|
||||
}
|
||||
|
||||
pub async fn verify(session_id: &str, ip_address: &str) -> ApiResult<Self> {
|
||||
// Check if the session exists
|
||||
let session = match Self::get(session_id).await? {
|
||||
Some(session) => session,
|
||||
None => return Err(Error::new(401, "Session does not exist".to_string())),
|
||||
};
|
||||
let session = Self::get(session_id).await?;
|
||||
|
||||
// Check if the IP Address matches the Session's IP Address
|
||||
if verify_hash(ip_address, &session.ip_address) {
|
||||
@@ -87,7 +102,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_cookie(&self) -> Cookie {
|
||||
pub fn cookie(&self) -> Cookie {
|
||||
let expires_at = match self.expires_at {
|
||||
Some(expires_at) => expires_at.timestamp(),
|
||||
None => DEFAULT_SESSION_TTL,
|
||||
@@ -96,14 +111,13 @@ impl Session {
|
||||
let mut cookie = Cookie::build(SESSION_COOKIE_NAME, self.session_id.clone())
|
||||
.path("/")
|
||||
.max_age(Duration::seconds(ttl))
|
||||
// TODO: enable secure and http_only
|
||||
.secure(true)
|
||||
.http_only(true)
|
||||
.finish();
|
||||
|
||||
if let Ok(environment) = std::env::var("ENVIRONMENT") {
|
||||
if environment == "development" || environment == "dev" {
|
||||
log::debug!(
|
||||
log::trace!(
|
||||
"Development cookie [Email: {}]: {}",
|
||||
self.email,
|
||||
self.session_id
|
||||
@@ -115,4 +129,22 @@ impl Session {
|
||||
|
||||
cookie
|
||||
}
|
||||
|
||||
pub fn empty_cookie() -> Cookie<'static> {
|
||||
let mut cookie = Cookie::build(SESSION_COOKIE_NAME, "")
|
||||
.path("/")
|
||||
.max_age(Duration::seconds(-1))
|
||||
.secure(true)
|
||||
.http_only(true)
|
||||
.finish();
|
||||
|
||||
if let Ok(environment) = std::env::var("ENVIRONMENT") {
|
||||
if environment == "development" || environment == "dev" {
|
||||
cookie.set_secure(false);
|
||||
cookie.set_http_only(false);
|
||||
}
|
||||
}
|
||||
|
||||
cookie
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,16 @@ pub async fn initialize() -> ApiResult<()> {
|
||||
|
||||
let db_url = format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
db_user, db_password, db_host, db_port, db_name
|
||||
&db_user, &db_password, &db_host, &db_port, &db_name
|
||||
);
|
||||
|
||||
log::info!("Connecting to database at {}...", &db_url);
|
||||
log::info!(
|
||||
"Connecting to database at postgres://{}:*****@{}:{}/{}...",
|
||||
&db_user,
|
||||
&db_host,
|
||||
&db_port,
|
||||
&db_name
|
||||
);
|
||||
// Setup Postgres pool connection
|
||||
let pool = PgPoolOptions::new()
|
||||
.max_connections(5)
|
||||
@@ -35,7 +41,7 @@ pub async fn initialize() -> ApiResult<()> {
|
||||
.await?;
|
||||
match POOL.set(pool) {
|
||||
Ok(_) => log::info!("Database connection established"),
|
||||
Err(_) => log::warn!("Database pool already initialized")
|
||||
Err(_) => log::warn!("Database pool already initialized"),
|
||||
}
|
||||
|
||||
// Setup Redis connection
|
||||
@@ -47,7 +53,7 @@ pub async fn initialize() -> ApiResult<()> {
|
||||
};
|
||||
match REDIS.set(redis) {
|
||||
Ok(_) => log::info!("Redis connection established"),
|
||||
Err(_) => log::warn!("Redis client already initialized")
|
||||
Err(_) => log::warn!("Redis client already initialized"),
|
||||
}
|
||||
|
||||
let schema = std::env::var("MINIO_SCHEMA").unwrap_or("http".to_string());
|
||||
@@ -76,7 +82,7 @@ pub async fn initialize() -> ApiResult<()> {
|
||||
|
||||
match BUCKET.set(*bucket) {
|
||||
Ok(_) => log::info!("Bucket initialized"),
|
||||
Err(_) => log::warn!("Bucket client already initialized")
|
||||
Err(_) => log::warn!("Bucket client already initialized"),
|
||||
}
|
||||
|
||||
// Run migrations
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::env;
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{App, HttpServer, middleware::Logger, web};
|
||||
use dotenv::from_filename;
|
||||
use moka::future::Cache;
|
||||
use crate::auth::hash;
|
||||
use crate::users::{User, ADMIN_ROLE};
|
||||
|
||||
@@ -63,14 +62,13 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.allow_any_header()
|
||||
.supports_credentials()
|
||||
.max_age(3600);
|
||||
App::new()
|
||||
.wrap(cors)
|
||||
.wrap(Logger::default())
|
||||
.service(web::scope("api")
|
||||
App::new().wrap(cors).wrap(Logger::default()).service(
|
||||
web::scope("api")
|
||||
.configure(airports::init_routes)
|
||||
.configure(metars::init_routes)
|
||||
.configure(auth::init_routes)
|
||||
.configure(users::init_routes))
|
||||
.configure(users::init_routes),
|
||||
)
|
||||
})
|
||||
.bind(format!("{}:{}", host, port))
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@ use crate::error::Error;
|
||||
use crate::{error::ApiResult, db};
|
||||
use chrono::{DateTime, Datelike, Utc};
|
||||
use std::collections::HashSet;
|
||||
use moka::future::Cache;
|
||||
use redis::{AsyncCommands, RedisResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::db::redis_async_connection;
|
||||
@@ -294,7 +293,7 @@ impl Metar {
|
||||
Ok(day) => day,
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
let mut observation_time_hour = match observation_time[2..4].parse::<u32>() {
|
||||
let observation_time_hour = match observation_time[2..4].parse::<u32>() {
|
||||
Ok(hour) => hour,
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::error::Error;
|
||||
use crate::metars::Metar;
|
||||
use actix_web::{get, web, HttpResponse, HttpRequest};
|
||||
use log::error;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use tokio::time::{sleep, Duration};
|
||||
// use tokio::time::{sleep, Duration};
|
||||
|
||||
// use crate::airports::{AirportDb, AirportFilter};
|
||||
use crate::metars::Metar;
|
||||
// use crate::metars::Metar;
|
||||
|
||||
pub fn update_airports() {
|
||||
// tokio::spawn(async {
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
// }
|
||||
// }
|
||||
|
||||
pub fn init_routes(config: &mut actix_web::web::ServiceConfig) {
|
||||
pub fn init_routes(_config: &mut actix_web::web::ServiceConfig) {
|
||||
// config.service(
|
||||
// web::scope("users")
|
||||
// .service(get_favorites)
|
||||
|
||||
Reference in New Issue
Block a user