Compare commits

..

4 commits

Author SHA1 Message Date
3369c6f084 fix response and sql request 2025-05-02 23:31:45 +02:00
8b0efd16fe fix sql request 2025-05-02 23:23:40 +02:00
8766ca57aa correct paths 2025-05-02 23:16:20 +02:00
8fcfef59d3 add test implementation for messaging 2025-05-02 23:15:27 +02:00
22 changed files with 362 additions and 336 deletions

View file

@ -9,7 +9,6 @@ lto = true
codegen-units = 1 codegen-units = 1
[dependencies] [dependencies]
actix-cors = "0.7.1"
actix-web = "4.10" actix-web = "4.10"
argon2 = { version = "0.5.3", features = ["std"] } argon2 = { version = "0.5.3", features = ["std"] }
clap = { version = "4.5.37", features = ["derive"] } clap = { version = "4.5.37", features = ["derive"] }
@ -22,7 +21,6 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
simple_logger = "5.0.0" simple_logger = "5.0.0"
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "postgres"] } sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "postgres"] }
redis = { version = "0.30", features= ["tokio-comp"] }
toml = "0.8" toml = "0.8"
url = { version = "2.5", features = ["serde"] } url = { version = "2.5", features = ["serde"] }
uuid = { version = "1.16", features = ["serde", "v7"] } uuid = { version = "1.16", features = ["serde", "v7"] }

View file

@ -18,12 +18,6 @@ RUN useradd --create-home --home-dir /gorb gorb
USER gorb USER gorb
ENV DATABASE_USERNAME="gorb" \ ENV DATABASE_USERNAME="gorb" DATABASE_PASSWORD="gorb" DATABASE="gorb" DATABASE_HOST="localhost" DATABASE_PORT="5432"
DATABASE_PASSWORD="gorb" \
DATABASE="gorb" \
DATABASE_HOST="database" \
DATABASE_PORT="5432" \
CACHE_DB_HOST="valkey" \
CACHE_DB_PORT="6379"
ENTRYPOINT ["/usr/bin/entrypoint.sh"] ENTRYPOINT ["/usr/bin/entrypoint.sh"]

View file

@ -34,8 +34,3 @@ services:
- POSTGRES_USER=gorb - POSTGRES_USER=gorb
- POSTGRES_PASSWORD=gorb - POSTGRES_PASSWORD=gorb
- POSTGRES_DB=gorb - POSTGRES_DB=gorb
valkey:
image: valkey/valkey
restart: always
networks:
- gorb

View file

@ -32,8 +32,3 @@ services:
- POSTGRES_USER=gorb - POSTGRES_USER=gorb
- POSTGRES_PASSWORD=gorb - POSTGRES_PASSWORD=gorb
- POSTGRES_DB=gorb - POSTGRES_DB=gorb
valkey:
image: valkey/valkey
restart: always
networks:
- gorb

View file

@ -16,10 +16,6 @@ password = "${DATABASE_PASSWORD}"
database = "${DATABASE}" database = "${DATABASE}"
host = "${DATABASE_HOST}" host = "${DATABASE_HOST}"
port = ${DATABASE_PORT} port = ${DATABASE_PORT}
[cache_database]
host = "${CACHE_DB_HOST}"
port = ${CACHE_DB_PORT}
EOF EOF
fi fi

View file

@ -1,11 +1,5 @@
use actix_web::Scope; pub mod v1;
use actix_web::web; pub mod versions;
mod v1; // This is purely for testing, will be deleted at a later date
mod versions; pub mod v0;
pub fn web() -> Scope {
web::scope("/api")
.service(v1::web())
.service(versions::res)
}

62
src/api/v0/channel.rs Normal file
View file

