diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index cc6cc57..50aff9d 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -1,13 +1,16 @@ use std::time::{SystemTime, UNIX_EPOCH}; -use actix_web::{error, post, web, Error, HttpResponse}; +use actix_web::{Error, HttpResponse, error, post, web}; use argon2::{PasswordHash, PasswordVerifier}; +use futures::StreamExt; use log::error; use regex::Regex; use serde::{Deserialize, Serialize}; -use futures::StreamExt; -use crate::{crypto::{generate_access_token, generate_refresh_token}, Data}; +use crate::{ + Data, + crypto::{generate_access_token, generate_refresh_token}, +}; #[derive(Deserialize)] struct LoginInformation { @@ -25,7 +28,10 @@ pub struct Response { const MAX_SIZE: usize = 262_144; #[post("/login")] -pub async fn response(mut payload: web::Payload, data: web::Data) -> Result { +pub async fn response( + mut payload: web::Payload, + data: web::Data, +) -> Result { let mut body = web::BytesMut::new(); while let Some(chunk) = payload.next().await { let chunk = chunk?; @@ -51,45 +57,82 @@ pub async fn response(mut payload: web::Payload, data: web::Data) -> Resul } if email_regex.is_match(&login_information.username) { - if let Ok(row) = sqlx::query_as("SELECT CAST(uuid as VARCHAR), password FROM users WHERE email = $1").bind(login_information.username).fetch_one(&data.pool).await { + if let Ok(row) = + sqlx::query_as("SELECT CAST(uuid as VARCHAR), password FROM users WHERE email = $1") + .bind(login_information.username) + .fetch_one(&data.pool) + .await + { let (uuid, password): (String, String) = row; - return Ok(login(data.clone(), uuid, login_information.password, password, login_information.device_name).await) + return Ok(login( + data.clone(), + uuid, + login_information.password, + password, + login_information.device_name, + ) + .await); } - return Ok(HttpResponse::Unauthorized().finish()) + return Ok(HttpResponse::Unauthorized().finish()); } else if username_regex.is_match(&login_information.username) { - if let Ok(row) = sqlx::query_as("SELECT CAST(uuid as VARCHAR), password FROM users WHERE username = $1").bind(login_information.username).fetch_one(&data.pool).await { + if let Ok(row) = + sqlx::query_as("SELECT CAST(uuid as VARCHAR), password FROM users WHERE username = $1") + .bind(login_information.username) + .fetch_one(&data.pool) + .await + { let (uuid, password): (String, String) = row; - return Ok(login(data.clone(), uuid, login_information.password, password, login_information.device_name).await) + return Ok(login( + data.clone(), + uuid, + login_information.password, + password, + login_information.device_name, + ) + .await); } - return Ok(HttpResponse::Unauthorized().finish()) + return Ok(HttpResponse::Unauthorized().finish()); } Ok(HttpResponse::Unauthorized().finish()) } -async fn login(data: actix_web::web::Data, uuid: String, request_password: String, database_password: String, device_name: String) -> HttpResponse { +async fn login( + data: actix_web::web::Data, + uuid: String, + request_password: String, + database_password: String, + device_name: String, +) -> HttpResponse { if let Ok(parsed_hash) = PasswordHash::new(&database_password) { - if data.argon2.verify_password(request_password.as_bytes(), &parsed_hash).is_ok() { + if data + .argon2 + .verify_password(request_password.as_bytes(), &parsed_hash) + .is_ok() + { let refresh_token = generate_refresh_token(); let access_token = generate_access_token(); if refresh_token.is_err() { error!("{}", refresh_token.unwrap_err()); - return HttpResponse::InternalServerError().finish() + return HttpResponse::InternalServerError().finish(); } let refresh_token = refresh_token.unwrap(); if access_token.is_err() { error!("{}", access_token.unwrap_err()); - return HttpResponse::InternalServerError().finish() + return HttpResponse::InternalServerError().finish(); } let access_token = access_token.unwrap(); - let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; + let current_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; if let Err(error) = sqlx::query(&format!("INSERT INTO refresh_tokens (token, uuid, created, device_name) VALUES ($1, '{}', $2, $3 )", uuid)) .bind(&refresh_token) @@ -114,10 +157,10 @@ async fn login(data: actix_web::web::Data, uuid: String, request_password: return HttpResponse::Ok().json(Response { access_token, refresh_token, - }) + }); } - return HttpResponse::Unauthorized().finish() + return HttpResponse::Unauthorized().finish(); } HttpResponse::InternalServerError().finish() diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index 39408b0..dcfeb6b 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -1,13 +1,16 @@ -use std::{str::FromStr, time::{SystemTime, UNIX_EPOCH}}; +use std::{ + str::FromStr, + time::{SystemTime, UNIX_EPOCH}, +}; -use actix_web::{web, HttpResponse, Scope}; +use actix_web::{HttpResponse, Scope, web}; use log::error; use sqlx::Postgres; use uuid::Uuid; -mod register; mod login; mod refresh; +mod register; mod revoke; pub fn web() -> Scope { @@ -18,24 +21,33 @@ pub fn web() -> Scope { .service(revoke::res) } -pub async fn check_access_token<'a>(access_token: String, pool: &'a sqlx::Pool) -> Result { - match sqlx::query_as("SELECT CAST(uuid as VARCHAR), created FROM access_tokens WHERE token = $1") - .bind(&access_token) - .fetch_one(&*pool) - .await { +pub async fn check_access_token( + access_token: String, + pool: &sqlx::Pool, +) -> Result { + match sqlx::query_as( + "SELECT CAST(uuid as VARCHAR), created FROM access_tokens WHERE token = $1", + ) + .bind(&access_token) + .fetch_one(pool) + .await + { Ok(row) => { let (uuid, created): (String, i64) = row; - let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; - + let current_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; + let lifetime = current_time - created; - + if lifetime > 3600 { - return Err(HttpResponse::Unauthorized().finish()) + return Err(HttpResponse::Unauthorized().finish()); } - + Ok(Uuid::from_str(&uuid).unwrap()) - }, + } Err(error) => { error!("{}", error); Err(HttpResponse::InternalServerError().finish()) diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index 1e09a8e..5ac2402 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -1,10 +1,13 @@ -use std::time::{SystemTime, UNIX_EPOCH}; -use actix_web::{error, post, web, Error, HttpResponse}; +use actix_web::{Error, HttpResponse, error, post, web}; +use futures::StreamExt; use log::error; use serde::{Deserialize, Serialize}; -use futures::StreamExt; +use std::time::{SystemTime, UNIX_EPOCH}; -use crate::{crypto::{generate_access_token, generate_refresh_token}, Data}; +use crate::{ + Data, + crypto::{generate_access_token, generate_refresh_token}, +}; #[derive(Deserialize)] struct RefreshRequest { @@ -33,32 +36,45 @@ pub async fn res(mut payload: web::Payload, data: web::Data) -> Result(&body)?; - let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; + let current_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; - if let Ok(row) = sqlx::query_as("SELECT CAST(uuid as VARCHAR), created FROM refresh_tokens WHERE token = $1").bind(&refresh_request.refresh_token).fetch_one(&data.pool).await { + if let Ok(row) = + sqlx::query_as("SELECT CAST(uuid as VARCHAR), created FROM refresh_tokens WHERE token = $1") + .bind(&refresh_request.refresh_token) + .fetch_one(&data.pool) + .await + { let (uuid, created): (String, i64) = row; if let Err(error) = sqlx::query("DELETE FROM access_tokens WHERE refresh_token = $1") .bind(&refresh_request.refresh_token) .execute(&data.pool) - .await { + .await + { error!("{}", error); } - + let lifetime = current_time - created; - + if lifetime > 2592000 { if let Err(error) = sqlx::query("DELETE FROM refresh_tokens WHERE token = $1") .bind(&refresh_request.refresh_token) .execute(&data.pool) - .await { + .await + { error!("{}", error); } - - return Ok(HttpResponse::Unauthorized().finish()) + + return Ok(HttpResponse::Unauthorized().finish()); } - let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; + let current_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; let mut refresh_token = refresh_request.refresh_token; @@ -67,23 +83,24 @@ pub async fn res(mut payload: web::Payload, data: web::Data) -> Result { refresh_token = new_refresh_token; - }, + } Err(error) => { error!("{}", error); - }, + } } } @@ -91,7 +108,7 @@ pub async fn res(mut payload: web::Payload, data: web::Data) -> Result) -> Result) -> Result 32 { - return Ok(HttpResponse::Forbidden().json( - ResponseError { - gorb_id_valid: false, - ..Default::default() - } - )) + if !username_regex.is_match(&account_information.identifier) + || account_information.identifier.len() < 3 + || account_information.identifier.len() > 32 + { + return Ok(HttpResponse::Forbidden().json(ResponseError { + gorb_id_valid: false, + ..Default::default() + })); } // Password is expected to be hashed using SHA3-384 let password_regex = Regex::new(r"[0-9a-f]{96}").unwrap(); if !password_regex.is_match(&account_information.password) { - return Ok(HttpResponse::Forbidden().json( - ResponseError { - password_hashed: false, - ..Default::default() - } - )) + return Ok(HttpResponse::Forbidden().json(ResponseError { + password_hashed: false, + ..Default::default() + })); } let salt = SaltString::generate(&mut OsRng); - if let Ok(hashed_password) = data.argon2.hash_password(account_information.password.as_bytes(), &salt) { + if let Ok(hashed_password) = data + .argon2 + .hash_password(account_information.password.as_bytes(), &salt) + { // TODO: Check security of this implementation - return Ok(match sqlx::query(&format!("INSERT INTO users (uuid, username, password, email) VALUES ( '{}', $1, $2, $3 )", uuid)) + return Ok( + match sqlx::query(&format!( + "INSERT INTO users (uuid, username, password, email) VALUES ( '{}', $1, $2, $3 )", + uuid + )) .bind(account_information.identifier) // FIXME: Password has no security currently, either from a client or server perspective .bind(hashed_password.to_string()) .bind(account_information.email) .execute(&data.pool) - .await { + .await + { Ok(_out) => { let refresh_token = generate_refresh_token(); let access_token = generate_access_token(); if refresh_token.is_err() { error!("{}", refresh_token.unwrap_err()); - return Ok(HttpResponse::InternalServerError().finish()) + return Ok(HttpResponse::InternalServerError().finish()); } let refresh_token = refresh_token.unwrap(); if access_token.is_err() { error!("{}", access_token.unwrap_err()); - return Ok(HttpResponse::InternalServerError().finish()) + return Ok(HttpResponse::InternalServerError().finish()); } let access_token = access_token.unwrap(); - let current_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64; + let current_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i64; if let Err(error) = sqlx::query(&format!("INSERT INTO refresh_tokens (token, uuid, created, device_name) VALUES ($1, '{}', $2, $3 )", uuid)) .bind(&refresh_token) @@ -153,32 +167,37 @@ pub async fn res(mut payload: web::Payload, data: web::Data) -> Result { let err_msg = error.as_database_error().unwrap().message(); - + match err_msg { - err_msg if err_msg.contains("unique") && err_msg.contains("username_key") => HttpResponse::Forbidden().json(ResponseError { - gorb_id_available: false, - ..Default::default() - }), - err_msg if err_msg.contains("unique") && err_msg.contains("email_key") => HttpResponse::Forbidden().json(ResponseError { - email_available: false, - ..Default::default() - }), + err_msg + if err_msg.contains("unique") && err_msg.contains("username_key") => + { + HttpResponse::Forbidden().json(ResponseError { + gorb_id_available: false, + ..Default::default() + }) + } + err_msg if err_msg.contains("unique") && err_msg.contains("email_key") => { + HttpResponse::Forbidden().json(ResponseError { + email_available: false, + ..Default::default() + }) + } _ => { error!("{}", err_msg); HttpResponse::InternalServerError().finish() } } - }, - }) + } + }, + ); } Ok(HttpResponse::InternalServerError().finish()) diff --git a/src/api/v1/auth/revoke.rs b/src/api/v1/auth/revoke.rs index 450ac06..f3285c4 100644 --- a/src/api/v1/auth/revoke.rs +++ b/src/api/v1/auth/revoke.rs @@ -1,10 +1,10 @@ -use actix_web::{error, post, web, Error, HttpResponse}; +use actix_web::{Error, HttpResponse, error, post, web}; use argon2::{PasswordHash, PasswordVerifier}; +use futures::{StreamExt, future}; use log::error; use serde::{Deserialize, Serialize}; -use futures::{future, StreamExt}; -use crate::{api::v1::auth::check_access_token, Data}; +use crate::{Data, api::v1::auth::check_access_token}; #[derive(Deserialize)] struct RevokeRequest { @@ -20,9 +20,7 @@ struct Response { impl Response { fn new(deleted: bool) -> Self { - Self { - deleted - } + Self { deleted } } } @@ -44,18 +42,21 @@ pub async fn res(mut payload: web::Payload, data: web::Data) -> Result) -> Result = tokens_raw.unwrap(); @@ -89,34 +97,45 @@ pub async fn res(mut payload: web::Payload, data: web::Data) -> Result> = results_access_tokens.iter().filter(|r| r.is_err()).collect(); - let refresh_tokens_errors: Vec<&Result> = results_refresh_tokens.iter().filter(|r| r.is_err()).collect(); + let access_tokens_errors: Vec<&Result> = + results_access_tokens + .iter() + .filter(|r| r.is_err()) + .collect(); + let refresh_tokens_errors: Vec<&Result> = + results_refresh_tokens + .iter() + .filter(|r| r.is_err()) + .collect(); if !access_tokens_errors.is_empty() && !refresh_tokens_errors.is_empty() { error!("{:?}", access_tokens_errors); error!("{:?}", refresh_tokens_errors); - return Ok(HttpResponse::InternalServerError().finish()) + return Ok(HttpResponse::InternalServerError().finish()); } else if !access_tokens_errors.is_empty() { error!("{:?}", access_tokens_errors); - return Ok(HttpResponse::InternalServerError().finish()) + return Ok(HttpResponse::InternalServerError().finish()); } else if !refresh_tokens_errors.is_empty() { error!("{:?}", refresh_tokens_errors); - return Ok(HttpResponse::InternalServerError().finish()) + return Ok(HttpResponse::InternalServerError().finish()); } - + Ok(HttpResponse::Ok().json(Response::new(true))) } diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 4eb1d63..2405b24 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -1,7 +1,7 @@ use actix_web::{Scope, web}; -mod stats; mod auth; +mod stats; mod user; pub fn web() -> Scope { diff --git a/src/api/v1/stats.rs b/src/api/v1/stats.rs index 62c3527..0ebf431 100644 --- a/src/api/v1/stats.rs +++ b/src/api/v1/stats.rs @@ -18,10 +18,13 @@ struct Response { #[get("/stats")] pub async fn res(data: web::Data) -> impl Responder { let accounts; - if let Ok(users) = sqlx::query("SELECT uuid FROM users").fetch_all(&data.pool).await { + if let Ok(users) = sqlx::query("SELECT uuid FROM users") + .fetch_all(&data.pool) + .await + { accounts = users.len(); } else { - return HttpResponse::InternalServerError().finish() + return HttpResponse::InternalServerError().finish(); } let response = Response { diff --git a/src/api/v1/user.rs b/src/api/v1/user.rs index 8a55288..b68ae90 100644 --- a/src/api/v1/user.rs +++ b/src/api/v1/user.rs @@ -1,10 +1,10 @@ -use actix_web::{error, post, web, Error, HttpResponse}; +use actix_web::{Error, HttpResponse, error, post, web}; +use futures::StreamExt; use log::error; use serde::{Deserialize, Serialize}; -use futures::StreamExt; use uuid::Uuid; -use crate::{api::v1::auth::check_access_token, Data}; +use crate::{Data, api::v1::auth::check_access_token}; #[derive(Deserialize)] struct AuthenticationRequest { @@ -21,7 +21,11 @@ struct Response { const MAX_SIZE: usize = 262_144; #[post("/user/{uuid}")] -pub async fn res(mut payload: web::Payload, path: web::Path<(String,)>, data: web::Data) -> Result { +pub async fn res( + mut payload: web::Payload, + path: web::Path<(String,)>, + data: web::Data, +) -> Result { let mut body = web::BytesMut::new(); while let Some(chunk) = payload.next().await { let chunk = chunk?; @@ -38,8 +42,8 @@ pub async fn res(mut payload: web::Payload, path: web::Path<(String,)>, data: we let authorized = check_access_token(authentication_request.access_token, &data.pool).await; - if authorized.is_err() { - return Ok(authorized.unwrap_err()) + if let Err(error) = authorized { + return Ok(error); } let mut uuid = authorized.unwrap(); @@ -48,23 +52,29 @@ pub async fn res(mut payload: web::Payload, path: web::Path<(String,)>, data: we let requested_uuid = Uuid::parse_str(&request); if requested_uuid.is_err() { - return Ok(HttpResponse::BadRequest().json(r#"{ "error": "UUID is invalid!" }"#)) + return Ok(HttpResponse::BadRequest().json(r#"{ "error": "UUID is invalid!" }"#)); } uuid = requested_uuid.unwrap() } - - let row = sqlx::query_as(&format!("SELECT username, display_name FROM users WHERE uuid = '{}'", uuid)) - .fetch_one(&data.pool) - .await; + let row = sqlx::query_as(&format!( + "SELECT username, display_name FROM users WHERE uuid = '{}'", + uuid + )) + .fetch_one(&data.pool) + .await; - if row.is_err() { - error!("{}", row.unwrap_err()); - return Ok(HttpResponse::InternalServerError().finish()) + if let Err(error) = row { + error!("{}", error); + return Ok(HttpResponse::InternalServerError().finish()); } let (username, display_name): (String, Option) = row.unwrap(); - Ok(HttpResponse::Ok().json(Response { uuid: uuid.to_string(), username, display_name: display_name.unwrap_or_default() })) + Ok(HttpResponse::Ok().json(Response { + uuid: uuid.to_string(), + username, + display_name: display_name.unwrap_or_default(), + })) } diff --git a/src/crypto.rs b/src/crypto.rs index ece899b..c4d96c8 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -4,11 +4,11 @@ use hex::encode; pub fn generate_access_token() -> Result { let mut buf = [0u8; 16]; fill(&mut buf)?; - Ok(encode(&buf)) + Ok(encode(buf)) } pub fn generate_refresh_token() -> Result { let mut buf = [0u8; 32]; fill(&mut buf)?; - Ok(encode(&buf)) + Ok(encode(buf)) } diff --git a/src/main.rs b/src/main.rs index 70865f1..0bf1ce0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,12 @@ struct Data { #[tokio::main] async fn main() -> Result<(), Error> { - SimpleLogger::new().with_level(log::LevelFilter::Info).with_colors(true).env().init().unwrap(); + SimpleLogger::new() + .with_level(log::LevelFilter::Info) + .with_colors(true) + .env() + .init() + .unwrap(); let args = Args::parse(); let config = ConfigBuilder::load(args.config).await?.build(); @@ -38,11 +43,12 @@ async fn main() -> Result<(), Error> { let pool = PgPool::connect_with(config.database.connect_options()).await?; - /* + /* TODO: Figure out if a table should be used here and if not then what. Also figure out if these should be different types from what they currently are and if we should add more "constraints" */ - sqlx::raw_sql(r#" + sqlx::raw_sql( + r#" CREATE TABLE IF NOT EXISTS users ( uuid uuid PRIMARY KEY UNIQUE NOT NULL, username varchar(32) UNIQUE NOT NULL, @@ -67,7 +73,8 @@ async fn main() -> Result<(), Error> { uuid uuid NOT NULL REFERENCES users(uuid), created int8 NOT NULL ) - "#) + "#, + ) .execute(&pool) .await?;