From 26b6601f5b9e6298be2bbcacc97931a47b0b5b29 Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 29 Apr 2025 21:53:49 +0200 Subject: [PATCH 1/2] feat: add in database support --- src/config.rs | 14 +++++++++++++- src/main.rs | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/config.rs b/src/config.rs index cefc272..27a426e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use crate::Error; use serde::Deserialize; +use sqlx::postgres::PgConnectOptions; use tokio::fs::read_to_string; #[derive(Debug, Deserialize)] @@ -12,7 +13,7 @@ pub struct ConfigBuilder { pub struct Database { username: String, password: String, - hostname: String, + host: String, database: String, port: u16, } @@ -67,3 +68,14 @@ pub struct Web { pub port: u16, pub ssl: bool, } + +impl Database { + pub fn connect_options(&self) -> PgConnectOptions { + PgConnectOptions::new() + .database(&self.database) + .host(&self.host) + .username(&self.username) + .password(&self.password) + .port(self.port) + } +} diff --git a/src/main.rs b/src/main.rs index 989630c..78f1fb9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use actix_web::{App, HttpServer, web}; +use sqlx::{Executor, PgPool, Pool, Postgres}; use std::time::SystemTime; mod config; use config::{Config, ConfigBuilder}; @@ -8,6 +9,7 @@ type Error = Box; #[derive(Clone)] struct Data { + pub pool: Pool, pub config: Config, pub start_time: SystemTime, } @@ -18,7 +20,20 @@ async fn main() -> Result<(), Error> { let web = config.web.clone(); + let pool = PgPool::connect_with(config.database.connect_options()).await?; + + pool.execute(r#"CREATE TABLE IF NOT EXISTS users ( + uuid uuid UNIQUE NOT NULL, + username varchar(32) UNIQUE NOT NULL, + display_name varchar(64), + password varchar(512) NOT NULL, + email varchar(100) UNIQUE NOT NULL, + email_verified integer NOT NULL DEFAULT '0', + PRIMARY KEY (uuid) + )"#).await?; + let data = Data { + pool, config, start_time: SystemTime::now(), }; From 19bad249d45f599c4eff80fa0175bc676e9027e0 Mon Sep 17 00:00:00 2001 From: Radical Date: Tue, 29 Apr 2025 21:54:41 +0200 Subject: [PATCH 2/2] feat: user registration Adds crates and code for user registration, this is EXTREMELY INSECURE AND FOR TESTING ONLY --- Cargo.toml | 8 ++- src/api/v1/mod.rs | 5 +- src/api/v1/register.rs | 134 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 src/api/v1/register.rs diff --git a/Cargo.toml b/Cargo.toml index 03eb2af..52ac0fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,10 +5,14 @@ edition = "2024" [dependencies] actix-web = "4.10" -serde = { version = "1.0.219", features = ["derive"] } +futures = "0.3" +regex = "1.11" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "postgres"] } toml = "0.8" -url = { version = "2.5.4", features = ["serde"] } +url = { version = "2.5", features = ["serde"] } +uuid = { version = "1.16", features = ["v7"] } [dependencies.tokio] version = "1.44" diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 542a0f5..690188f 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -1,7 +1,10 @@ use actix_web::{Scope, web}; mod stats; +mod register; pub fn web() -> Scope { - web::scope("/v1").service(stats::res) + web::scope("/v1") + .service(stats::res) + .service(register::res) } diff --git a/src/api/v1/register.rs b/src/api/v1/register.rs new file mode 100644 index 0000000..63dca6d --- /dev/null +++ b/src/api/v1/register.rs @@ -0,0 +1,134 @@ +use actix_web::{error, post, web, Error, HttpResponse}; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use futures::StreamExt; +use sqlx::Executor; +use uuid::Uuid; + +use crate::Data; + +const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION"); + +#[derive(Deserialize)] +struct AccountInformation { + identifier: String, + email: String, + password: String, + device_name: String, +} + +#[derive(Serialize)] +struct ResponseError { + signups_enabled: bool, + gorb_id_valid: bool, + gorb_id_available: bool, + email_valid: bool, + email_available: bool, + password_minimum_length: bool, + password_special_characters: bool, + password_letters: bool, + password_numbers: bool, +} + +impl Default for ResponseError { + fn default() -> Self { + Self { + signups_enabled: true, + gorb_id_valid: true, + gorb_id_available: true, + email_valid: true, + email_available: true, + password_minimum_length: true, + password_special_characters: true, + password_letters: true, + password_numbers: true, + } + } +} + +#[derive(Serialize)] +struct Response { + access_token: String, + user_id: String, + expires_in: u64, + refresh_token: String, +} + +const MAX_SIZE: usize = 262_144; + +#[post("/register")] +pub async fn res(mut payload: web::Payload, data: web::Data) -> Result { + 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 account_information = serde_json::from_slice::(&body)?; + + let uuid = Uuid::now_v7(); + + let email_regex = 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(); + + if !email_regex.is_match(&account_information.email) { + return Ok(HttpResponse::Forbidden().json( + ResponseError { + email_valid: false, + ..Default::default() + } + )) + } + + let username_regex = Regex::new(r"[a-zA-Z0-9.-_]").unwrap(); + + 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() + } + )) + } + + Ok(match data.pool.execute( + &*format!( + "INSERT INTO users VALUES ( '{}', '{}', NULL, '{}', '{}', '0' )", + uuid, + account_information.identifier, + account_information.password, + account_information.email, + ) + ).await { + Ok(v) => { + HttpResponse::Ok().json( + Response { + access_token: "bogus".to_string(), + user_id: "bogus".to_string(), + expires_in: 1, + refresh_token: "bogus".to_string(), + } + ) + }, + Err(error) => { + 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() + }), + _ => HttpResponse::Forbidden().json(ResponseError { + ..Default::default() + }) + } + }, + }) +}