@ -0,0 +1,62 @@
use actix_web::{Error, HttpResponse, error, post, web};
use futures::StreamExt;
use log::error;
use serde::{Deserialize, Serialize};
use sqlx::prelude::FromRow;
use crate::{Data, api::v1::auth::check_access_token};
#[derive(Deserialize)]
struct Request {
access_token: String,
start: i32,
amount: i32,
}
#[derive(Serialize, FromRow)]
struct Response {
timestamp: i64,
uuid: String,
message: String,
}
const MAX_SIZE: usize = 262_144;
#[post("/channel")]
pub async fn res(
mut payload: web::Payload,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let mut body = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
let chunk = chunk?;
// limit max size of in-memory payload
if (body.len() + chunk.len()) > MAX_SIZE {
return Err(error::ErrorBadRequest("overflow"));
}
body.extend_from_slice(&chunk);
}
let request = serde_json::from_slice::<Request>(&body)?;
let authorized = check_access_token(request.access_token, &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let row = sqlx::query_as("SELECT timestamp, CAST(uuid AS VARCHAR), message FROM channel ORDER BY timestamp DESC LIMIT $1 OFFSET $2")
.bind(request.amount)
.bind(request.start)
.fetch_all(&data.pool)
.await;
if let Err(error) = row {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
let messages: Vec<Response> = row.unwrap();
Ok(HttpResponse::Ok().json(messages))
}

10
src/api/v0/mod.rs Normal file
View file

@ -0,0 +1,10 @@
use actix_web::{Scope, web};
mod channel;
mod send;
pub fn web() -> Scope {
web::scope("/v0")
.service(channel::res)
.service(send::res)
}

60
src/api/v0/send.rs Normal file
View file

@ -0,0 +1,60 @@
use std::time::{SystemTime, UNIX_EPOCH};
use actix_web::{Error, HttpResponse, error, post, web};
use futures::StreamExt;
use log::error;
use serde::Deserialize;
use crate::{Data, api::v1::auth::check_access_token};
#[derive(Deserialize)]
struct Request {
access_token: String,
message: String,
}
const MAX_SIZE: usize = 262_144;
#[post("/send")]
pub async fn res(
mut payload: web::Payload,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let mut body = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
let chunk = chunk?;
// limit max size of in-memory payload
if (body.len() + chunk.len()) > MAX_SIZE {
return Err(error::ErrorBadRequest("overflow"));
}
body.extend_from_slice(&chunk);
}
let request = serde_json::from_slice::<Request>(&body)?;
let authorized = check_access_token(request.access_token, &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as i64;
let row = sqlx::query(&format!("INSERT INTO channel (timestamp, uuid, message) VALUES ($1, '{}', $2)", uuid))
.bind(current_time)
.bind(request.message)
.execute(&data.pool)
.await;
if let Err(error) = row {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Ok().finish())
}

View file

@ -1,17 +1,17 @@
use std::time::{SystemTime, UNIX_EPOCH}; 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 argon2::{PasswordHash, PasswordVerifier};
use futures::StreamExt; use futures::StreamExt;
use log::error; use log::error;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, Data Data,
api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX},
crypto::{generate_access_token, generate_refresh_token},
}; };
use super::Response;
#[derive(Deserialize)] #[derive(Deserialize)]
struct LoginInformation { struct LoginInformation {
username: String, username: String,
@ -19,6 +19,12 @@ struct LoginInformation {
device_name: String, device_name: String,
} }
#[derive(Serialize)]
pub struct Response {
pub access_token: String,
pub refresh_token: String,
}
const MAX_SIZE: usize = 262_144; const MAX_SIZE: usize = 262_144;
#[post("/login")] #[post("/login")]
@ -154,7 +160,7 @@ async fn login(
.as_secs() as i64; .as_secs() as i64;
if let Err(error) = sqlx::query(&format!( if let Err(error) = sqlx::query(&format!(
"INSERT INTO refresh_tokens (token, uuid, created_at, device_name) VALUES ($1, '{}', $2, $3 )", "INSERT INTO refresh_tokens (token, uuid, created, device_name) VALUES ($1, '{}', $2, $3 )",
uuid uuid
)) ))
.bind(&refresh_token) .bind(&refresh_token)
@ -168,7 +174,7 @@ async fn login(
} }
if let Err(error) = sqlx::query(&format!( if let Err(error) = sqlx::query(&format!(
"INSERT INTO access_tokens (token, refresh_token, uuid, created_at) VALUES ($1, $2, '{}', $3 )", "INSERT INTO access_tokens (token, refresh_token, uuid, created) VALUES ($1, $2, '{}', $3 )",
uuid uuid
)) ))
.bind(&access_token) .bind(&access_token)
@ -181,7 +187,8 @@ async fn login(
return HttpResponse::InternalServerError().finish() return HttpResponse::InternalServerError().finish()
} }
HttpResponse::Ok().cookie(refresh_token_cookie(refresh_token)).json(Response { HttpResponse::Ok().json(Response {
access_token, access_token,
refresh_token,
}) })
} }

View file

