Updates to account, ui, etc
This commit is contained in:
@@ -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()),
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ const TABLE_NAME: &str = "frequencies";
|
||||
pub struct Frequency {
|
||||
#[serde(rename = "id")]
|
||||
pub frequency_id: String,
|
||||
#[serde(rename = "name")]
|
||||
pub frequency_name: Option<String>,
|
||||
pub frequency_mhz: f32,
|
||||
}
|
||||
|
||||
@@ -19,6 +21,7 @@ pub struct FrequencyRow {
|
||||
pub id: Uuid,
|
||||
pub icao: String,
|
||||
pub frequency_id: String,
|
||||
pub frequency_name: Option<String>,
|
||||
pub frequency_mhz: f32,
|
||||
}
|
||||
|
||||
@@ -28,6 +31,8 @@ pub struct UpdateFrequency {
|
||||
pub icao: Option<String>,
|
||||
#[serde(rename = "id", skip_serializing_if = "Option::is_none")]
|
||||
pub frequency_id: Option<String>,
|
||||
#[serde(rename = "name", skip_serializing_if = "Option::is_none")]
|
||||
pub frequency_name: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub frequency_mhz: Option<f32>,
|
||||
}
|
||||
@@ -36,6 +41,7 @@ impl From<FrequencyRow> for Frequency {
|
||||
fn from(frequency: FrequencyRow) -> Self {
|
||||
Self {
|
||||
frequency_id: frequency.frequency_id.clone(),
|
||||
frequency_name: frequency.frequency_name.clone(),
|
||||
frequency_mhz: frequency.frequency_mhz,
|
||||
}
|
||||
}
|
||||
@@ -47,6 +53,7 @@ impl Frequency {
|
||||
id: Uuid::new_v4(),
|
||||
icao: icao.to_string(),
|
||||
frequency_id: frequency.frequency_id.clone(),
|
||||
frequency_name: frequency.frequency_name.clone(),
|
||||
frequency_mhz: frequency.frequency_mhz.clone(),
|
||||
}
|
||||
}
|
||||
@@ -96,13 +103,14 @@ impl Frequency {
|
||||
|
||||
for chunk in frequencies.chunks(chunk_size) {
|
||||
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(&format!(
|
||||
"INSERT INTO {} (id, icao, frequency_id, frequency_mhz) ",
|
||||
"INSERT INTO {} (id, icao, frequency_id, frequency_name, frequency_mhz) ",
|
||||
TABLE_NAME
|
||||
));
|
||||
query_builder.push_values(chunk, |mut b, row| {
|
||||
b.push_bind(&row.id)
|
||||
.push_bind(&row.icao)
|
||||
.push_bind(&row.frequency_id)
|
||||
.push_bind(&row.frequency_name)
|
||||
.push_bind(&row.frequency_mhz);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ pub async fn initialize() -> ApiResult<()> {
|
||||
let password = std::env::var("POSTGRES_PASSWORD").expect("POSTGRES_PASSWORD must be set");
|
||||
let host: String = std::env::var("POSTGRES_HOST").expect("POSTGRES_HOST must be set");
|
||||
let port = std::env::var("POSTGRES_PORT").unwrap_or("5432".to_string());
|
||||
let name = std::env::var("POSTGRES_NAME").unwrap_or("aviation".to_string());
|
||||
let name = std::env::var("POSTGRES_DB").unwrap_or("aviation_db".to_string());
|
||||
|
||||
let db_url = format!(
|
||||
"postgres://{}:{}@{}:{}/{}",
|
||||
|
||||
@@ -4,6 +4,7 @@ use actix_cors::Cors;
|
||||
use actix_web::{App, HttpServer, middleware::Logger, web};
|
||||
use dotenv::from_filename;
|
||||
use reqwest::Certificate;
|
||||
use uuid::Uuid;
|
||||
use crate::account::hash;
|
||||
use crate::users::{User, ADMIN_ROLE};
|
||||
|
||||
@@ -32,7 +33,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let admin_password = env::var("ADMIN_PASSWORD");
|
||||
if admin_email.is_ok() && admin_password.is_ok() {
|
||||
let email = admin_email.unwrap();
|
||||
if User::select(&email).await.is_none() {
|
||||
if User::select_by_email(&email).await.is_none() {
|
||||
log::debug!("Creating default administrator");
|
||||
let password = admin_password.unwrap();
|
||||
let password_hash = hash(&password)?;
|
||||
@@ -42,6 +43,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
);
|
||||
}
|
||||
let admin_user = User {
|
||||
id: Uuid::new_v4(),
|
||||
email,
|
||||
email_verified: true,
|
||||
password_hash,
|
||||
|
||||
@@ -21,6 +21,7 @@ impl RegisterRequest {
|
||||
pub fn to_user(self) -> ApiResult<User> {
|
||||
let password_hash = hash(&self.password)?;
|
||||
Ok(User {
|
||||
id: Uuid::new_v4(),
|
||||
email: self.email.to_lowercase(),
|
||||
email_verified: false,
|
||||
password_hash,
|
||||
@@ -42,17 +43,19 @@ pub struct LoginRequest {
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct UserResponse {
|
||||
pub email_verified: bool,
|
||||
pub id: Uuid,
|
||||
pub role: String,
|
||||
pub first_name: String,
|
||||
pub last_name: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub avatar: Option<String>,
|
||||
pub email_verified: bool,
|
||||
}
|
||||
|
||||
impl From<User> for UserResponse {
|
||||
fn from(user: User) -> Self {
|
||||
UserResponse {
|
||||
id: user.id,
|
||||
email_verified: user.email_verified,
|
||||
role: user.role,
|
||||
first_name: user.first_name,
|
||||
@@ -74,7 +77,7 @@ pub struct UpdateUser {
|
||||
}
|
||||
|
||||
impl UpdateUser {
|
||||
pub async fn update(&self, email: &str) -> ApiResult<User> {
|
||||
pub async fn update(&self, id: &Uuid) -> ApiResult<User> {
|
||||
let pool = db::pool();
|
||||
|
||||
let mut query_builder: QueryBuilder<Postgres> =
|
||||
@@ -130,8 +133,8 @@ impl UpdateUser {
|
||||
query_builder.push("updated_at = ");
|
||||
query_builder.push_bind(Utc::now());
|
||||
|
||||
query_builder.push(" WHERE email = ");
|
||||
query_builder.push_bind(email.to_string());
|
||||
query_builder.push(" WHERE id = ");
|
||||
query_builder.push_bind(id);
|
||||
query_builder.push(" RETURNING *");
|
||||
|
||||
let query = query_builder.build_query_as::<User>();
|
||||
@@ -143,6 +146,7 @@ impl UpdateUser {
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct User {
|
||||
pub id: Uuid,
|
||||
pub email: String,
|
||||
pub email_verified: bool,
|
||||
pub password_hash: String,
|
||||
@@ -155,7 +159,26 @@ pub struct User {
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn select(email: &str) -> Option<Self> {
|
||||
pub async fn select(id: &Uuid) -> Option<Self> {
|
||||
let pool = db::pool();
|
||||
let user: Option<Self> = sqlx::query_as::<_, Self>(&format!(
|
||||
r#"
|
||||
SELECT * FROM {} WHERE id = $1
|
||||
"#,
|
||||
TABLE_NAME
|
||||
))
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!("Unable to find user by id '{}': {}", id, err);
|
||||
None
|
||||
});
|
||||
|
||||
user
|
||||
}
|
||||
|
||||
pub async fn select_by_email(email: &str) -> Option<Self> {
|
||||
let pool = db::pool();
|
||||
let user: Option<Self> = sqlx::query_as::<_, Self>(&format!(
|
||||
r#"
|
||||
@@ -167,7 +190,7 @@ impl User {
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.unwrap_or_else(|err| {
|
||||
log::error!("Unable to find user '{}': {}", email, err);
|
||||
log::error!("Unable to find user by email '{}': {}", email, err);
|
||||
None
|
||||
});
|
||||
|
||||
@@ -193,6 +216,7 @@ impl User {
|
||||
let user: User = sqlx::query_as::<_, Self>(&format!(
|
||||
r#"
|
||||
INSERT INTO {} (
|
||||
id,
|
||||
email,
|
||||
email_verified,
|
||||
password_hash,
|
||||
@@ -203,11 +227,12 @@ impl User {
|
||||
created_at,
|
||||
updated_at
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
RETURNING *
|
||||
"#,
|
||||
TABLE_NAME,
|
||||
))
|
||||
.bind(&self.id)
|
||||
.bind(&self.email)
|
||||
.bind(&self.email_verified)
|
||||
.bind(&self.password_hash)
|
||||
|
||||
Reference in New Issue
Block a user