@ -7,7 +7,6 @@ use std::{
use actix_web::{HttpResponse, Scope, web}; use actix_web::{HttpResponse, Scope, web};
use log::error; use log::error;
use regex::Regex; use regex::Regex;
use serde::Serialize;
use sqlx::Postgres; use sqlx::Postgres;
use uuid::Uuid; use uuid::Uuid;
@ -16,16 +15,12 @@ mod refresh;
mod register; mod register;
mod revoke; mod revoke;
#[derive(Serialize)]
struct Response {
access_token: String,
}
static EMAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| { static EMAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+(?:\.[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+)*@(?:[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?").unwrap() Regex::new(r"[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+(?:\.[-A-Za-z0-9!#$%&'*+/=?^_`{|}~]+)*@(?:[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?\.)+[A-Za-z0-9](?:[-A-Za-z0-9]*[A-Za-z0-9])?").unwrap()
}); });
static USERNAME_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); // FIXME: This regex doesnt seem to be working
static USERNAME_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[a-zA-Z0-9.-_]").unwrap());
// Password is expected to be hashed using SHA3-384 // Password is expected to be hashed using SHA3-384
static PASSWORD_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); static PASSWORD_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap());
@ -39,36 +34,33 @@ pub fn web() -> Scope {
} }
pub async fn check_access_token( pub async fn check_access_token(
access_token: &str, access_token: String,
pool: &sqlx::Pool<Postgres>, pool: &sqlx::Pool<Postgres>,
) -> Result<Uuid, HttpResponse> { ) -> Result<Uuid, HttpResponse> {
let row = let row = sqlx::query_as(
sqlx::query_as("SELECT CAST(uuid as VARCHAR), created_at FROM access_tokens WHERE token = $1") "SELECT CAST(uuid as VARCHAR), created FROM access_tokens WHERE token = $1",
)
.bind(&access_token) .bind(&access_token)
.fetch_one(pool) .fetch_one(pool)
.await; .await;
if let Err(error) = row { if let Err(error) = row {
if error.to_string() if error.to_string() == "no rows returned by a query that expected to return at least one row" {
== "no rows returned by a query that expected to return at least one row" return Err(HttpResponse::Unauthorized().finish())
{
return Err(HttpResponse::Unauthorized().finish());
} }
error!("{}", error); error!("{}", error);
return Err(HttpResponse::InternalServerError().json( return Err(HttpResponse::InternalServerError().json(r#"{ "error": "Unhandled exception occured, contact the server administrator" }"#))
r#"{ "error": "Unhandled exception occured, contact the server administrator" }"#,
));
} }
let (uuid, created_at): (String, i64) = row.unwrap(); let (uuid, created): (String, i64) = row.unwrap();
let current_time = SystemTime::now() let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
.unwrap() .unwrap()
.as_secs() as i64; .as_secs() as i64;
let lifetime = current_time - created_at; let lifetime = current_time - created;
if lifetime > 3600 { if lifetime > 3600 {
return Err(HttpResponse::Unauthorized().finish()); return Err(HttpResponse::Unauthorized().finish());

View file

@ -1,22 +1,40 @@
use actix_web::{post, web, Error, HttpRequest, HttpResponse}; use actix_web::{Error, HttpResponse, error, post, web};
use futures::StreamExt;
use log::error; use log::error;
use serde::{Deserialize, Serialize};
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{SystemTime, UNIX_EPOCH};
use crate::{ use crate::{
utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, Data Data,
crypto::{generate_access_token, generate_refresh_token},
}; };
use super::Response; #[derive(Deserialize)]
struct RefreshRequest {
refresh_token: String,
}
#[derive(Serialize)]
struct Response {
refresh_token: String,
access_token: String,
}
const MAX_SIZE: usize = 262_144;
#[post("/refresh")] #[post("/refresh")]
pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { pub async fn res(mut payload: web::Payload, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let recv_refresh_token_cookie = req.cookie("refresh_token"); let mut body = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
if let None = recv_refresh_token_cookie { let chunk = chunk?;
return Ok(HttpResponse::Unauthorized().finish()) // limit max size of in-memory payload
if (body.len() + chunk.len()) > MAX_SIZE {
return Err(error::ErrorBadRequest("overflow"));
}
body.extend_from_slice(&chunk);
} }
let mut refresh_token = String::from(recv_refresh_token_cookie.unwrap().value()); let refresh_request = serde_json::from_slice::<RefreshRequest>(&body)?;
let current_time = SystemTime::now() let current_time = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@ -24,29 +42,33 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
.as_secs() as i64; .as_secs() as i64;
if let Ok(row) = if let Ok(row) =
sqlx::query_scalar("SELECT created_at FROM refresh_tokens WHERE token = $1") sqlx::query_as("SELECT CAST(uuid as VARCHAR), created FROM refresh_tokens WHERE token = $1")
.bind(&refresh_token) .bind(&refresh_request.refresh_token)
.fetch_one(&data.pool) .fetch_one(&data.pool)
.await .await
{ {
let created_at: i64 = row; let (uuid, created): (String, i64) = row;
let lifetime = current_time - created_at; if let Err(error) = sqlx::query("DELETE FROM access_tokens WHERE refresh_token = $1")
.bind(&refresh_request.refresh_token)
if lifetime > 2592000 {
if let Err(error) = sqlx::query("DELETE FROM refresh_tokens WHERE token = $1")
.bind(&refresh_token)
.execute(&data.pool) .execute(&data.pool)
.await .await
{ {
error!("{}", error); error!("{}", error);
} }
let mut refresh_token_cookie = refresh_token_cookie(refresh_token); let lifetime = current_time - created;
refresh_token_cookie.make_removal(); 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
{
error!("{}", error);
}
return Ok(HttpResponse::Unauthorized().cookie(refresh_token_cookie).finish()); return Ok(HttpResponse::Unauthorized().finish());
} }
let current_time = SystemTime::now() let current_time = SystemTime::now()
@ -54,6 +76,8 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
.unwrap() .unwrap()
.as_secs() as i64; .as_secs() as i64;
let mut refresh_token = refresh_request.refresh_token;
if lifetime > 1987200 { if lifetime > 1987200 {
let new_refresh_token = generate_refresh_token(); let new_refresh_token = generate_refresh_token();
@ -64,7 +88,7 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
let new_refresh_token = new_refresh_token.unwrap(); let new_refresh_token = new_refresh_token.unwrap();
match sqlx::query("UPDATE refresh_tokens SET token = $1, created_at = $2 WHERE token = $3") match sqlx::query("UPDATE refresh_tokens SET token = $1, created = $2 WHERE token = $3")
.bind(&new_refresh_token) .bind(&new_refresh_token)
.bind(current_time) .bind(current_time)
.bind(&refresh_token) .bind(&refresh_token)
@ -89,24 +113,21 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
let access_token = access_token.unwrap(); let access_token = access_token.unwrap();
if let Err(error) = sqlx::query("UPDATE access_tokens SET token = $1, created_at = $2 WHERE refresh_token = $3") if let Err(error) = sqlx::query(&format!("INSERT INTO access_tokens (token, refresh_token, uuid, created) VALUES ($1, $2, '{}', $3 )", uuid))
.bind(&access_token) .bind(&access_token)
.bind(current_time)
.bind(&refresh_token) .bind(&refresh_token)
.bind(current_time)
.execute(&data.pool) .execute(&data.pool)
.await { .await {
error!("{}", error); error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish()) return Ok(HttpResponse::InternalServerError().finish())
} }
return Ok(HttpResponse::Ok().cookie(refresh_token_cookie(refresh_token)).json(Response { return Ok(HttpResponse::Ok().json(Response {
refresh_token,
access_token, access_token,
})); }));
} }
let mut refresh_token_cookie = refresh_token_cookie(refresh_token); Ok(HttpResponse::Unauthorized().finish())
refresh_token_cookie.make_removal();
Ok(HttpResponse::Unauthorized().cookie(refresh_token_cookie).finish())
} }

View file

@ -10,9 +10,11 @@ use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use super::Response; use super::login::Response;
use crate::{ use crate::{
api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX}, utils::{generate_access_token, generate_refresh_token, refresh_token_cookie}, Data Data,
api::v1::auth::{EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX},
crypto::{generate_access_token, generate_refresh_token},
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
@ -137,7 +139,7 @@ pub async fn res(mut payload: web::Payload, data: web::Data<Data>) -> Result<Htt
.unwrap() .unwrap()
.as_secs() as i64; .as_secs() as i64;
if let Err(error) = sqlx::query(&format!("INSERT INTO refresh_tokens (token, uuid, created_at, device_name) VALUES ($1, '{}', $2, $3 )", uuid)) if let Err(error) = sqlx::query(&format!("INSERT INTO refresh_tokens (token, uuid, created, device_name) VALUES ($1, '{}', $2, $3 )", uuid))
.bind(&refresh_token) .bind(&refresh_token)
.bind(current_time) .bind(current_time)
.bind(account_information.device_name) .bind(account_information.device_name)
@ -147,7 +149,7 @@ pub async fn res(mut payload: web::Payload, data: web::Data<Data>) -> Result<Htt
return Ok(HttpResponse::InternalServerError().finish()) return Ok(HttpResponse::InternalServerError().finish())
} }
if let Err(error) = sqlx::query(&format!("INSERT INTO access_tokens (token, refresh_token, uuid, created_at) VALUES ($1, $2, '{}', $3 )", uuid)) if let Err(error) = sqlx::query(&format!("INSERT INTO access_tokens (token, refresh_token, uuid, created) VALUES ($1, $2, '{}', $3 )", uuid))
.bind(&access_token) .bind(&access_token)
.bind(&refresh_token) .bind(&refresh_token)
.bind(current_time) .bind(current_time)
@ -157,8 +159,9 @@ pub async fn res(mut payload: web::Payload, data: web::Data<Data>) -> Result<Htt
return Ok(HttpResponse::InternalServerError().finish()) return Ok(HttpResponse::InternalServerError().finish())
} }
HttpResponse::Ok().cookie(refresh_token_cookie(refresh_token)).json(Response { HttpResponse::Ok().json(Response {
access_token, access_token,
refresh_token,
}) })
} }
Err(error) => { Err(error) => {

View file

@ -1,13 +1,14 @@
use actix_web::{Error, HttpRequest, HttpResponse, error, post, web}; use actix_web::{Error, HttpResponse, error, post, web};
use argon2::{PasswordHash, PasswordVerifier}; use argon2::{PasswordHash, PasswordVerifier};
use futures::{StreamExt, future}; use futures::{StreamExt, future};
use log::error; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{Data, api::v1::auth::check_access_token, utils::get_auth_header}; use crate::{Data, api::v1::auth::check_access_token};
#[derive(Deserialize)] #[derive(Deserialize)]
struct RevokeRequest { struct RevokeRequest {
access_token: String,
password: String, password: String,
device_name: String, device_name: String,
} }
@ -26,19 +27,7 @@ impl Response {
const MAX_SIZE: usize = 262_144; const MAX_SIZE: usize = 262_144;
#[post("/revoke")] #[post("/revoke")]
pub async fn res( pub async fn res(mut payload: web::Payload, data: web::Data<Data>) -> Result<HttpResponse, Error> {
req: HttpRequest,
mut payload: web::Payload,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers);
if let Err(error) = auth_header {
return Ok(error);
}
let mut body = web::BytesMut::new(); let mut body = web::BytesMut::new();
while let Some(chunk) = payload.next().await { while let Some(chunk) = payload.next().await {
let chunk = chunk?; let chunk = chunk?;
@ -51,7 +40,7 @@ pub async fn res(
let revoke_request = serde_json::from_slice::<RevokeRequest>(&body)?; let revoke_request = serde_json::from_slice::<RevokeRequest>(&body)?;
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await; let authorized = check_access_token(revoke_request.access_token, &data.pool).await;
if let Err(error) = authorized { if let Err(error) = authorized {
return Ok(error); return Ok(error);
@ -105,9 +94,16 @@ pub async fn res(
let tokens: Vec<String> = tokens_raw.unwrap(); let tokens: Vec<String> = tokens_raw.unwrap();
let mut access_tokens_delete = vec![];
let mut refresh_tokens_delete = vec![]; let mut refresh_tokens_delete = vec![];
for token in tokens { for token in tokens {
access_tokens_delete.push(
sqlx::query("DELETE FROM access_tokens WHERE refresh_token = $1")
.bind(token.clone())
.execute(&data.pool),
);
refresh_tokens_delete.push( refresh_tokens_delete.push(
sqlx::query("DELETE FROM refresh_tokens WHERE token = $1") sqlx::query("DELETE FROM refresh_tokens WHERE token = $1")
.bind(token.clone()) .bind(token.clone())
@ -115,16 +111,29 @@ pub async fn res(
); );
} }
let results = future::join_all(refresh_tokens_delete).await; let results_access_tokens = future::join_all(access_tokens_delete).await;
let results_refresh_tokens = future::join_all(refresh_tokens_delete).await;
let errors: Vec<&Result<sqlx::postgres::PgQueryResult, sqlx::Error>> = let access_tokens_errors: Vec<&Result<sqlx::postgres::PgQueryResult, sqlx::Error>> =
results results_access_tokens
.iter()
.filter(|r| r.is_err())
.collect();
let refresh_tokens_errors: Vec<&Result<sqlx::postgres::PgQueryResult, sqlx::Error>> =
results_refresh_tokens
.iter() .iter()
.filter(|r| r.is_err()) .filter(|r| r.is_err())
.collect(); .collect();
if !errors.is_empty() { if !access_tokens_errors.is_empty() && !refresh_tokens_errors.is_empty() {
error!("{:?}", errors); error!("{:?}", access_tokens_errors);
error!("{:?}", refresh_tokens_errors);
return Ok(HttpResponse::InternalServerError().finish());
} else if !access_tokens_errors.is_empty() {
error!("{:?}", access_tokens_errors);
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());
} }

View file

@ -1,6 +1,6 @@
use actix_web::{Scope, web}; use actix_web::{Scope, web};
mod auth; pub mod auth;
mod stats; mod stats;
mod users; mod users;

View file

@ -1,8 +1,14 @@
use actix_web::{Error, HttpRequest, HttpResponse, get, web}; use actix_web::{Error, HttpResponse, error, post, web};
use futures::StreamExt;
use log::error; use log::error;
use serde::Serialize; use serde::{Deserialize, Serialize};
use crate::{Data, api::v1::auth::check_access_token, utils::get_auth_header}; use crate::{Data, api::v1::auth::check_access_token};
#[derive(Deserialize)]
struct AuthenticationRequest {
access_token: String,
}
#[derive(Serialize)] #[derive(Serialize)]
struct Response { struct Response {
@ -11,17 +17,26 @@ struct Response {
display_name: String, display_name: String,
} }
#[get("/me")] const MAX_SIZE: usize = 262_144;
pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers); #[post("/me")]
pub async fn res(
if let Err(error) = auth_header { mut payload: web::Payload,
return Ok(error); data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let mut body = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
let chunk = chunk?;
// limit max size of in-memory payload
if (body.len() + chunk.len()) > MAX_SIZE {
return Err(error::ErrorBadRequest("overflow"));
}
body.extend_from_slice(&chunk);
} }
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await; let authentication_request = serde_json::from_slice::<AuthenticationRequest>(&body)?;
let authorized = check_access_token(authentication_request.access_token, &data.pool).await;
if let Err(error) = authorized { if let Err(error) = authorized {
return Ok(error); return Ok(error);

View file

@ -1,16 +1,18 @@
use crate::{Data, api::v1::auth::check_access_token, utils::get_auth_header}; use actix_web::{error, post, web, Error, HttpResponse, Scope};
use actix_web::{get, web, Error, HttpRequest, HttpResponse, Scope}; use futures::StreamExt;
use log::error; use log::error;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::prelude::FromRow; use sqlx::prelude::FromRow;
use crate::{Data, api::v1::auth::check_access_token};
mod me; mod me;
mod uuid; mod uuid;
#[derive(Deserialize)] #[derive(Deserialize)]
struct RequestQuery { struct Request {
start: Option<i32>, access_token: String,
amount: Option<i32>, start: i32,
amount: i32,
} }
#[derive(Serialize, FromRow)] #[derive(Serialize, FromRow)]
@ -21,6 +23,8 @@ struct Response {
email: String, email: String,
} }
const MAX_SIZE: usize = 262_144;
pub fn web() -> Scope { pub fn web() -> Scope {
web::scope("/users") web::scope("/users")
.service(res) .service(res)
@ -28,33 +32,36 @@ pub fn web() -> Scope {
.service(uuid::res) .service(uuid::res)
} }
#[get("")] #[post("")]
pub async fn res( pub async fn res(
req: HttpRequest, mut payload: web::Payload,
request_query: web::Query<RequestQuery>,
data: web::Data<Data>, data: web::Data<Data>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let headers = req.headers(); let mut body = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
let auth_header = get_auth_header(headers); let chunk = chunk?;
// limit max size of in-memory payload
let start = request_query.start.unwrap_or(0); if (body.len() + chunk.len()) > MAX_SIZE {
return Err(error::ErrorBadRequest("overflow"));
let amount = request_query.amount.unwrap_or(10); }
body.extend_from_slice(&chunk);
if amount > 100 {
return Ok(HttpResponse::BadRequest().finish());
} }
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await; let request = serde_json::from_slice::<Request>(&body)?;
if request.amount > 100 {
return Ok(HttpResponse::BadRequest().finish())
}
let authorized = check_access_token(request.access_token, &data.pool).await;
if let Err(error) = authorized { if let Err(error) = authorized {
return Ok(error); return Ok(error);
} }
let row = sqlx::query_as("SELECT CAST(uuid AS VARCHAR), username, display_name, email FROM users ORDER BY username LIMIT $1 OFFSET $2") let row = sqlx::query_as("SELECT CAST(uuid AS VARCHAR), username, display_name, email FROM users ORDER BY username LIMIT $1 OFFSET $2")
.bind(amount) .bind(request.amount)
.bind(start) .bind(request.start)
.fetch_all(&data.pool) .fetch_all(&data.pool)
.await; .await;
@ -67,3 +74,4 @@ pub async fn res(
Ok(HttpResponse::Ok().json(accounts)) Ok(HttpResponse::Ok().json(accounts))
} }

View file

@ -1,45 +1,51 @@
use actix_web::{Error, HttpRequest, HttpResponse, get, web}; use actix_web::{Error, HttpResponse, error, post, web};
use futures::StreamExt;
use log::error; use log::error;
use serde::Serialize; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{Data, api::v1::auth::check_access_token, utils::get_auth_header}; use crate::{Data, api::v1::auth::check_access_token};
#[derive(Serialize, Clone)] #[derive(Deserialize)]
struct AuthenticationRequest {
access_token: String,
}
#[derive(Serialize)]
struct Response { struct Response {
uuid: String, uuid: String,
username: String, username: String,
display_name: String, display_name: String,
} }
#[get("/{uuid}")] const MAX_SIZE: usize = 262_144;
#[post("/{uuid}")]
pub async fn res( pub async fn res(
req: HttpRequest, mut payload: web::Payload,
path: web::Path<(Uuid,)>, path: web::Path<(Uuid,)>,
data: web::Data<Data>, data: web::Data<Data>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let headers = req.headers(); let mut body = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
let chunk = chunk?;
// limit max size of in-memory payload
if (body.len() + chunk.len()) > MAX_SIZE {
return Err(error::ErrorBadRequest("overflow"));
}
body.extend_from_slice(&chunk);
}
let uuid = path.into_inner().0; let uuid = path.into_inner().0;
let auth_header = get_auth_header(headers); let authentication_request = serde_json::from_slice::<AuthenticationRequest>(&body)?;
if let Err(error) = auth_header { let authorized = check_access_token(authentication_request.access_token, &data.pool).await;
return Ok(error);
}
let authorized = check_access_token(auth_header.unwrap(), &data.pool).await;
if let Err(error) = authorized { if let Err(error) = authorized {
return Ok(error); return Ok(error);
} }
let cache_result = data.get_cache_key(uuid.to_string()).await;
if let Ok(cache_hit) = cache_result {
return Ok(HttpResponse::Ok().content_type("application/json").body(cache_hit))
}
let row = sqlx::query_as(&format!( let row = sqlx::query_as(&format!(
"SELECT username, display_name FROM users WHERE uuid = '{}'", "SELECT username, display_name FROM users WHERE uuid = '{}'",
uuid uuid
@ -54,18 +60,9 @@ pub async fn res(
let (username, display_name): (String, Option<String>) = row.unwrap(); let (username, display_name): (String, Option<String>) = row.unwrap();
let user = Response { Ok(HttpResponse::Ok().json(Response {
uuid: uuid.to_string(), uuid: uuid.to_string(),
username, username,
display_name: display_name.unwrap_or_default(), display_name: display_name.unwrap_or_default(),
}; }))
let cache_result = data.set_cache_key(uuid.to_string(), user.clone(), 1800).await;
if let Err(error) = cache_result {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Ok().json(user))
} }

View file

@ -7,7 +7,6 @@ use tokio::fs::read_to_string;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ConfigBuilder { pub struct ConfigBuilder {
database: Database, database: Database,
cache_database: CacheDatabase,
web: Option<WebBuilder>, web: Option<WebBuilder>,
} }
@ -20,15 +19,6 @@ pub struct Database {
port: u16, port: u16,
} }
#[derive(Debug, Deserialize, Clone)]
pub struct CacheDatabase {
username: Option<String>,
password: Option<String>,
host: String,
database: Option<String>,
port: u16,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct WebBuilder { struct WebBuilder {
url: Option<String>, url: Option<String>,
@ -61,7 +51,6 @@ impl ConfigBuilder {
Config { Config {
database: self.database, database: self.database,
cache_database: self.cache_database,
web, web,
} }
} }
@ -70,7 +59,6 @@ impl ConfigBuilder {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Config { pub struct Config {
pub database: Database, pub database: Database,
pub cache_database: CacheDatabase,
pub web: Web, pub web: Web,
} }
@ -90,33 +78,3 @@ impl Database {
.port(self.port) .port(self.port)
} }
} }
impl CacheDatabase {
pub fn url(&self) -> String {
let mut url = String::from("redis://");
if let Some(username) = &self.username {
url += username;
}
if let Some(password) = &self.password {
url += ":";
url += password;
}
if self.username.is_some() || self.password.is_some() {
url += "@";
}
url += &self.host;
url += ":";
url += &self.port.to_string();
if let Some(database) = &self.database {
url += "/";
url += database;
}
url
}
}

14
src/crypto.rs Normal file
View file

@ -0,0 +1,14 @@
use getrandom::fill;
use hex::encode;
pub fn generate_access_token() -> Result<String, getrandom::Error> {
let mut buf = [0u8; 16];
fill(&mut buf)?;
Ok(encode(buf))
}
pub fn generate_refresh_token() -> Result<String, getrandom::Error> {
let mut buf = [0u8; 32];
fill(&mut buf)?;
Ok(encode(buf))
}

View file

@ -1,4 +1,3 @@
use actix_cors::Cors;
use actix_web::{App, HttpServer, web}; use actix_web::{App, HttpServer, web};
use argon2::Argon2; use argon2::Argon2;
use clap::Parser; use clap::Parser;
@ -8,8 +7,7 @@ use std::time::SystemTime;
mod config; mod config;
use config::{Config, ConfigBuilder}; use config::{Config, ConfigBuilder};
mod api; mod api;
pub mod crypto;
pub mod utils;
type Error = Box<dyn std::error::Error>; type Error = Box<dyn std::error::Error>;
@ -23,7 +21,6 @@ struct Args {
#[derive(Clone)] #[derive(Clone)]
struct Data { struct Data {
pub pool: Pool<Postgres>, pub pool: Pool<Postgres>,
pub cache_pool: redis::Client,
pub _config: Config, pub _config: Config,
pub argon2: Argon2<'static>, pub argon2: Argon2<'static>,
pub start_time: SystemTime, pub start_time: SystemTime,
@ -45,8 +42,6 @@ async fn main() -> Result<(), Error> {
let pool = PgPool::connect_with(config.database.connect_options()).await?; let pool = PgPool::connect_with(config.database.connect_options()).await?;
let cache_pool = redis::Client::open(config.cache_database.url())?;
/* /*
TODO: Figure out if a table should be used here and if not then what. 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" Also figure out if these should be different types from what they currently are and if we should add more "constraints"
@ -68,14 +63,19 @@ async fn main() -> Result<(), Error> {
CREATE TABLE IF NOT EXISTS refresh_tokens ( CREATE TABLE IF NOT EXISTS refresh_tokens (
token varchar(64) PRIMARY KEY UNIQUE NOT NULL, token varchar(64) PRIMARY KEY UNIQUE NOT NULL,
uuid uuid NOT NULL REFERENCES users(uuid), uuid uuid NOT NULL REFERENCES users(uuid),
created_at int8 NOT NULL, created int8 NOT NULL,
device_name varchar(16) NOT NULL device_name varchar(16) NOT NULL
); );
CREATE TABLE IF NOT EXISTS access_tokens ( CREATE TABLE IF NOT EXISTS access_tokens (
token varchar(32) PRIMARY KEY UNIQUE NOT NULL, token varchar(32) PRIMARY KEY UNIQUE NOT NULL,
refresh_token varchar(64) UNIQUE NOT NULL REFERENCES refresh_tokens(token) ON UPDATE CASCADE ON DELETE CASCADE, refresh_token varchar(64) UNIQUE NOT NULL REFERENCES refresh_tokens(token),
uuid uuid NOT NULL REFERENCES users(uuid), uuid uuid NOT NULL REFERENCES users(uuid),
created_at int8 NOT NULL created int8 NOT NULL
);
CREATE TABLE IF NOT EXISTS channel (
timestamp int8 PRIMARY KEY NOT NULL,
uuid uuid NOT NULL REFERENCES users(uuid),
message varchar(2000) NOT NULL
) )
"#, "#,
) )
@ -84,46 +84,18 @@ async fn main() -> Result<(), Error> {
let data = Data { let data = Data {
pool, pool,
cache_pool,
_config: config, _config: config,
// TODO: Possibly implement "pepper" into this (thinking it could generate one if it doesnt exist and store it on disk) // TODO: Possibly implement "pepper" into this (thinking it could generate one if it doesnt exist and store it on disk)
argon2: Argon2::default(), argon2: Argon2::default(),
start_time: SystemTime::now(), start_time: SystemTime::now(),
}; };
HttpServer::new(move || { HttpServer::new(move || {
// Set CORS headers
let cors = Cors::default()
/*
Set Allowed-Control-Allow-Origin header to whatever
the request's Origin header is. Must be done like this
rather than setting it to "*" due to CORS not allowing
sending of credentials (cookies) with wildcard origin.
*/
.allowed_origin_fn(|_origin, _req_head| {
true
})
/*
Allows any request method in CORS preflight requests.
This will be restricted to only ones actually in use later.
*/
.allow_any_method()
/*
Allows any header(s) in request in CORS preflight requests.
This wll be restricted to only ones actually in use later.
*/
.allow_any_header()
/*
Allows browser to include cookies in requests.
This is needed for receiving the secure HttpOnly refresh_token cookie.
*/
.supports_credentials();
App::new() App::new()
.app_data(web::Data::new(data.clone())) .app_data(web::Data::new(data.clone()))
.wrap(cors) .service(api::versions::res)
.service(api::web()) .service(api::v1::web())
.service(api::v0::web())
}) })
.bind((web.url, web.port))? .bind((web.url, web.port))?
.run() .run()

View file

@ -1,74 +0,0 @@
use actix_web::{cookie::{time::Duration, Cookie, SameSite}, http::header::HeaderMap, HttpResponse};
use getrandom::fill;
use hex::encode;
use redis::RedisError;
use serde::Serialize;
use crate::Data;
pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, HttpResponse> {
let auth_token = headers.get(actix_web::http::header::AUTHORIZATION);
if let None = auth_token {
return Err(HttpResponse::Unauthorized().finish());
}
let auth = auth_token.unwrap().to_str();
if let Err(error) = auth {
return Err(HttpResponse::Unauthorized().json(format!(r#" {{ "error": "{}" }} "#, error)));
}
let auth_value = auth.unwrap().split_whitespace().nth(1);
if let None = auth_value {
return Err(HttpResponse::BadRequest().finish());
}
Ok(auth_value.unwrap())
}
pub fn refresh_token_cookie(refresh_token: String) -> Cookie<'static> {
Cookie::build("refresh_token", refresh_token)
.http_only(true)
.secure(true)
.same_site(SameSite::None)
.path("/api")
.max_age(Duration::days(30))
.finish()
}
pub fn generate_access_token() -> Result<String, getrandom::Error> {
let mut buf = [0u8; 16];
fill(&mut buf)?;
Ok(encode(buf))
}
pub fn generate_refresh_token() -> Result<String, getrandom::Error> {
let mut buf = [0u8; 32];
fill(&mut buf)?;
Ok(encode(buf))
}
impl Data {
pub async fn set_cache_key(&self, key: String, value: impl Serialize, expire: u32) -> Result<(), RedisError> {
let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?;
let key_encoded = encode(key);
let value_json = serde_json::to_string(&value).unwrap();
redis::cmd("SET",).arg(&[key_encoded.clone(), value_json]).exec_async(&mut conn).await?;
redis::cmd("EXPIRE").arg(&[key_encoded, expire.to_string()]).exec_async(&mut conn).await
}
pub async fn get_cache_key(&self, key: String) -> Result<String, RedisError> {
let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?;
let key_encoded = encode(key);
redis::cmd("GET").arg(key_encoded).query_async(&mut conn).await
}
}