diff --git a/.woodpecker/build-and-publish.yml b/.woodpecker/build-and-publish.yml index 7f7096b..57f2761 100644 --- a/.woodpecker/build-and-publish.yml +++ b/.woodpecker/build-and-publish.yml @@ -1,13 +1,14 @@ +when: + - event: push + branch: main + steps: - name: build-x86_64 - image: rust:1.88-bookworm + image: rust:bookworm commands: - cargo build --release - when: - - event: push - - name: build-arm64 - image: rust:1.88-bookworm + image: rust:bookworm commands: - dpkg --add-architecture arm64 - apt-get update -y && apt-get install -y crossbuild-essential-arm64 libssl-dev:arm64 @@ -17,9 +18,6 @@ steps: CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc PKG_CONFIG_ALLOW_CROSS: 1 PKG_CONFIG_PATH: /usr/aarch64-linux-gnu/lib/pkgconfig - when: - - event: push - - name: container-build-and-publish image: docker commands: @@ -30,20 +28,3 @@ steps: from_secret: docker_password volumes: - /var/run/podman/podman.sock:/var/run/docker.sock - when: - - branch: main - event: push - - - name: container-build-and-publish-staging - image: docker - commands: - - docker login --username radical --password $PASSWORD git.gorb.app - - docker buildx build --platform linux/amd64,linux/arm64 --rm --push -t git.gorb.app/gorb/backend:staging . - environment: - PASSWORD: - from_secret: docker_password - volumes: - - /var/run/podman/podman.sock:/var/run/docker.sock - when: - - branch: staging - event: push diff --git a/.woodpecker/publish-docs.yml b/.woodpecker/publish-docs.yml index 7744dc7..e6ce482 100644 --- a/.woodpecker/publish-docs.yml +++ b/.woodpecker/publish-docs.yml @@ -4,7 +4,7 @@ when: steps: - name: build-docs - image: rust:1.88-bookworm + image: rust:bookworm commands: - cargo doc --release --no-deps diff --git a/Cargo.toml b/Cargo.toml index cdbcc0f..c1c71bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,28 +20,29 @@ thiserror = "2.0.12" # CLI clap = { version = "4.5", features = ["derive"] } log = "0.4" +simple_logger = "5.0.0" # async +futures = "0.3" tokio = { version = "1.46", features = ["full"] } futures-util = "0.3.31" # Data (de)serialization serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -toml = "0.9" -bytes = "1.10.1" +toml = "0.8" # File Storage bindet = "0.3.2" bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false } # Web Server -axum = { version = "0.8.4", features = ["multipart", "ws"] } -axum-extra = { version = "0.10.1", features = ["cookie", "typed-header"] } -tower-http = { version = "0.6.6", features = ["cors"] } -#socketioxide = { version = "0.17.2", features = ["state"] } +actix-web = "4.11" +actix-cors = "0.7.1" +actix-ws = "0.3.0" +actix-multipart = "0.7.2" url = { version = "2.5", features = ["serde"] } -time = "0.3.41" +tokio-tungstenite = { version = "0.27", features = ["native-tls", "url"] } # Database uuid = { version = "1.17", features = ["serde", "v7"] } @@ -59,5 +60,5 @@ regex = "1.11" random-string = "1.1" lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } chrono = { version = "0.4.41", features = ["serde"] } -tracing-subscriber = "0.3.19" -rand = "0.9.1" + + diff --git a/migrations/2025-07-22-195121_add_ban/down.sql b/migrations/2025-07-12-124819_message_is_edited/down.sql similarity index 51% rename from migrations/2025-07-22-195121_add_ban/down.sql rename to migrations/2025-07-12-124819_message_is_edited/down.sql index 62fe554..1a636b9 100644 --- a/migrations/2025-07-22-195121_add_ban/down.sql +++ b/migrations/2025-07-12-124819_message_is_edited/down.sql @@ -1,2 +1,2 @@ -- This file should undo anything in `up.sql` -DROP TABLE guild_bans; +ALTER TABLE messages DROP COLUMN is_edited; diff --git a/migrations/2025-07-12-124819_message_is_edited/up.sql b/migrations/2025-07-12-124819_message_is_edited/up.sql new file mode 100644 index 0000000..e600a55 --- /dev/null +++ b/migrations/2025-07-12-124819_message_is_edited/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE messages ADD COLUMN is_edited BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/migrations/2025-07-12-135941_add_channel_categories/down.sql b/migrations/2025-07-12-135941_add_channel_categories/down.sql new file mode 100644 index 0000000..1bc51d2 --- /dev/null +++ b/migrations/2025-07-12-135941_add_channel_categories/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE channels DROP COLUMN in_category; + +DROP TABLE categories; diff --git a/migrations/2025-07-12-135941_add_channel_categories/up.sql b/migrations/2025-07-12-135941_add_channel_categories/up.sql new file mode 100644 index 0000000..a2855b6 --- /dev/null +++ b/migrations/2025-07-12-135941_add_channel_categories/up.sql @@ -0,0 +1,10 @@ +-- Your SQL goes here +CREATE TABLE categories ( + uuid UUID PRIMARY KEY NOT NULL, + guild_uuid UUID NOT NULL REFERENCES guilds(uuid), + name VARCHAR(32) NOT NULL, + description VARCHAR(500) DEFAULT NULL, + is_above UUID UNIQUE REFERENCES categories(uuid) DEFAULT NULL +); + +ALTER TABLE channels ADD COLUMN in_category UUID REFERENCES categories(uuid) DEFAULT NULL; diff --git a/migrations/2025-07-13-155008_unique_guild_members/down.sql b/migrations/2025-07-13-155008_unique_guild_members/down.sql deleted file mode 100644 index 013105c..0000000 --- a/migrations/2025-07-13-155008_unique_guild_members/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE guild_members DROP CONSTRAINT guild_members_user_uuid_guild_uuid_key; diff --git a/migrations/2025-07-13-155008_unique_guild_members/up.sql b/migrations/2025-07-13-155008_unique_guild_members/up.sql deleted file mode 100644 index d139337..0000000 --- a/migrations/2025-07-13-155008_unique_guild_members/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE guild_members ADD UNIQUE (user_uuid, guild_uuid) \ No newline at end of file diff --git a/migrations/2025-07-15-002434_increase_device_name_length/down.sql b/migrations/2025-07-15-002434_increase_device_name_length/down.sql deleted file mode 100644 index 4fe6628..0000000 --- a/migrations/2025-07-15-002434_increase_device_name_length/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE refresh_tokens ALTER COLUMN device_name TYPE varchar(16); \ No newline at end of file diff --git a/migrations/2025-07-15-002434_increase_device_name_length/up.sql b/migrations/2025-07-15-002434_increase_device_name_length/up.sql deleted file mode 100644 index 9d44298..0000000 --- a/migrations/2025-07-15-002434_increase_device_name_length/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE refresh_tokens ALTER COLUMN device_name TYPE varchar(64); \ No newline at end of file diff --git a/migrations/2025-07-22-195121_add_ban/up.sql b/migrations/2025-07-22-195121_add_ban/up.sql deleted file mode 100644 index a590142..0000000 --- a/migrations/2025-07-22-195121_add_ban/up.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Your SQL goes here -CREATE TABLE guild_bans ( - guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE, - user_uuid uuid NOT NULL REFERENCES users(uuid), - reason VARCHAR(200) DEFAULT NULL, - banned_since TIMESTAMPTZ NOT NULL DEFAULT NOW(), - PRIMARY KEY (user_uuid, guild_uuid) -); diff --git a/migrations/2025-07-31-133510_roles_uuid_index/down.sql b/migrations/2025-07-31-133510_roles_uuid_index/down.sql deleted file mode 100644 index efe3f3f..0000000 --- a/migrations/2025-07-31-133510_roles_uuid_index/down.sql +++ /dev/null @@ -1,5 +0,0 @@ --- This file should undo anything in `up.sql` -DROP INDEX roles_guuid_uuid; -ALTER TABLE roles DROP CONSTRAINT roles_pkey; -CREATE UNIQUE INDEX roles_pkey ON roles (uuid, guild_uuid); -ALTER TABLE roles ADD PRIMARY KEY USING INDEX roles_pkey; diff --git a/migrations/2025-07-31-133510_roles_uuid_index/up.sql b/migrations/2025-07-31-133510_roles_uuid_index/up.sql deleted file mode 100644 index 792e7fd..0000000 --- a/migrations/2025-07-31-133510_roles_uuid_index/up.sql +++ /dev/null @@ -1,5 +0,0 @@ --- Your SQL goes here -ALTER TABLE roles DROP CONSTRAINT roles_pkey; -CREATE UNIQUE INDEX roles_pkey ON roles (uuid); -ALTER TABLE roles ADD PRIMARY KEY USING INDEX roles_pkey; -CREATE UNIQUE INDEX roles_guuid_uuid ON roles (uuid, guild_uuid); \ No newline at end of file diff --git a/migrations/2025-08-04-180235_add_status_to_user/down.sql b/migrations/2025-08-04-180235_add_status_to_user/down.sql deleted file mode 100644 index 163f7f1..0000000 --- a/migrations/2025-08-04-180235_add_status_to_user/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE users DROP COLUMN online_status; diff --git a/migrations/2025-08-04-180235_add_status_to_user/up.sql b/migrations/2025-08-04-180235_add_status_to_user/up.sql deleted file mode 100644 index ac16d77..0000000 --- a/migrations/2025-08-04-180235_add_status_to_user/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE users ADD COLUMN online_status INT2 NOT NULL DEFAULT 0; diff --git a/src/api/mod.rs b/src/api/mod.rs index 5aaa8a5..6d83e02 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,16 +1,13 @@ //! `/api` Contains the entire API -use std::sync::Arc; - -use axum::{Router, routing::get}; - -use crate::AppState; +use actix_web::Scope; +use actix_web::web; mod v1; mod versions; -pub fn router(path: &str, app_state: Arc) -> Router> { - Router::new() - .route(&format!("{path}/versions"), get(versions::versions)) - .nest(&format!("{path}/v1"), v1::router(app_state)) +pub fn web(path: &str) -> Scope { + web::scope(path.trim_end_matches('/')) + .service(v1::web()) + .service(versions::get) } diff --git a/src/api/v1/auth/devices.rs b/src/api/v1/auth/devices.rs deleted file mode 100644 index 35fe957..0000000 --- a/src/api/v1/auth/devices.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! `/api/v1/auth/devices` Returns list of logged in devices - -use std::sync::Arc; - -use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse}; -use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; -use diesel_async::RunQueryDsl; -use serde::Serialize; -use uuid::Uuid; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - schema::refresh_tokens::{self, dsl}, -}; - -#[derive(Serialize, Selectable, Queryable)] -#[diesel(table_name = refresh_tokens)] -#[diesel(check_for_backend(diesel::pg::Pg))] -struct Device { - device_name: String, - created_at: i64, -} - -/// `GET /api/v1/auth/devices` Returns list of logged in devices -/// -/// requires auth: no -/// -/// ### Response Example -/// ``` -/// json!([ -/// { -/// "device_name": "My Device!" -/// "created_at": "1752418856" -/// } -/// -/// ]); -/// ``` -pub async fn get( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let devices: Vec = dsl::refresh_tokens - .filter(dsl::uuid.eq(uuid)) - .select(Device::as_select()) - .get_results(&mut app_state.pool.get().await?) - .await?; - - Ok((StatusCode::OK, Json(devices))) -} diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index 22cc838..2faaeb4 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -1,47 +1,39 @@ -use std::{ - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; +use actix_web::{HttpResponse, post, web}; use argon2::{PasswordHash, PasswordVerifier}; -use axum::{ - Json, - extract::State, - http::{HeaderValue, StatusCode}, - response::IntoResponse, -}; use diesel::{ExpressionMethods, QueryDsl, dsl::insert_into}; use diesel_async::RunQueryDsl; use serde::Deserialize; -use super::Response; use crate::{ - AppState, + Data, error::Error, schema::*, - utils::{ - PASSWORD_REGEX, generate_device_name, generate_token, new_refresh_token_cookie, - user_uuid_from_identifier, - }, + utils::{PASSWORD_REGEX, generate_token, new_refresh_token_cookie, user_uuid_from_identifier}, }; +use super::Response; + #[derive(Deserialize)] -pub struct LoginInformation { +struct LoginInformation { username: String, password: String, + device_name: String, } +#[post("/login")] pub async fn response( - State(app_state): State>, - Json(login_information): Json, -) -> Result { + login_information: web::Json, + data: web::Data, +) -> Result { if !PASSWORD_REGEX.is_match(&login_information.password) { - return Err(Error::BadRequest("Bad password".to_string())); + return Ok(HttpResponse::Forbidden().json(r#"{ "password_hashed": false }"#)); } use users::dsl; - let mut conn = app_state.pool.get().await?; + let mut conn = data.pool.get().await?; let uuid = user_uuid_from_identifier(&mut conn, &login_information.username).await?; @@ -54,7 +46,7 @@ pub async fn response( let parsed_hash = PasswordHash::new(&database_password) .map_err(|e| Error::PasswordHashError(e.to_string()))?; - if app_state + if data .argon2 .verify_password(login_information.password.as_bytes(), &parsed_hash) .is_err() @@ -71,14 +63,12 @@ pub async fn response( use refresh_tokens::dsl as rdsl; - let device_name = generate_device_name(); - insert_into(refresh_tokens::table) .values(( rdsl::token.eq(&refresh_token), rdsl::uuid.eq(uuid), rdsl::created_at.eq(current_time), - rdsl::device_name.eq(&device_name), + rdsl::device_name.eq(&login_information.device_name), )) .execute(&mut conn) .await?; @@ -95,21 +85,7 @@ pub async fn response( .execute(&mut conn) .await?; - let mut response = ( - StatusCode::OK, - Json(Response { - access_token, - device_name, - }), - ) - .into_response(); - - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str( - &new_refresh_token_cookie(&app_state.config, refresh_token).to_string(), - )?, - ); - - Ok(response) + Ok(HttpResponse::Ok() + .cookie(new_refresh_token_cookie(&data.config, refresh_token)) + .json(Response { access_token })) } diff --git a/src/api/v1/auth/logout.rs b/src/api/v1/auth/logout.rs index 977d452..b805d91 100644 --- a/src/api/v1/auth/logout.rs +++ b/src/api/v1/auth/logout.rs @@ -1,16 +1,9 @@ -use std::sync::Arc; - -use axum::{ - extract::State, - http::{HeaderValue, StatusCode}, - response::IntoResponse, -}; -use axum_extra::extract::CookieJar; +use actix_web::{HttpRequest, HttpResponse, get, web}; use diesel::{ExpressionMethods, delete}; use diesel_async::RunQueryDsl; use crate::{ - AppState, + Data, error::Error, schema::refresh_tokens::{self, dsl}, }; @@ -27,39 +20,28 @@ use crate::{ /// /// 401 Unauthorized (no refresh token found) /// -pub async fn res( - State(app_state): State>, - jar: CookieJar, -) -> Result { - let mut refresh_token_cookie = jar - .get("refresh_token") - .ok_or(Error::Unauthorized( - "request has no refresh token".to_string(), - ))? - .to_owned(); +#[get("/logout")] +pub async fn res(req: HttpRequest, data: web::Data) -> Result { + let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized( + "request has no refresh token".to_string(), + ))?; - let refresh_token = String::from(refresh_token_cookie.value_trimmed()); + let refresh_token = String::from(refresh_token_cookie.value()); - let mut conn = app_state.pool.get().await?; + let mut conn = data.pool.get().await?; let deleted = delete(refresh_tokens::table) .filter(dsl::token.eq(refresh_token)) .execute(&mut conn) .await?; - let mut response; + refresh_token_cookie.make_removal(); if deleted == 0 { - response = StatusCode::NOT_FOUND.into_response(); - } else { - response = StatusCode::OK.into_response(); + return Ok(HttpResponse::NotFound() + .cookie(refresh_token_cookie) + .finish()); } - refresh_token_cookie.make_removal(); - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str(&refresh_token_cookie.to_string())?, - ); - - Ok(response) + Ok(HttpResponse::Ok().cookie(refresh_token_cookie).finish()) } diff --git a/src/api/v1/auth/mod.rs b/src/api/v1/auth/mod.rs index 9a72f11..75a6b0b 100644 --- a/src/api/v1/auth/mod.rs +++ b/src/api/v1/auth/mod.rs @@ -1,27 +1,13 @@ -use std::{ - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; -use axum::{ - Router, - extract::{Request, State}, - middleware::{Next, from_fn_with_state}, - response::IntoResponse, - routing::{delete, get, post}, -}; -use axum_extra::{ - TypedHeader, - headers::{Authorization, authorization::Bearer}, -}; +use actix_web::{Scope, web}; use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use serde::Serialize; use uuid::Uuid; -use crate::{AppState, Conn, error::Error, schema::access_tokens::dsl}; +use crate::{Conn, error::Error, schema::access_tokens::dsl}; -mod devices; mod login; mod logout; mod refresh; @@ -31,27 +17,21 @@ mod revoke; mod verify_email; #[derive(Serialize)] -pub struct Response { +struct Response { access_token: String, - device_name: String, } -pub fn router(app_state: Arc) -> Router> { - let router_with_auth = Router::new() - .route("/verify-email", get(verify_email::get)) - .route("/verify-email", post(verify_email::post)) - .route("/revoke", post(revoke::post)) - .route("/devices", get(devices::get)) - .layer(from_fn_with_state(app_state, CurrentUser::check_auth_layer)); - - Router::new() - .route("/register", post(register::post)) - .route("/login", post(login::response)) - .route("/logout", delete(logout::res)) - .route("/refresh", post(refresh::post)) - .route("/reset-password", get(reset_password::get)) - .route("/reset-password", post(reset_password::post)) - .merge(router_with_auth) +pub fn web() -> Scope { + web::scope("/auth") + .service(register::res) + .service(login::response) + .service(logout::res) + .service(refresh::res) + .service(revoke::res) + .service(verify_email::get) + .service(verify_email::post) + .service(reset_password::get) + .service(reset_password::post) } pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result { @@ -78,21 +58,3 @@ pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result(pub Uuid); - -impl CurrentUser { - pub async fn check_auth_layer( - State(app_state): State>, - TypedHeader(auth): TypedHeader>, - mut req: Request, - next: Next, - ) -> Result { - let current_user = - CurrentUser(check_access_token(auth.token(), &mut app_state.pool.get().await?).await?); - - req.extensions_mut().insert(current_user); - Ok(next.run(req).await) - } -} diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index 4b96226..abd9a34 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -1,21 +1,11 @@ -use axum::{ - Json, - extract::State, - http::{HeaderValue, StatusCode}, - response::IntoResponse, -}; -use axum_extra::extract::CookieJar; +use actix_web::{HttpRequest, HttpResponse, post, web}; use diesel::{ExpressionMethods, QueryDsl, delete, update}; use diesel_async::RunQueryDsl; use log::error; -use std::{ - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; -use super::Response; use crate::{ - AppState, + Data, error::Error, schema::{ access_tokens::{self, dsl}, @@ -24,22 +14,19 @@ use crate::{ utils::{generate_token, new_refresh_token_cookie}, }; -pub async fn post( - State(app_state): State>, - jar: CookieJar, -) -> Result { - let mut refresh_token_cookie = jar - .get("refresh_token") - .ok_or(Error::Unauthorized( - "request has no refresh token".to_string(), - ))? - .to_owned(); +use super::Response; - let mut refresh_token = String::from(refresh_token_cookie.value_trimmed()); +#[post("/refresh")] +pub async fn res(req: HttpRequest, data: web::Data) -> Result { + let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized( + "request has no refresh token".to_string(), + ))?; + + let mut refresh_token = String::from(refresh_token_cookie.value()); let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; - let mut conn = app_state.pool.get().await?; + let mut conn = data.pool.get().await?; if let Ok(created_at) = rdsl::refresh_tokens .filter(rdsl::token.eq(&refresh_token)) @@ -58,19 +45,14 @@ pub async fn post( error!("{error}"); } - let mut response = StatusCode::UNAUTHORIZED.into_response(); - refresh_token_cookie.make_removal(); - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str(&refresh_token_cookie.to_string())?, - ); - return Ok(response); + return Ok(HttpResponse::Unauthorized() + .cookie(refresh_token_cookie) + .finish()); } let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; - let mut device_name: String = String::new(); if lifetime > 1987200 { let new_refresh_token = generate_token::<32>()?; @@ -81,13 +63,11 @@ pub async fn post( rdsl::token.eq(&new_refresh_token), rdsl::created_at.eq(current_time), )) - .returning(rdsl::device_name) - .get_result::(&mut conn) + .execute(&mut conn) .await { - Ok(existing_device_name) => { + Ok(_) => { refresh_token = new_refresh_token; - device_name = existing_device_name; } Err(error) => { error!("{error}"); @@ -106,33 +86,14 @@ pub async fn post( .execute(&mut conn) .await?; - let mut response = ( - StatusCode::OK, - Json(Response { - access_token, - device_name, - }), - ) - .into_response(); - - // TODO: Dont set this when refresh token is unchanged - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str( - &new_refresh_token_cookie(&app_state.config, refresh_token).to_string(), - )?, - ); - - return Ok(response); + return Ok(HttpResponse::Ok() + .cookie(new_refresh_token_cookie(&data.config, refresh_token)) + .json(Response { access_token })); } - let mut response = StatusCode::UNAUTHORIZED.into_response(); - refresh_token_cookie.make_removal(); - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str(&refresh_token_cookie.to_string())?, - ); - Ok(response) + Ok(HttpResponse::Unauthorized() + .cookie(refresh_token_cookie) + .finish()) } diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index 545e5aa..e57a1ae 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -1,18 +1,10 @@ -use std::{ - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; +use actix_web::{HttpResponse, post, web}; use argon2::{ PasswordHasher, password_hash::{SaltString, rand_core::OsRng}, }; -use axum::{ - Json, - extract::State, - http::{HeaderValue, StatusCode}, - response::IntoResponse, -}; use diesel::{ExpressionMethods, dsl::insert_into}; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; @@ -20,7 +12,7 @@ use uuid::Uuid; use super::Response; use crate::{ - AppState, + Data, error::Error, objects::Member, schema::{ @@ -29,26 +21,30 @@ use crate::{ users::{self, dsl as udsl}, }, utils::{ - EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_device_name, generate_token, - new_refresh_token_cookie, + EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_token, new_refresh_token_cookie, }, }; #[derive(Deserialize)] -pub struct AccountInformation { +struct AccountInformation { identifier: String, email: String, password: String, + device_name: String, } #[derive(Serialize)] -pub struct ResponseError { +struct ResponseError { signups_enabled: bool, gorb_id_valid: bool, gorb_id_available: bool, email_valid: bool, email_available: bool, - password_strength: bool, + password_hashed: bool, + password_minimum_length: bool, + password_special_characters: bool, + password_letters: bool, + password_numbers: bool, } impl Default for ResponseError { @@ -59,16 +55,21 @@ impl Default for ResponseError { gorb_id_available: true, email_valid: true, email_available: true, - password_strength: true, + password_hashed: true, + password_minimum_length: true, + password_special_characters: true, + password_letters: true, + password_numbers: true, } } } -pub async fn post( - State(app_state): State>, - Json(account_information): Json, -) -> Result { - if !app_state.config.instance.registration { +#[post("/register")] +pub async fn res( + account_information: web::Json, + data: web::Data, +) -> Result { + if !data.config.instance.registration { return Err(Error::Forbidden( "registration is disabled on this instance".to_string(), )); @@ -77,48 +78,36 @@ pub async fn post( let uuid = Uuid::now_v7(); if !EMAIL_REGEX.is_match(&account_information.email) { - return Ok(( - StatusCode::FORBIDDEN, - Json(ResponseError { - email_valid: false, - ..Default::default() - }), - ) - .into_response()); + return Ok(HttpResponse::Forbidden().json(ResponseError { + email_valid: false, + ..Default::default() + })); } if !USERNAME_REGEX.is_match(&account_information.identifier) || account_information.identifier.len() < 3 || account_information.identifier.len() > 32 { - return Ok(( - StatusCode::FORBIDDEN, - Json(ResponseError { - gorb_id_valid: false, - ..Default::default() - }), - ) - .into_response()); + return Ok(HttpResponse::Forbidden().json(ResponseError { + gorb_id_valid: false, + ..Default::default() + })); } if !PASSWORD_REGEX.is_match(&account_information.password) { - return Ok(( - StatusCode::FORBIDDEN, - Json(ResponseError { - password_strength: false, - ..Default::default() - }), - ) - .into_response()); + return Ok(HttpResponse::Forbidden().json(ResponseError { + password_hashed: false, + ..Default::default() + })); } let salt = SaltString::generate(&mut OsRng); - if let Ok(hashed_password) = app_state + if let Ok(hashed_password) = data .argon2 .hash_password(account_information.password.as_bytes(), &salt) { - let mut conn = app_state.pool.get().await?; + let mut conn = data.pool.get().await?; // TODO: Check security of this implementation insert_into(users::table) @@ -136,14 +125,12 @@ pub async fn post( let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; - let device_name = generate_device_name(); - insert_into(refresh_tokens::table) .values(( rdsl::token.eq(&refresh_token), rdsl::uuid.eq(uuid), rdsl::created_at.eq(current_time), - rdsl::device_name.eq(&device_name), + rdsl::device_name.eq(&account_information.device_name), )) .execute(&mut conn) .await?; @@ -158,28 +145,14 @@ pub async fn post( .execute(&mut conn) .await?; - if let Some(initial_guild) = app_state.config.instance.initial_guild { - Member::new(&mut conn, &app_state.cache_pool, uuid, initial_guild).await?; + if let Some(initial_guild) = data.config.instance.initial_guild { + Member::new(&data, uuid, initial_guild).await?; } - let mut response = ( - StatusCode::OK, - Json(Response { - access_token, - device_name, - }), - ) - .into_response(); - - response.headers_mut().append( - "Set-Cookie", - HeaderValue::from_str( - &new_refresh_token_cookie(&app_state.config, refresh_token).to_string(), - )?, - ); - - return Ok(response); + return Ok(HttpResponse::Ok() + .cookie(new_refresh_token_cookie(&data.config, refresh_token)) + .json(Response { access_token })); } - Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response()) + Ok(HttpResponse::InternalServerError().finish()) } diff --git a/src/api/v1/auth/reset_password.rs b/src/api/v1/auth/reset_password.rs index 35c4b41..9a4497f 100644 --- a/src/api/v1/auth/reset_password.rs +++ b/src/api/v1/auth/reset_password.rs @@ -1,20 +1,13 @@ //! `/api/v1/auth/reset-password` Endpoints for resetting user password -use std::sync::Arc; - -use axum::{ - Json, - extract::{Query, State}, - http::StatusCode, - response::IntoResponse, -}; +use actix_web::{HttpResponse, get, post, web}; use chrono::{Duration, Utc}; use serde::Deserialize; -use crate::{AppState, error::Error, objects::PasswordResetToken}; +use crate::{Data, error::Error, objects::PasswordResetToken}; #[derive(Deserialize)] -pub struct QueryParams { +struct Query { identifier: String, } @@ -27,28 +20,17 @@ pub struct QueryParams { /// /// ### Responses /// 200 Email sent -/// /// 429 Too Many Requests -/// /// 404 Not found -/// /// 400 Bad request /// -pub async fn get( - State(app_state): State>, - query: Query, -) -> Result { - let mut conn = app_state.pool.get().await?; - - if let Ok(password_reset_token) = PasswordResetToken::get_with_identifier( - &mut conn, - &app_state.cache_pool, - query.identifier.clone(), - ) - .await +#[get("/reset-password")] +pub async fn get(query: web::Query, data: web::Data) -> Result { + if let Ok(password_reset_token) = + PasswordResetToken::get_with_identifier(&data, query.identifier.clone()).await { if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) { - password_reset_token.delete(&app_state.cache_pool).await?; + password_reset_token.delete(&data).await?; } else { return Err(Error::TooManyRequests( "Please allow 1 hour before sending a new email".to_string(), @@ -56,13 +38,13 @@ pub async fn get( } } - PasswordResetToken::new(&mut conn, &app_state, query.identifier.clone()).await?; + PasswordResetToken::new(&data, query.identifier.clone()).await?; - Ok(StatusCode::OK) + Ok(HttpResponse::Ok().finish()) } #[derive(Deserialize)] -pub struct ResetPassword { +struct ResetPassword { password: String, token: String, } @@ -81,27 +63,20 @@ pub struct ResetPassword { /// /// ### Responses /// 200 Success -/// /// 410 Token Expired -/// /// 404 Not Found -/// /// 400 Bad Request /// +#[post("/reset-password")] pub async fn post( - State(app_state): State>, - reset_password: Json, -) -> Result { - let password_reset_token = - PasswordResetToken::get(&app_state.cache_pool, reset_password.token.clone()).await?; + reset_password: web::Json, + data: web::Data, +) -> Result { + let password_reset_token = PasswordResetToken::get(&data, reset_password.token.clone()).await?; password_reset_token - .set_password( - &mut app_state.pool.get().await?, - &app_state, - reset_password.password.clone(), - ) + .set_password(&data, reset_password.password.clone()) .await?; - Ok(StatusCode::OK) + Ok(HttpResponse::Ok().finish()) } diff --git a/src/api/v1/auth/revoke.rs b/src/api/v1/auth/revoke.rs index 90b96ae..2e95884 100644 --- a/src/api/v1/auth/revoke.rs +++ b/src/api/v1/auth/revoke.rs @@ -1,35 +1,38 @@ -use std::sync::Arc; - +use actix_web::{HttpRequest, HttpResponse, post, web}; use argon2::{PasswordHash, PasswordVerifier}; -use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse}; use diesel::{ExpressionMethods, QueryDsl, delete}; use diesel_async::RunQueryDsl; use serde::Deserialize; -use uuid::Uuid; use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, - schema::{ - refresh_tokens::{self, dsl as rdsl}, - users::dsl as udsl, - }, + schema::refresh_tokens::{self, dsl as rdsl}, + schema::users::dsl as udsl, + utils::get_auth_header, }; #[derive(Deserialize)] -pub struct RevokeRequest { +struct RevokeRequest { password: String, device_name: String, } // TODO: Should maybe be a delete request? -pub async fn post( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, - Json(revoke_request): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; +#[post("/revoke")] +pub async fn res( + req: HttpRequest, + revoke_request: web::Json, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; let database_password: String = udsl::users .filter(udsl::uuid.eq(uuid)) @@ -40,7 +43,7 @@ pub async fn post( let hashed_password = PasswordHash::new(&database_password) .map_err(|e| Error::PasswordHashError(e.to_string()))?; - if app_state + if data .argon2 .verify_password(revoke_request.password.as_bytes(), &hashed_password) .is_err() @@ -56,5 +59,5 @@ pub async fn post( .execute(&mut conn) .await?; - Ok(StatusCode::OK) + Ok(HttpResponse::Ok().finish()) } diff --git a/src/api/v1/auth/verify_email.rs b/src/api/v1/auth/verify_email.rs index 1cb8aef..6b895aa 100644 --- a/src/api/v1/auth/verify_email.rs +++ b/src/api/v1/auth/verify_email.rs @@ -1,26 +1,19 @@ //! `/api/v1/auth/verify-email` Endpoints for verifying user emails -use std::sync::Arc; - -use axum::{ - Extension, - extract::{Query, State}, - http::StatusCode, - response::IntoResponse, -}; +use actix_web::{HttpRequest, HttpResponse, get, post, web}; use chrono::{Duration, Utc}; use serde::Deserialize; -use uuid::Uuid; use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, objects::{EmailToken, Me}, + utils::get_auth_header, }; #[derive(Deserialize)] -pub struct QueryParams { +struct Query { token: String, } @@ -42,30 +35,37 @@ pub struct QueryParams { /// /// 401 Unauthorized /// +#[get("/verify-email")] pub async fn get( - State(app_state): State>, - Query(query): Query, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + query: web::Query, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; let me = Me::get(&mut conn, uuid).await?; if me.email_verified { - return Ok(StatusCode::NO_CONTENT); + return Ok(HttpResponse::NoContent().finish()); } - let email_token = EmailToken::get(&app_state.cache_pool, me.uuid).await?; + let email_token = EmailToken::get(&data, me.uuid).await?; if query.token != email_token.token { - return Ok(StatusCode::UNAUTHORIZED); + return Ok(HttpResponse::Unauthorized().finish()); } me.verify_email(&mut conn).await?; - email_token.delete(&app_state.cache_pool).await?; + email_token.delete(&data).await?; - Ok(StatusCode::OK) + Ok(HttpResponse::Ok().finish()) } /// `POST /api/v1/auth/verify-email` Sends user verification email @@ -81,19 +81,25 @@ pub async fn get( /// /// 401 Unauthorized /// -pub async fn post( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; +#[post("/verify-email")] +pub async fn post(req: HttpRequest, data: web::Data) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + let me = Me::get(&mut conn, uuid).await?; if me.email_verified { - return Ok(StatusCode::NO_CONTENT); + return Ok(HttpResponse::NoContent().finish()); } - if let Ok(email_token) = EmailToken::get(&app_state.cache_pool, me.uuid).await { + if let Ok(email_token) = EmailToken::get(&data, me.uuid).await { if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(1) { - email_token.delete(&app_state.cache_pool).await?; + email_token.delete(&data).await?; } else { return Err(Error::TooManyRequests( "Please allow 1 hour before sending a new email".to_string(), @@ -101,7 +107,7 @@ pub async fn post( } } - EmailToken::new(&app_state, me).await?; + EmailToken::new(&data, me).await?; - Ok(StatusCode::OK) + Ok(HttpResponse::Ok().finish()) } diff --git a/src/api/v1/channels/mod.rs b/src/api/v1/channels/mod.rs index fa90ccd..e9558c9 100644 --- a/src/api/v1/channels/mod.rs +++ b/src/api/v1/channels/mod.rs @@ -1,25 +1,12 @@ -use std::sync::Arc; - -use axum::{ - Router, - middleware::from_fn_with_state, - routing::{any, delete, get, patch}, -}; -//use socketioxide::SocketIo; - -use crate::{AppState, api::v1::auth::CurrentUser}; +use actix_web::{Scope, web}; mod uuid; -pub fn router(app_state: Arc) -> Router> { - let router_with_auth = Router::new() - .route("/{uuid}", get(uuid::get)) - .route("/{uuid}", delete(uuid::delete)) - .route("/{uuid}", patch(uuid::patch)) - .route("/{uuid}/messages", get(uuid::messages::get)) - .layer(from_fn_with_state(app_state, CurrentUser::check_auth_layer)); - - Router::new() - .route("/{uuid}/socket", any(uuid::socket::ws)) - .merge(router_with_auth) +pub fn web() -> Scope { + web::scope("/channels") + .service(uuid::get) + .service(uuid::delete) + .service(uuid::patch) + .service(uuid::messages::get) + .service(uuid::socket::ws) } diff --git a/src/api/v1/channels/uuid/messages.rs b/src/api/v1/channels/uuid/messages.rs index 1f9010d..9fdea0b 100644 --- a/src/api/v1/channels/uuid/messages.rs +++ b/src/api/v1/channels/uuid/messages.rs @@ -1,25 +1,18 @@ //! `/api/v1/channels/{uuid}/messages` Endpoints related to channel messages -use std::sync::Arc; - use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, objects::{Channel, Member}, - utils::global_checks, + utils::{get_auth_header, global_checks}, }; use ::uuid::Uuid; -use axum::{ - Extension, Json, - extract::{Path, Query, State}, - http::StatusCode, - response::IntoResponse, -}; +use actix_web::{HttpRequest, HttpResponse, get, web}; use serde::Deserialize; #[derive(Deserialize)] -pub struct MessageRequest { +struct MessageRequest { amount: i64, offset: i64, } @@ -54,28 +47,32 @@ pub struct MessageRequest { /// }); /// ``` /// +#[get("/{uuid}/messages")] pub async fn get( - State(app_state): State>, - Path(channel_uuid): Path, - Query(message_request): Query, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(Uuid,)>, + message_request: web::Query, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; - let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; + let channel_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + let channel = Channel::fetch_one(&data, channel_uuid).await?; Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; let messages = channel - .fetch_messages( - &mut conn, - &app_state.cache_pool, - message_request.amount, - message_request.offset, - ) + .fetch_messages(&data, message_request.amount, message_request.offset) .await?; - Ok((StatusCode::OK, Json(messages))) + Ok(HttpResponse::Ok().json(messages)) } diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index f5566b3..d7cfa39 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -3,65 +3,75 @@ pub mod messages; pub mod socket; -use std::sync::Arc; - use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, - utils::global_checks, + utils::{get_auth_header, global_checks}, }; -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; - +use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; use serde::Deserialize; use uuid::Uuid; +#[get("/{uuid}")] pub async fn get( - State(app_state): State>, - Path(channel_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; - let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; + let channel_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + let channel = Channel::fetch_one(&data, channel_uuid).await?; Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; - Ok((StatusCode::OK, Json(channel))) + Ok(HttpResponse::Ok().json(channel)) } +#[delete("/{uuid}")] pub async fn delete( - State(app_state): State>, - Path(channel_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; - let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; + let channel_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + let channel = Channel::fetch_one(&data, channel_uuid).await?; let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; member - .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageChannel) + .check_permission(&data, Permissions::DeleteChannel) .await?; - channel.delete(&mut conn, &app_state.cache_pool).await?; + channel.delete(&data).await?; - Ok(StatusCode::OK) + Ok(HttpResponse::Ok().finish()) } #[derive(Deserialize)] -pub struct NewInfo { +struct NewInfo { name: Option, description: Option, is_above: Option, @@ -98,45 +108,48 @@ pub struct NewInfo { /// }); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps +#[patch("/{uuid}")] pub async fn patch( - State(app_state): State>, - Path(channel_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, - Json(new_info): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(Uuid,)>, + new_info: web::Json, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; - let mut channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; + let channel_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + let mut channel = Channel::fetch_one(&data, channel_uuid).await?; let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; member - .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageChannel) + .check_permission(&data, Permissions::ManageChannel) .await?; if let Some(new_name) = &new_info.name { - channel - .set_name(&mut conn, &app_state.cache_pool, new_name.to_string()) - .await?; + channel.set_name(&data, new_name.to_string()).await?; } if let Some(new_description) = &new_info.description { channel - .set_description( - &mut conn, - &app_state.cache_pool, - new_description.to_string(), - ) + .set_description(&data, new_description.to_string()) .await?; } if let Some(new_is_above) = &new_info.is_above { channel - .set_description(&mut conn, &app_state.cache_pool, new_is_above.to_string()) + .set_description(&data, new_is_above.to_string()) .await?; } - Ok((StatusCode::OK, Json(channel))) + Ok(HttpResponse::Ok().json(channel)) } diff --git a/src/api/v1/channels/uuid/socket.rs b/src/api/v1/channels/uuid/socket.rs index ac04301..7233f39 100644 --- a/src/api/v1/channels/uuid/socket.rs +++ b/src/api/v1/channels/uuid/socket.rs @@ -1,20 +1,18 @@ -use std::sync::Arc; - -use axum::{ - extract::{Path, State, WebSocketUpgrade, ws::Message}, - http::HeaderMap, - response::IntoResponse, +use actix_web::{ + Error, HttpRequest, HttpResponse, get, + http::header::{HeaderValue, SEC_WEBSOCKET_PROTOCOL}, + rt, web, }; -use futures_util::{SinkExt, StreamExt}; +use actix_ws::AggregatedMessage; +use futures_util::StreamExt as _; use serde::Deserialize; use uuid::Uuid; use crate::{ - AppState, + Data, api::v1::auth::check_access_token, - error::Error, objects::{Channel, Member}, - utils::global_checks, + utils::{get_ws_protocol_header, global_checks}, }; #[derive(Deserialize)] @@ -23,115 +21,100 @@ struct MessageBody { reply_to: Option, } +#[get("/{uuid}/socket")] pub async fn ws( - ws: WebSocketUpgrade, - State(app_state): State>, - Path(channel_uuid): Path, - headers: HeaderMap, -) -> Result { + req: HttpRequest, + path: web::Path<(Uuid,)>, + stream: web::Payload, + data: web::Data, +) -> Result { + // Get all headers + let headers = req.headers(); + // Retrieve auth header - let auth_token = headers.get(axum::http::header::SEC_WEBSOCKET_PROTOCOL); + let auth_header = get_ws_protocol_header(headers)?; - if auth_token.is_none() { - return Err(Error::Unauthorized( - "No authorization header provided".to_string(), - )); - } + // Get uuid from path + let channel_uuid = path.into_inner().0; - let auth_raw = auth_token.unwrap().to_str()?; - - let mut auth = auth_raw.split_whitespace(); - - let response_proto = auth.next(); - - let auth_value = auth.next(); - - if response_proto.is_none() { - return Err(Error::BadRequest( - "Sec-WebSocket-Protocol header is empty".to_string(), - )); - } else if response_proto.is_some_and(|rp| rp != "Authorization,") { - return Err(Error::BadRequest( - "First protocol should be Authorization".to_string(), - )); - } - - if auth_value.is_none() { - return Err(Error::BadRequest("No token provided".to_string())); - } - - let auth_header = auth_value.unwrap(); - - let mut conn = app_state - .pool - .get() - .await - .map_err(crate::error::Error::from)?; + let mut conn = data.pool.get().await.map_err(crate::error::Error::from)?; // Authorize client using auth header let uuid = check_access_token(auth_header, &mut conn).await?; - global_checks(&mut conn, &app_state.config, uuid).await?; + global_checks(&data, uuid).await?; - let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?; + let channel = Channel::fetch_one(&data, channel_uuid).await?; Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; - let mut pubsub = app_state + let (mut res, mut session_1, stream) = actix_ws::handle(&req, stream)?; + + let mut stream = stream + .aggregate_continuations() + // aggregate continuation frames up to 1MiB + .max_continuation_size(2_usize.pow(20)); + + let mut pubsub = data .cache_pool .get_async_pubsub() .await .map_err(crate::error::Error::from)?; - let mut res = ws.on_upgrade(async move |socket| { - let (mut sender, mut receiver) = socket.split(); + let mut session_2 = session_1.clone(); - tokio::spawn(async move { - pubsub.subscribe(channel_uuid.to_string()).await?; - while let Some(msg) = pubsub.on_message().next().await { - let payload: String = msg.get_payload()?; - sender.send(payload.into()).await?; - } + rt::spawn(async move { + pubsub.subscribe(channel_uuid.to_string()).await?; + while let Some(msg) = pubsub.on_message().next().await { + let payload: String = msg.get_payload()?; + session_1.text(payload).await?; + } - Ok::<(), crate::error::Error>(()) - }); + Ok::<(), crate::error::Error>(()) + }); + + // start task but don't wait for it + rt::spawn(async move { + // receive messages from websocket + while let Some(msg) = stream.next().await { + match msg { + Ok(AggregatedMessage::Text(text)) => { + let mut conn = data.cache_pool.get_multiplexed_tokio_connection().await?; - tokio::spawn(async move { - while let Some(msg) = receiver.next().await { - if let Ok(Message::Text(text)) = msg { let message_body: MessageBody = serde_json::from_str(&text)?; let message = channel - .new_message( - &mut conn, - &app_state.cache_pool, - uuid, - message_body.message, - message_body.reply_to, - ) + .new_message(&data, uuid, message_body.message, message_body.reply_to) .await?; redis::cmd("PUBLISH") .arg(&[channel_uuid.to_string(), serde_json::to_string(&message)?]) - .exec_async( - &mut app_state - .cache_pool - .get_multiplexed_tokio_connection() - .await?, - ) + .exec_async(&mut conn) .await?; } - } - Ok::<(), crate::error::Error>(()) - }); + Ok(AggregatedMessage::Binary(bin)) => { + // echo binary message + session_2.binary(bin).await?; + } + + Ok(AggregatedMessage::Ping(msg)) => { + // respond to PING frame with PONG frame + session_2.pong(&msg).await?; + } + + _ => {} + } + } + + Ok::<(), crate::error::Error>(()) }); let headers = res.headers_mut(); headers.append( - axum::http::header::SEC_WEBSOCKET_PROTOCOL, - "Authorization".parse()?, + SEC_WEBSOCKET_PROTOCOL, + HeaderValue::from_str("Authorization")?, ); // respond immediately with response connected to WS session diff --git a/src/api/v1/guilds/mod.rs b/src/api/v1/guilds/mod.rs index 5b9f089..ada5dc8 100644 --- a/src/api/v1/guilds/mod.rs +++ b/src/api/v1/guilds/mod.rs @@ -1,37 +1,28 @@ //! `/api/v1/guilds` Guild related endpoints -use std::sync::Arc; - -use ::uuid::Uuid; -use axum::{ - Extension, Json, Router, - extract::State, - http::StatusCode, - response::IntoResponse, - routing::{get, post}, -}; +use actix_web::{HttpRequest, HttpResponse, Scope, get, post, web}; use serde::Deserialize; mod uuid; use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, objects::{Guild, StartAmountQuery}, - utils::global_checks, + utils::{get_auth_header, global_checks}, }; #[derive(Deserialize)] -pub struct GuildInfo { +struct GuildInfo { name: String, } -pub fn router() -> Router> { - Router::new() - .route("/", post(new)) - .route("/", get(get_guilds)) - .nest("/{uuid}", uuid::router()) +pub fn web() -> Scope { + web::scope("/guilds") + .service(post) + .service(get) + .service(uuid::web()) } /// `POST /api/v1/guilds` Creates a new guild @@ -58,19 +49,23 @@ pub fn router() -> Router> { /// }); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -pub async fn new( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, - Json(guild_info): Json, -) -> Result { - let guild = Guild::new( - &mut app_state.pool.get().await?, - guild_info.name.clone(), - uuid, - ) - .await?; +#[post("")] +pub async fn post( + req: HttpRequest, + guild_info: web::Json, + data: web::Data, +) -> Result { + let headers = req.headers(); - Ok((StatusCode::OK, Json(guild))) + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + let guild = Guild::new(&mut conn, guild_info.name.clone(), uuid).await?; + + Ok(HttpResponse::Ok().json(guild)) } /// `GET /api/v1/servers` Fetches all guilds @@ -120,19 +115,25 @@ pub async fn new( /// ]); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -pub async fn get_guilds( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, - Json(request_query): Json, -) -> Result { +#[get("")] +pub async fn get( + req: HttpRequest, + request_query: web::Query, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + let start = request_query.start.unwrap_or(0); + let amount = request_query.amount.unwrap_or(10); - let mut conn = app_state.pool.get().await?; + let uuid = check_access_token(auth_header, &mut data.pool.get().await?).await?; - global_checks(&mut conn, &app_state.config, uuid).await?; + global_checks(&data, uuid).await?; - let guilds = Guild::fetch_amount(&mut conn, start, amount).await?; + let guilds = Guild::fetch_amount(&data.pool, start, amount).await?; - Ok((StatusCode::OK, Json(guilds))) + Ok(HttpResponse::Ok().json(guilds)) } diff --git a/src/api/v1/guilds/uuid/bans.rs b/src/api/v1/guilds/uuid/bans.rs deleted file mode 100644 index 2e31a59..0000000 --- a/src/api/v1/guilds/uuid/bans.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::sync::Arc; - -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use uuid::Uuid; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{GuildBan, Member, Permissions}, - utils::global_checks, -}; - -pub async fn get( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let caller = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - caller - .check_permission(&mut conn, &app_state.cache_pool, Permissions::BanMember) - .await?; - - let all_guild_bans = GuildBan::fetch_all(&mut conn, guild_uuid).await?; - - Ok((StatusCode::OK, Json(all_guild_bans))) -} - -pub async fn unban( - State(app_state): State>, - Path((guild_uuid, user_uuid)): Path<(Uuid, Uuid)>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let caller = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - caller - .check_permission(&mut conn, &app_state.cache_pool, Permissions::BanMember) - .await?; - - let ban = GuildBan::fetch_one(&mut conn, guild_uuid, user_uuid).await?; - - ban.unban(&mut conn).await?; - - Ok(StatusCode::OK) -} diff --git a/src/api/v1/guilds/uuid/categories.rs b/src/api/v1/guilds/uuid/categories.rs new file mode 100644 index 0000000..3c52925 --- /dev/null +++ b/src/api/v1/guilds/uuid/categories.rs @@ -0,0 +1,92 @@ +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + objects::{Channel, Member, Permissions}, + utils::{get_auth_header, global_checks, order_by_is_above}, +}; +use ::uuid::Uuid; +use actix_web::{HttpRequest, HttpResponse, get, post, web}; +use serde::Deserialize; + +#[derive(Deserialize)] +struct ChannelInfo { + name: String, + description: Option, +} + +#[get("{uuid}/categories")] +pub async fn get( + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + Member::check_membership(&mut conn, uuid, guild_uuid).await?; + + if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_channels")).await { + return Ok(HttpResponse::Ok() + .content_type("application/json") + .body(cache_hit)); + } + + let channels = Channel::fetch_all(&data.pool, guild_uuid).await?; + + let channels_ordered = order_by_is_above(channels).await?; + + data.set_cache_key( + format!("{guild_uuid}_channels"), + channels_ordered.clone(), + 1800, + ) + .await?; + + Ok(HttpResponse::Ok().json(channels_ordered)) +} + +#[post("{uuid}/categories")] +pub async fn create( + req: HttpRequest, + channel_info: web::Json, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; + + member + .check_permission(&data, Permissions::CreateChannel) + .await?; + + let channel = Channel::new( + data.clone(), + guild_uuid, + channel_info.name.clone(), + channel_info.description.clone(), + ) + .await?; + + Ok(HttpResponse::Ok().json(channel)) +} diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index 1cd7f78..b9f91cb 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -1,87 +1,92 @@ -use std::sync::Arc; - -use ::uuid::Uuid; -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use serde::Deserialize; - use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, - utils::{CacheFns, global_checks, order_by_is_above}, + utils::{get_auth_header, global_checks, order_by_is_above}, }; +use ::uuid::Uuid; +use actix_web::{HttpRequest, HttpResponse, get, post, web}; +use serde::Deserialize; #[derive(Deserialize)] -pub struct ChannelInfo { +struct ChannelInfo { name: String, description: Option, } +#[get("{uuid}/channels")] pub async fn get( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?; - if let Ok(cache_hit) = app_state - .cache_pool - .get_cache_key::>(format!("{guild_uuid}_channels")) - .await - { - return Ok((StatusCode::OK, Json(cache_hit)).into_response()); + if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_channels")).await { + return Ok(HttpResponse::Ok() + .content_type("application/json") + .body(cache_hit)); } - let channels = Channel::fetch_all(&mut conn, guild_uuid).await?; + let channels = Channel::fetch_all(&data.pool, guild_uuid).await?; let channels_ordered = order_by_is_above(channels).await?; - app_state - .cache_pool - .set_cache_key( - format!("{guild_uuid}_channels"), - channels_ordered.clone(), - 1800, - ) - .await?; + data.set_cache_key( + format!("{guild_uuid}_channels"), + channels_ordered.clone(), + 1800, + ) + .await?; - Ok((StatusCode::OK, Json(channels_ordered)).into_response()) + Ok(HttpResponse::Ok().json(channels_ordered)) } +#[post("{uuid}/channels")] pub async fn create( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, - Json(channel_info): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + channel_info: web::Json, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member - .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageChannel) + .check_permission(&data, Permissions::ManageChannel) .await?; let channel = Channel::new( - &mut conn, - &app_state.cache_pool, + data.clone(), guild_uuid, channel_info.name.clone(), channel_info.description.clone(), ) .await?; - Ok((StatusCode::OK, Json(channel))) + Ok(HttpResponse::Ok().json(channel)) } diff --git a/src/api/v1/guilds/uuid/icon.rs b/src/api/v1/guilds/uuid/icon.rs new file mode 100644 index 0000000..600ccba --- /dev/null +++ b/src/api/v1/guilds/uuid/icon.rs @@ -0,0 +1,62 @@ +//! `/api/v1/guilds/{uuid}/icon` icon related endpoints, will probably be replaced by a multipart post to above endpoint + +use actix_web::{HttpRequest, HttpResponse, put, web}; +use futures_util::StreamExt as _; +use uuid::Uuid; + +use crate::{ + Data, + api::v1::auth::check_access_token, + error::Error, + objects::{Guild, Member, Permissions}, + utils::{get_auth_header, global_checks}, +}; + +/// `PUT /api/v1/guilds/{uuid}/icon` Icon upload +/// +/// requires auth: no +/// +/// put request expects a file and nothing else +#[put("{uuid}/icon")] +pub async fn upload( + req: HttpRequest, + path: web::Path<(Uuid,)>, + mut payload: web::Payload, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; + + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; + + member + .check_permission(&data, Permissions::ManageGuild) + .await?; + + let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?; + + let mut bytes = web::BytesMut::new(); + while let Some(item) = payload.next().await { + bytes.extend_from_slice(&item?); + } + + guild + .set_icon( + &data.bunny_storage, + &mut conn, + data.config.bunny.cdn_url.clone(), + bytes, + ) + .await?; + + Ok(HttpResponse::Ok().finish()) +} diff --git a/src/api/v1/guilds/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs index fa06f44..f1c62bc 100644 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ b/src/api/v1/guilds/uuid/invites/mod.rs @@ -1,35 +1,37 @@ -use std::sync::Arc; - -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; +use actix_web::{HttpRequest, HttpResponse, get, post, web}; use serde::Deserialize; use uuid::Uuid; use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, objects::{Guild, Member, Permissions}, - utils::global_checks, + utils::{get_auth_header, global_checks}, }; #[derive(Deserialize)] -pub struct InviteRequest { +struct InviteRequest { custom_id: Option, } +#[get("{uuid}/invites")] pub async fn get( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?; @@ -37,23 +39,32 @@ pub async fn get( let invites = guild.get_invites(&mut conn).await?; - Ok((StatusCode::OK, Json(invites))) + Ok(HttpResponse::Ok().json(invites)) } +#[post("{uuid}/invites")] pub async fn create( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, - Json(invite_request): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(Uuid,)>, + invite_request: web::Json, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member - .check_permission(&mut conn, &app_state.cache_pool, Permissions::CreateInvite) + .check_permission(&data, Permissions::CreateInvite) .await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; @@ -62,5 +73,5 @@ pub async fn create( .create_invite(&mut conn, uuid, invite_request.custom_id.clone()) .await?; - Ok((StatusCode::OK, Json(invite))) + Ok(HttpResponse::Ok().json(invite)) } diff --git a/src/api/v1/guilds/uuid/members.rs b/src/api/v1/guilds/uuid/members.rs index 0e1d2bc..0afc2c5 100644 --- a/src/api/v1/guilds/uuid/members.rs +++ b/src/api/v1/guilds/uuid/members.rs @@ -1,43 +1,36 @@ -use std::sync::Arc; - -use ::uuid::Uuid; -use axum::{ - Extension, Json, - extract::{Path, Query, State}, - http::StatusCode, - response::IntoResponse, -}; - use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, - objects::{Me, Member, PaginationRequest}, - utils::global_checks, + objects::{Me, Member}, + utils::{get_auth_header, global_checks}, }; +use ::uuid::Uuid; +use actix_web::{HttpRequest, HttpResponse, get, web}; +#[get("{uuid}/members")] pub async fn get( - State(app_state): State>, - Path(guild_uuid): Path, - Query(pagination): Query, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?; let me = Me::get(&mut conn, uuid).await?; - let members = Member::fetch_page( - &mut conn, - &app_state.cache_pool, - &me, - guild_uuid, - pagination, - ) - .await?; + let members = Member::fetch_all(&data, &me, guild_uuid).await?; - Ok((StatusCode::OK, Json(members))) + Ok(HttpResponse::Ok().json(members)) } diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index 53f469b..de89c2e 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -1,51 +1,44 @@ //! `/api/v1/guilds/{uuid}` Specific server endpoints -use std::sync::Arc; - -use axum::{ - Extension, Json, Router, - extract::{Multipart, Path, State}, - http::StatusCode, - response::IntoResponse, - routing::{delete, get, patch, post}, -}; -use bytes::Bytes; +use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use uuid::Uuid; -mod bans; mod channels; +mod icon; mod invites; mod members; mod roles; +mod categories; use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, - objects::{Guild, Member, Permissions}, - utils::global_checks, + objects::{Guild, Member}, + utils::{get_auth_header, global_checks}, }; -pub fn router() -> Router> { - Router::new() +pub fn web() -> Scope { + web::scope("") // Servers - .route("/", get(get_guild)) - .route("/", patch(edit)) + .service(get) + // Categories + .service(categories::get) + .service(categories::create) // Channels - .route("/channels", get(channels::get)) - .route("/channels", post(channels::create)) + .service(channels::get) + .service(channels::create) // Roles - .route("/roles", get(roles::get)) - .route("/roles", post(roles::create)) - .route("/roles/{role_uuid}", get(roles::uuid::get)) + .service(roles::get) + .service(roles::create) + .service(roles::uuid::get) // Invites - .route("/invites", get(invites::get)) - .route("/invites", post(invites::create)) + .service(invites::get) + .service(invites::create) + // Icon + .service(icon::upload) // Members - .route("/members", get(members::get)) - // Bans - .route("/bans", get(bans::get)) - .route("/bans/{uuid}", delete(bans::unban)) + .service(members::get) } /// `GET /api/v1/guilds/{uuid}` DESCRIPTION @@ -81,58 +74,27 @@ pub fn router() -> Router> { /// "member_count": 20 /// }); /// ``` -pub async fn get_guild( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; +#[get("/{uuid}")] +pub async fn get( + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; - Ok((StatusCode::OK, Json(guild))) -} - -/// `PATCH /api/v1/guilds/{uuid}` change guild settings -/// -/// requires auth: yes -pub async fn edit( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, - mut multipart: Multipart, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - member - .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageGuild) - .await?; - - let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?; - - let mut icon: Option = None; - - while let Some(field) = multipart.next_field().await.unwrap() { - let name = field - .name() - .ok_or(Error::BadRequest("Field has no name".to_string()))?; - - if name == "icon" { - icon = Some(field.bytes().await?); - } - } - - if let Some(icon) = icon { - guild.set_icon(&mut conn, &app_state, icon).await?; - } - - Ok(StatusCode::OK) + Ok(HttpResponse::Ok().json(guild)) } diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index d3660ce..0fcc5b3 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -1,77 +1,82 @@ -use std::sync::Arc; - use ::uuid::Uuid; -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; +use actix_web::{HttpRequest, HttpResponse, get, post, web}; use serde::Deserialize; use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, objects::{Member, Permissions, Role}, - utils::{CacheFns, global_checks, order_by_is_above}, + utils::{get_auth_header, global_checks, order_by_is_above}, }; pub mod uuid; #[derive(Deserialize)] -pub struct RoleInfo { +struct RoleInfo { name: String, } +#[get("{uuid}/roles")] pub async fn get( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?; - if let Ok(cache_hit) = app_state - .cache_pool - .get_cache_key::>(format!("{guild_uuid}_roles")) - .await - { - return Ok((StatusCode::OK, Json(cache_hit)).into_response()); + if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_roles")).await { + return Ok(HttpResponse::Ok() + .content_type("application/json") + .body(cache_hit)); } let roles = Role::fetch_all(&mut conn, guild_uuid).await?; let roles_ordered = order_by_is_above(roles).await?; - app_state - .cache_pool - .set_cache_key(format!("{guild_uuid}_roles"), roles_ordered.clone(), 1800) + data.set_cache_key(format!("{guild_uuid}_roles"), roles_ordered.clone(), 1800) .await?; - Ok((StatusCode::OK, Json(roles_ordered)).into_response()) + Ok(HttpResponse::Ok().json(roles_ordered)) } +#[post("{uuid}/roles")] pub async fn create( - State(app_state): State>, - Path(guild_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, - Json(role_info): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + role_info: web::Json, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let guild_uuid = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; member - .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageRole) + .check_permission(&data, Permissions::ManageRole) .await?; let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?; - Ok((StatusCode::OK, Json(role)).into_response()) + Ok(HttpResponse::Ok().json(role)) } diff --git a/src/api/v1/guilds/uuid/roles/uuid.rs b/src/api/v1/guilds/uuid/roles/uuid.rs index e7890d0..bd747d8 100644 --- a/src/api/v1/guilds/uuid/roles/uuid.rs +++ b/src/api/v1/guilds/uuid/roles/uuid.rs @@ -1,46 +1,43 @@ -use std::sync::Arc; - -use ::uuid::Uuid; -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; - use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, objects::{Member, Role}, - utils::{CacheFns, global_checks}, + utils::{get_auth_header, global_checks}, }; +use ::uuid::Uuid; +use actix_web::{HttpRequest, HttpResponse, get, web}; +#[get("{uuid}/roles/{role_uuid}")] pub async fn get( - State(app_state): State>, - Path((guild_uuid, role_uuid)): Path<(Uuid, Uuid)>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(Uuid, Uuid)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let (guild_uuid, role_uuid) = path.into_inner(); + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?; - if let Ok(cache_hit) = app_state - .cache_pool - .get_cache_key::(format!("{role_uuid}")) - .await - { - return Ok((StatusCode::OK, Json(cache_hit)).into_response()); + if let Ok(cache_hit) = data.get_cache_key(format!("{role_uuid}")).await { + return Ok(HttpResponse::Ok() + .content_type("application/json") + .body(cache_hit)); } let role = Role::fetch_one(&mut conn, role_uuid).await?; - app_state - .cache_pool - .set_cache_key(format!("{role_uuid}"), role.clone(), 60) + data.set_cache_key(format!("{role_uuid}"), role.clone(), 60) .await?; - Ok((StatusCode::OK, Json(role)).into_response()) + Ok(HttpResponse::Ok().json(role)) } diff --git a/src/api/v1/invites/id.rs b/src/api/v1/invites/id.rs index 99f177f..22e2868 100644 --- a/src/api/v1/invites/id.rs +++ b/src/api/v1/invites/id.rs @@ -1,48 +1,49 @@ -use std::sync::Arc; - -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use uuid::Uuid; +use actix_web::{HttpRequest, HttpResponse, get, post, web}; use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, objects::{Guild, Invite, Member}, - utils::global_checks, + utils::{get_auth_header, global_checks}, }; -pub async fn get( - State(app_state): State>, - Path(invite_id): Path, -) -> Result { - let mut conn = app_state.pool.get().await?; +#[get("{id}")] +pub async fn get(path: web::Path<(String,)>, data: web::Data) -> Result { + let mut conn = data.pool.get().await?; + + let invite_id = path.into_inner().0; let invite = Invite::fetch_one(&mut conn, invite_id).await?; let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; - Ok((StatusCode::OK, Json(guild))) + Ok(HttpResponse::Ok().json(guild)) } +#[post("{id}")] pub async fn join( - State(app_state): State>, - Path(invite_id): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(String,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let invite_id = path.into_inner().0; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; let invite = Invite::fetch_one(&mut conn, invite_id).await?; let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; - Member::new(&mut conn, &app_state.cache_pool, uuid, guild.uuid).await?; + Member::new(&data, uuid, guild.uuid).await?; - Ok((StatusCode::OK, Json(guild))) + Ok(HttpResponse::Ok().json(guild)) } diff --git a/src/api/v1/invites/mod.rs b/src/api/v1/invites/mod.rs index 50fb707..3714a83 100644 --- a/src/api/v1/invites/mod.rs +++ b/src/api/v1/invites/mod.rs @@ -1,16 +1,7 @@ -use std::sync::Arc; - -use axum::{ - Router, - routing::{get, post}, -}; - -use crate::AppState; +use actix_web::{Scope, web}; mod id; -pub fn router() -> Router> { - Router::new() - .route("/{id}", get(id::get)) - .route("/{id}", post(id::join)) +pub fn web() -> Scope { + web::scope("/invites").service(id::get).service(id::join) } diff --git a/src/api/v1/me/friends/mod.rs b/src/api/v1/me/friends/mod.rs index 904a1f5..f946616 100644 --- a/src/api/v1/me/friends/mod.rs +++ b/src/api/v1/me/friends/mod.rs @@ -1,38 +1,40 @@ -use std::sync::Arc; - use ::uuid::Uuid; -use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse}; +use actix_web::{HttpRequest, HttpResponse, get, post, web}; use serde::Deserialize; pub mod uuid; use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, objects::Me, - utils::{global_checks, user_uuid_from_username}, + utils::{get_auth_header, global_checks}, }; /// Returns a list of users that are your friends -pub async fn get( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; +#[get("/friends")] +pub async fn get(req: HttpRequest, data: web::Data) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; let me = Me::get(&mut conn, uuid).await?; - let friends = me.get_friends(&mut conn, &app_state.cache_pool).await?; + let friends = me.get_friends(&data).await?; - Ok((StatusCode::OK, Json(friends))) + Ok(HttpResponse::Ok().json(friends)) } #[derive(Deserialize)] -pub struct UserReq { - username: String, +struct UserReq { + uuid: Uuid, } /// `POST /api/v1/me/friends` Send friend request @@ -54,19 +56,25 @@ pub struct UserReq { /// /// 400 Bad Request (usually means users are already friends) /// +#[post("/friends")] pub async fn post( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, - Json(user_request): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + json: web::Json, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; let me = Me::get(&mut conn, uuid).await?; - let target_uuid = user_uuid_from_username(&mut conn, &user_request.username).await?; - me.add_friend(&mut conn, target_uuid).await?; + me.add_friend(&mut conn, json.uuid).await?; - Ok(StatusCode::OK) + Ok(HttpResponse::Ok().finish()) } diff --git a/src/api/v1/me/friends/uuid.rs b/src/api/v1/me/friends/uuid.rs index 35f0742..34bfeff 100644 --- a/src/api/v1/me/friends/uuid.rs +++ b/src/api/v1/me/friends/uuid.rs @@ -1,29 +1,33 @@ -use std::sync::Arc; - -use axum::{ - Extension, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; +use actix_web::{HttpRequest, HttpResponse, delete, web}; use uuid::Uuid; use crate::{ - AppState, api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks, + Data, + api::v1::auth::check_access_token, + error::Error, + objects::Me, + utils::{get_auth_header, global_checks}, }; +#[delete("/friends/{uuid}")] pub async fn delete( - State(app_state): State>, - Path(friend_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; let me = Me::get(&mut conn, uuid).await?; - me.remove_friend(&mut conn, friend_uuid).await?; + me.remove_friend(&mut conn, path.0).await?; - Ok(StatusCode::OK) + Ok(HttpResponse::Ok().finish()) } diff --git a/src/api/v1/me/guilds.rs b/src/api/v1/me/guilds.rs index 42d5c21..71cfca4 100644 --- a/src/api/v1/me/guilds.rs +++ b/src/api/v1/me/guilds.rs @@ -1,12 +1,13 @@ //! `/api/v1/me/guilds` Contains endpoint related to guild memberships -use std::sync::Arc; - -use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse}; -use uuid::Uuid; +use actix_web::{HttpRequest, HttpResponse, get, web}; use crate::{ - AppState, api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks, + Data, + api::v1::auth::check_access_token, + error::Error, + objects::Me, + utils::{get_auth_header, global_checks}, }; /// `GET /api/v1/me/guilds` Returns all guild memberships in a list @@ -54,17 +55,21 @@ use crate::{ /// ]); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -pub async fn get( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; +#[get("/guilds")] +pub async fn get(req: HttpRequest, data: web::Data) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; let me = Me::get(&mut conn, uuid).await?; let memberships = me.fetch_memberships(&mut conn).await?; - Ok((StatusCode::OK, Json(memberships))) + Ok(HttpResponse::Ok().json(memberships)) } diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index 9d75d26..f667ca4 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -1,120 +1,108 @@ -use std::sync::Arc; - -use axum::{ - Extension, Json, Router, - extract::{DefaultBodyLimit, Multipart, State}, - http::StatusCode, - response::IntoResponse, - routing::{delete, get, patch, post}, -}; -use bytes::Bytes; +use actix_multipart::form::{MultipartForm, json::Json as MpJson, tempfile::TempFile}; +use actix_web::{HttpRequest, HttpResponse, Scope, get, patch, web}; use serde::Deserialize; -use uuid::Uuid; use crate::{ - AppState, api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks, + Data, + api::v1::auth::check_access_token, + error::Error, + objects::Me, + utils::{get_auth_header, global_checks}, }; mod friends; mod guilds; -pub fn router() -> Router> { - Router::new() - .route("/", get(get_me)) - .route( - "/", - patch(update).layer(DefaultBodyLimit::max( - 100 * 1024 * 1024, /* limit is in bytes */ - )), - ) - .route("/guilds", get(guilds::get)) - .route("/friends", get(friends::get)) - .route("/friends", post(friends::post)) - .route("/friends/{uuid}", delete(friends::uuid::delete)) +pub fn web() -> Scope { + web::scope("/me") + .service(get) + .service(update) + .service(guilds::get) + .service(friends::get) + .service(friends::post) + .service(friends::uuid::delete) } -pub async fn get_me( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let me = Me::get(&mut app_state.pool.get().await?, uuid).await?; +#[get("")] +pub async fn get(req: HttpRequest, data: web::Data) -> Result { + let headers = req.headers(); - Ok((StatusCode::OK, Json(me))) + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + let me = Me::get(&mut conn, uuid).await?; + + Ok(HttpResponse::Ok().json(me)) } -#[derive(Default, Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Clone)] struct NewInfo { username: Option, display_name: Option, + //password: Option, will probably be handled through a reset password link email: Option, pronouns: Option, about: Option, - online_status: Option, } +#[derive(Debug, MultipartForm)] +struct UploadForm { + #[multipart(limit = "100MB")] + avatar: Option, + json: MpJson, +} + +#[patch("")] pub async fn update( - State(app_state): State>, - Extension(CurrentUser(uuid)): Extension>, - mut multipart: Multipart, -) -> Result { - let mut json_raw: Option = None; - let mut avatar: Option = None; + req: HttpRequest, + MultipartForm(form): MultipartForm, + data: web::Data, +) -> Result { + let headers = req.headers(); - while let Some(field) = multipart.next_field().await.unwrap() { - let name = field - .name() - .ok_or(Error::BadRequest("Field has no name".to_string()))?; + let auth_header = get_auth_header(headers)?; - if name == "avatar" { - avatar = Some(field.bytes().await?); - } else if name == "json" { - json_raw = Some(serde_json::from_str(&field.text().await?)?) - } - } + let mut conn = data.pool.get().await?; - let json = json_raw.unwrap_or_default(); + let uuid = check_access_token(auth_header, &mut conn).await?; - let mut conn = app_state.pool.get().await?; - - if avatar.is_some() || json.username.is_some() || json.display_name.is_some() { - global_checks(&mut conn, &app_state.config, uuid).await?; + if form.avatar.is_some() || form.json.username.is_some() || form.json.display_name.is_some() { + global_checks(&data, uuid).await?; } let mut me = Me::get(&mut conn, uuid).await?; - if let Some(avatar) = avatar { - me.set_avatar(&mut conn, &app_state, avatar).await?; - } + if let Some(avatar) = form.avatar { + let bytes = tokio::fs::read(avatar.file).await?; - if let Some(username) = &json.username { - me.set_username(&mut conn, &app_state.cache_pool, username.clone()) + let byte_slice: &[u8] = &bytes; + + me.set_avatar(&data, data.config.bunny.cdn_url.clone(), byte_slice.into()) .await?; } - if let Some(display_name) = &json.display_name { - me.set_display_name(&mut conn, &app_state.cache_pool, display_name.clone()) - .await?; + if let Some(username) = &form.json.username { + me.set_username(&data, username.clone()).await?; } - if let Some(email) = &json.email { - me.set_email(&mut conn, &app_state.cache_pool, email.clone()) - .await?; + if let Some(display_name) = &form.json.display_name { + me.set_display_name(&data, display_name.clone()).await?; } - if let Some(pronouns) = &json.pronouns { - me.set_pronouns(&mut conn, &app_state.cache_pool, pronouns.clone()) - .await?; + if let Some(email) = &form.json.email { + me.set_email(&data, email.clone()).await?; } - if let Some(about) = &json.about { - me.set_about(&mut conn, &app_state.cache_pool, about.clone()) - .await?; + if let Some(pronouns) = &form.json.pronouns { + me.set_pronouns(&data, pronouns.clone()).await?; } - if let Some(online_status) = &json.online_status { - me.set_online_status(&mut conn, &app_state.cache_pool, *online_status) - .await?; + if let Some(about) = &form.json.about { + me.set_about(&data, about.clone()).await?; } - Ok(StatusCode::OK) + Ok(HttpResponse::Ok().finish()) } diff --git a/src/api/v1/members/mod.rs b/src/api/v1/members/mod.rs deleted file mode 100644 index 59ceac2..0000000 --- a/src/api/v1/members/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::sync::Arc; - -use axum::{ - Router, - routing::{delete, get, post}, -}; - -use crate::AppState; - -mod uuid; - -pub fn router() -> Router> { - Router::new() - .route("/{uuid}", get(uuid::get)) - .route("/{uuid}", delete(uuid::delete)) - .route("/{uuid}/ban", post(uuid::ban::post)) -} diff --git a/src/api/v1/members/uuid/ban.rs b/src/api/v1/members/uuid/ban.rs deleted file mode 100644 index e828e69..0000000 --- a/src/api/v1/members/uuid/ban.rs +++ /dev/null @@ -1,48 +0,0 @@ -use std::sync::Arc; - -use axum::{ - Extension, - extract::{Json, Path, State}, - http::StatusCode, - response::IntoResponse, -}; -use serde::Deserialize; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Member, Permissions}, - utils::global_checks, -}; - -use uuid::Uuid; - -#[derive(Deserialize)] -pub struct RequstBody { - reason: String, -} - -pub async fn post( - State(app_state): State>, - Path(member_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, - Json(payload): Json, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let member = - Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, None, member_uuid).await?; - - let caller = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; - - caller - .check_permission(&mut conn, &app_state.cache_pool, Permissions::BanMember) - .await?; - - member.ban(&mut conn, &payload.reason).await?; - - Ok(StatusCode::OK) -} diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs deleted file mode 100644 index 5bfd129..0000000 --- a/src/api/v1/members/uuid/mod.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! `/api/v1/members/{uuid}` Member specific endpoints - -pub mod ban; - -use std::sync::Arc; - -use crate::{ - AppState, - api::v1::auth::CurrentUser, - error::Error, - objects::{Me, Member, Permissions}, - utils::global_checks, -}; -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; - -use uuid::Uuid; - -pub async fn get( - State(app_state): State>, - Path(member_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let me = Me::get(&mut conn, uuid).await?; - - let member = - Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, Some(&me), member_uuid) - .await?; - Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; - - Ok((StatusCode::OK, Json(member))) -} - -pub async fn delete( - State(app_state): State>, - Path(member_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; - - global_checks(&mut conn, &app_state.config, uuid).await?; - - let me = Me::get(&mut conn, uuid).await?; - - let member = - Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, Some(&me), member_uuid) - .await?; - - let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; - - deleter - .check_permission(&mut conn, &app_state.cache_pool, Permissions::KickMember) - .await?; - - member.delete(&mut conn).await?; - - Ok(StatusCode::OK) -} diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 70271ef..6c2df0b 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -1,35 +1,22 @@ //! `/api/v1` Contains version 1 of the api -use std::sync::Arc; - -use axum::{Router, middleware::from_fn_with_state, routing::get}; - -use crate::{AppState, api::v1::auth::CurrentUser}; +use actix_web::{Scope, web}; mod auth; mod channels; mod guilds; mod invites; mod me; -mod members; mod stats; mod users; -pub fn router(app_state: Arc) -> Router> { - let router_with_auth = Router::new() - .nest("/users", users::router()) - .nest("/guilds", guilds::router()) - .nest("/invites", invites::router()) - .nest("/members", members::router()) - .nest("/me", me::router()) - .layer(from_fn_with_state( - app_state.clone(), - CurrentUser::check_auth_layer, - )); - - Router::new() - .route("/stats", get(stats::res)) - .nest("/auth", auth::router(app_state.clone())) - .nest("/channels", channels::router(app_state)) - .merge(router_with_auth) +pub fn web() -> Scope { + web::scope("/v1") + .service(stats::res) + .service(auth::web()) + .service(users::web()) + .service(channels::web()) + .service(guilds::web()) + .service(invites::web()) + .service(me::web()) } diff --git a/src/api/v1/stats.rs b/src/api/v1/stats.rs index 17c5df6..760ec71 100644 --- a/src/api/v1/stats.rs +++ b/src/api/v1/stats.rs @@ -1,17 +1,13 @@ //! `/api/v1/stats` Returns stats about the server -use std::sync::Arc; use std::time::SystemTime; -use axum::Json; -use axum::extract::State; -use axum::http::StatusCode; -use axum::response::IntoResponse; +use actix_web::{HttpResponse, get, web}; use diesel::QueryDsl; use diesel_async::RunQueryDsl; use serde::Serialize; -use crate::AppState; +use crate::Data; use crate::error::Error; use crate::schema::users::dsl::{users, uuid}; @@ -43,26 +39,27 @@ struct Response { /// "build_number": "39d01bb" /// }); /// ``` -pub async fn res(State(app_state): State>) -> Result { +#[get("/stats")] +pub async fn res(data: web::Data) -> Result { let accounts: i64 = users .select(uuid) .count() - .get_result(&mut app_state.pool.get().await?) + .get_result(&mut data.pool.get().await?) .await?; let response = Response { // TODO: Get number of accounts from db accounts, uptime: SystemTime::now() - .duration_since(app_state.start_time) + .duration_since(data.start_time) .expect("Seriously why dont you have time??") .as_secs(), version: String::from(VERSION.unwrap_or("UNKNOWN")), - registration_enabled: app_state.config.instance.registration, - email_verification_required: app_state.config.instance.require_email_verification, + registration_enabled: data.config.instance.registration, + email_verification_required: data.config.instance.require_email_verification, // TODO: Get build number from git hash or remove this from the spec build_number: String::from(GIT_SHORT_HASH), }; - Ok((StatusCode::OK, Json(response))) + Ok(HttpResponse::Ok().json(response)) } diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index 999e13f..334fd5f 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -1,30 +1,19 @@ //! `/api/v1/users` Contains endpoints related to all users -use std::sync::Arc; - -use ::uuid::Uuid; -use axum::{ - Extension, Json, Router, - extract::{Query, State}, - http::StatusCode, - response::IntoResponse, - routing::get, -}; +use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, objects::{StartAmountQuery, User}, - utils::global_checks, + utils::{get_auth_header, global_checks}, }; mod uuid; -pub fn router() -> Router> { - Router::new() - .route("/", get(users)) - .route("/{uuid}", get(uuid::get)) +pub fn web() -> Scope { + web::scope("/users").service(get).service(uuid::get) } /// `GET /api/v1/users` Returns all users on this instance @@ -57,24 +46,31 @@ pub fn router() -> Router> { /// ]); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -pub async fn users( - State(app_state): State>, - Query(request_query): Query, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { +#[get("")] +pub async fn get( + req: HttpRequest, + request_query: web::Query, + data: web::Data, +) -> Result { + let headers = req.headers(); + + let auth_header = get_auth_header(headers)?; + let start = request_query.start.unwrap_or(0); let amount = request_query.amount.unwrap_or(10); if amount > 100 { - return Ok(StatusCode::BAD_REQUEST.into_response()); + return Ok(HttpResponse::BadRequest().finish()); } - let mut conn = app_state.pool.get().await?; + let mut conn = data.pool.get().await?; - global_checks(&mut conn, &app_state.config, uuid).await?; + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; let users = User::fetch_amount(&mut conn, start, amount).await?; - Ok((StatusCode::OK, Json(users)).into_response()) + Ok(HttpResponse::Ok().json(users)) } diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index e015c3c..5d36b75 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -1,21 +1,14 @@ //! `/api/v1/users/{uuid}` Specific user endpoints -use std::sync::Arc; - -use axum::{ - Extension, Json, - extract::{Path, State}, - http::StatusCode, - response::IntoResponse, -}; +use actix_web::{HttpRequest, HttpResponse, get, web}; use uuid::Uuid; use crate::{ - AppState, - api::v1::auth::CurrentUser, + Data, + api::v1::auth::check_access_token, error::Error, objects::{Me, User}, - utils::global_checks, + utils::{get_auth_header, global_checks}, }; /// `GET /api/v1/users/{uuid}` Returns user with the given UUID @@ -34,19 +27,27 @@ use crate::{ /// }); /// ``` /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps +#[get("/{uuid}")] pub async fn get( - State(app_state): State>, - Path(user_uuid): Path, - Extension(CurrentUser(uuid)): Extension>, -) -> Result { - let mut conn = app_state.pool.get().await?; + req: HttpRequest, + path: web::Path<(Uuid,)>, + data: web::Data, +) -> Result { + let headers = req.headers(); - global_checks(&mut conn, &app_state.config, uuid).await?; + let user_uuid = path.into_inner().0; + + let auth_header = get_auth_header(headers)?; + + let mut conn = data.pool.get().await?; + + let uuid = check_access_token(auth_header, &mut conn).await?; + + global_checks(&data, uuid).await?; let me = Me::get(&mut conn, uuid).await?; - let user = - User::fetch_one_with_friendship(&mut conn, &app_state.cache_pool, &me, user_uuid).await?; + let user = User::fetch_one_with_friendship(&data, &me, user_uuid).await?; - Ok((StatusCode::OK, Json(user))) + Ok(HttpResponse::Ok().json(user)) } diff --git a/src/api/versions.rs b/src/api/versions.rs index 3c9576b..0c3e106 100644 --- a/src/api/versions.rs +++ b/src/api/versions.rs @@ -1,5 +1,5 @@ //! `/api/v1/versions` Returns info about api versions -use axum::{Json, http::StatusCode, response::IntoResponse}; +use actix_web::{HttpResponse, Responder, get}; use serde::Serialize; #[derive(Serialize)] @@ -24,12 +24,13 @@ struct UnstableFeatures; /// ] /// }); /// ``` -pub async fn versions() -> impl IntoResponse { +#[get("/versions")] +pub async fn get() -> impl Responder { let response = Response { unstable_features: UnstableFeatures, // TODO: Find a way to dynamically update this possibly? versions: vec![String::from("1")], }; - (StatusCode::OK, Json(response)) + HttpResponse::Ok().json(response) } diff --git a/src/error.rs b/src/error.rs index d6f7a12..35b533d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,16 +1,12 @@ use std::{io, time::SystemTimeError}; -use axum::{ - Json, - extract::{ - multipart::MultipartError, - rejection::{JsonRejection, QueryRejection}, - }, +use actix_web::{ + HttpResponse, + error::{PayloadError, ResponseError}, http::{ StatusCode, - header::{InvalidHeaderValue, ToStrError}, + header::{ContentType, ToStrError}, }, - response::IntoResponse, }; use bunny_api_tokio::error::Error as BunnyError; use deadpool::managed::{BuildError, PoolError}; @@ -58,13 +54,9 @@ pub enum Error { #[error(transparent)] UrlParseError(#[from] url::ParseError), #[error(transparent)] - JsonRejection(#[from] JsonRejection), + PayloadError(#[from] PayloadError), #[error(transparent)] - QueryRejection(#[from] QueryRejection), - #[error(transparent)] - MultipartError(#[from] MultipartError), - #[error(transparent)] - InvalidHeaderValue(#[from] InvalidHeaderValue), + WsClosed(#[from] actix_ws::Closed), #[error(transparent)] EmailError(#[from] EmailError), #[error(transparent)] @@ -83,45 +75,28 @@ pub enum Error { TooManyRequests(String), #[error("{0}")] InternalServerError(String), - // TODO: remove when doing socket.io - #[error(transparent)] - AxumError(#[from] axum::Error), } -impl IntoResponse for Error { - fn into_response(self) -> axum::response::Response { - let error = match self { - Error::SqlError(DieselError::NotFound) => { - (StatusCode::NOT_FOUND, Json(WebError::new(self.to_string()))) - } - Error::BunnyError(BunnyError::NotFound(_)) => { - (StatusCode::NOT_FOUND, Json(WebError::new(self.to_string()))) - } - Error::BadRequest(_) => ( - StatusCode::BAD_REQUEST, - Json(WebError::new(self.to_string())), - ), - Error::Unauthorized(_) => ( - StatusCode::UNAUTHORIZED, - Json(WebError::new(self.to_string())), - ), - Error::Forbidden(_) => (StatusCode::FORBIDDEN, Json(WebError::new(self.to_string()))), - Error::TooManyRequests(_) => ( - StatusCode::TOO_MANY_REQUESTS, - Json(WebError::new(self.to_string())), - ), - _ => ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(WebError::new(self.to_string())), - ), - }; - - let (code, _) = error; - +impl ResponseError for Error { + fn error_response(&self) -> HttpResponse { debug!("{self:?}"); - error!("{code}: {self}"); + error!("{}: {}", self.status_code(), self); - error.into_response() + HttpResponse::build(self.status_code()) + .insert_header(ContentType::json()) + .json(WebError::new(self.to_string())) + } + + fn status_code(&self) -> StatusCode { + match *self { + Error::SqlError(DieselError::NotFound) => StatusCode::NOT_FOUND, + Error::BunnyError(BunnyError::NotFound(_)) => StatusCode::NOT_FOUND, + Error::BadRequest(_) => StatusCode::BAD_REQUEST, + Error::Unauthorized(_) => StatusCode::UNAUTHORIZED, + Error::Forbidden(_) => StatusCode::FORBIDDEN, + Error::TooManyRequests(_) => StatusCode::TOO_MANY_REQUESTS, + _ => StatusCode::INTERNAL_SERVER_ERROR, + } } } diff --git a/src/main.rs b/src/main.rs index 13e661d..248289a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,16 @@ +use actix_cors::Cors; +use actix_web::{App, HttpServer, web}; use argon2::Argon2; -use axum::{ - Router, - http::{Method, header}, -}; use clap::Parser; -use config::{Config, ConfigBuilder}; use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; -use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; use error::Error; use objects::MailClient; -use std::{sync::Arc, time::SystemTime}; -use tower_http::cors::{AllowOrigin, CorsLayer}; +use simple_logger::SimpleLogger; +use std::time::SystemTime; +mod config; +use config::{Config, ConfigBuilder}; +use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); @@ -19,13 +18,10 @@ type Conn = deadpool::managed::Object>; mod api; -mod config; pub mod error; pub mod objects; pub mod schema; -//mod socket; pub mod utils; -mod wordlist; #[derive(Parser, Debug)] #[command(version, about, long_about = None)] @@ -35,7 +31,7 @@ struct Args { } #[derive(Clone)] -pub struct AppState { +pub struct Data { pub pool: deadpool::managed::Pool< AsyncDieselConnectionManager, Conn, @@ -50,8 +46,12 @@ pub struct AppState { #[tokio::main] async fn main() -> Result<(), Error> { - tracing_subscriber::fmt::init(); - + 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(); @@ -112,7 +112,7 @@ async fn main() -> Result<(), Error> { ) */ - let app_state = Arc::new(AppState { + let data = Data { pool, cache_pool, config, @@ -121,56 +121,42 @@ async fn main() -> Result<(), Error> { start_time: SystemTime::now(), bunny_storage, mail_client, - }); + }; - let cors = CorsLayer::new() - // Allow any origin (equivalent to allowed_origin_fn returning true) - .allow_origin(AllowOrigin::predicate(|_origin, _request_head| true)) - .allow_methods(vec![ - Method::GET, - Method::POST, - Method::PUT, - Method::DELETE, - Method::HEAD, - Method::OPTIONS, - Method::CONNECT, - Method::PATCH, - Method::TRACE, - ]) - .allow_headers(vec![ - header::ACCEPT, - header::ACCEPT_LANGUAGE, - header::AUTHORIZATION, - header::CONTENT_LANGUAGE, - header::CONTENT_TYPE, - header::ORIGIN, - header::ACCEPT, - header::COOKIE, - "x-requested-with".parse().unwrap(), - ]) - // Allow credentials - .allow_credentials(true); + 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(); - /*let (socket_io, io) = SocketIo::builder() - .with_state(app_state.clone()) - .build_layer(); - - io.ns("/", socket::on_connect); - */ - // build our application with a route - let app = Router::new() - // `GET /` goes to `root` - .merge(api::router( - web.backend_url.path().trim_end_matches("/"), - app_state.clone(), - )) - .with_state(app_state) - //.layer(socket_io) - .layer(cors); - - // run our app with hyper, listening globally on port 3000 - let listener = tokio::net::TcpListener::bind(web.ip + ":" + &web.port.to_string()).await?; - axum::serve(listener, app).await?; + App::new() + .app_data(web::Data::new(data.clone())) + .wrap(cors) + .service(api::web(data.config.web.backend_url.path())) + }) + .bind((web.ip, web.port))? + .run() + .await?; Ok(()) } diff --git a/src/objects/bans.rs b/src/objects/bans.rs deleted file mode 100644 index 602afa6..0000000 --- a/src/objects/bans.rs +++ /dev/null @@ -1,57 +0,0 @@ -use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use diesel_async::RunQueryDsl; - -use crate::{Conn, error::Error, objects::load_or_empty, schema::guild_bans}; - -#[derive(Selectable, Queryable, Serialize, Deserialize)] -#[diesel(table_name = guild_bans)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct GuildBan { - pub guild_uuid: Uuid, - pub user_uuid: Uuid, - pub reason: Option, - pub banned_since: chrono::DateTime, -} - -impl GuildBan { - pub async fn fetch_one( - conn: &mut Conn, - guild_uuid: Uuid, - user_uuid: Uuid, - ) -> Result { - use guild_bans::dsl; - let guild_ban = dsl::guild_bans - .filter(dsl::guild_uuid.eq(guild_uuid)) - .filter(dsl::user_uuid.eq(user_uuid)) - .select(GuildBan::as_select()) - .get_result(conn) - .await?; - - Ok(guild_ban) - } - - pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { - use guild_bans::dsl; - let all_guild_bans = load_or_empty( - dsl::guild_bans - .filter(dsl::guild_uuid.eq(guild_uuid)) - .load(conn) - .await, - )?; - - Ok(all_guild_bans) - } - - pub async fn unban(self, conn: &mut Conn) -> Result<(), Error> { - use guild_bans::dsl; - diesel::delete(guild_bans::table) - .filter(dsl::guild_uuid.eq(self.guild_uuid)) - .filter(dsl::user_uuid.eq(self.user_uuid)) - .execute(conn) - .await?; - Ok(()) - } -} diff --git a/src/objects/categories.rs b/src/objects/categories.rs new file mode 100644 index 0000000..f2c6a5f --- /dev/null +++ b/src/objects/categories.rs @@ -0,0 +1,361 @@ +use diesel::{ + ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete, + insert_into, update, +}; +use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{ + Conn, Data, + error::Error, + schema::categories, + utils::{CHANNEL_REGEX, order_by_is_above}, +}; + +use super::{HasIsAbove, HasUuid, Message, load_or_empty, message::MessageBuilder}; + +#[derive(Serialize, Deserialize, Queryable, Selectable, Insertable, Clone, Debug)] +#[diesel(table_name = categories)] +#[diesel(check_for_backend(diesel::pg::Pg))] +struct Category { + uuid: Uuid, + guild_uuid: Uuid, + name: String, + description: Option, + is_above: Option, +} + +impl HasUuid for Category { + fn uuid(&self) -> &Uuid { + self.uuid.as_ref() + } +} + +impl HasIsAbove for Category { + fn is_above(&self) -> Option<&Uuid> { + self.is_above.as_ref() + } +} + +impl Category { + pub async fn fetch_all( + pool: &deadpool::managed::Pool< + AsyncDieselConnectionManager, + Conn, + >, + guild_uuid: Uuid, + ) -> Result, Error> { + let mut conn = pool.get().await?; + + use channels::dsl; + let channel_builders: Vec = load_or_empty( + dsl::channels + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(ChannelBuilder::as_select()) + .load(&mut conn) + .await, + )?; + + let channel_futures = channel_builders.iter().map(async move |c| { + let mut conn = pool.get().await?; + c.clone().build(&mut conn).await + }); + + futures::future::try_join_all(channel_futures).await + } + + pub async fn fetch_one(data: &Data, channel_uuid: Uuid) -> Result { + if let Ok(cache_hit) = data.get_cache_key(channel_uuid.to_string()).await { + return Ok(serde_json::from_str(&cache_hit)?); + } + + let mut conn = data.pool.get().await?; + + use channels::dsl; + let channel_builder: ChannelBuilder = dsl::channels + .filter(dsl::uuid.eq(channel_uuid)) + .select(ChannelBuilder::as_select()) + .get_result(&mut conn) + .await?; + + let channel = channel_builder.build(&mut conn).await?; + + data.set_cache_key(channel_uuid.to_string(), channel.clone(), 60) + .await?; + + Ok(channel) + } + + pub async fn new( + data: actix_web::web::Data, + guild_uuid: Uuid, + name: String, + description: Option, + ) -> Result { + if !CHANNEL_REGEX.is_match(&name) { + return Err(Error::BadRequest("Channel name is invalid".to_string())); + } + + let mut conn = data.pool.get().await?; + + let channel_uuid = Uuid::now_v7(); + + let channels = Self::fetch_all(&data.pool, guild_uuid).await?; + + let channels_ordered = order_by_is_above(channels).await?; + + let last_channel = channels_ordered.last(); + + let new_channel = ChannelBuilder { + uuid: channel_uuid, + guild_uuid, + name: name.clone(), + description: description.clone(), + is_above: None, + }; + + insert_into(channels::table) + .values(new_channel.clone()) + .execute(&mut conn) + .await?; + + if let Some(old_last_channel) = last_channel { + use channels::dsl; + update(channels::table) + .filter(dsl::uuid.eq(old_last_channel.uuid)) + .set(dsl::is_above.eq(new_channel.uuid)) + .execute(&mut conn) + .await?; + } + + // returns different object because there's no reason to build the channelbuilder (wastes 1 database request) + let channel = Self { + uuid: channel_uuid, + guild_uuid, + name, + description, + is_above: None, + permissions: vec![], + }; + + data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) + .await?; + + if data + .get_cache_key(format!("{guild_uuid}_channels")) + .await + .is_ok() + { + data.del_cache_key(format!("{guild_uuid}_channels")).await?; + } + + Ok(channel) + } + + pub async fn delete(self, data: &Data) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use channels::dsl; + match update(channels::table) + .filter(dsl::is_above.eq(self.uuid)) + .set(dsl::is_above.eq(None::)) + .execute(&mut conn) + .await + { + Ok(r) => Ok(r), + Err(diesel::result::Error::NotFound) => Ok(0), + Err(e) => Err(e), + }?; + + delete(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .execute(&mut conn) + .await?; + + match update(channels::table) + .filter(dsl::is_above.eq(self.uuid)) + .set(dsl::is_above.eq(self.is_above)) + .execute(&mut conn) + .await + { + Ok(r) => Ok(r), + Err(diesel::result::Error::NotFound) => Ok(0), + Err(e) => Err(e), + }?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await?; + } + + if data + .get_cache_key(format!("{}_channels", self.guild_uuid)) + .await + .is_ok() + { + data.del_cache_key(format!("{}_channels", self.guild_uuid)) + .await?; + } + + Ok(()) + } + + pub async fn fetch_messages( + &self, + data: &Data, + amount: i64, + offset: i64, + ) -> Result, Error> { + let mut conn = data.pool.get().await?; + + use messages::dsl; + let messages: Vec = load_or_empty( + dsl::messages + .filter(dsl::channel_uuid.eq(self.uuid)) + .select(MessageBuilder::as_select()) + .order(dsl::uuid.desc()) + .limit(amount) + .offset(offset) + .load(&mut conn) + .await, + )?; + + let message_futures = messages.iter().map(async move |b| b.build(data).await); + + futures::future::try_join_all(message_futures).await + } + + pub async fn new_message( + &self, + data: &Data, + user_uuid: Uuid, + message: String, + reply_to: Option, + ) -> Result { + let message_uuid = Uuid::now_v7(); + + let message = MessageBuilder { + uuid: message_uuid, + channel_uuid: self.uuid, + user_uuid, + message, + reply_to, + is_edited: false, + }; + + let mut conn = data.pool.get().await?; + + insert_into(messages::table) + .values(message.clone()) + .execute(&mut conn) + .await?; + + message.build(data).await + } + + /*pub async fn edit_message(&self, data: &Data, user_uuid: Uuid, message_uuid: Uuid, message: String) -> Result { + use messages::dsl; + + let mut conn = data.pool.get().await?; + + update(messages::table) + .filter(dsl::user_uuid.eq(user_uuid)) + .filter(dsl::uuid.eq(message_uuid)) + .set((dsl::is_edited.eq(true), dsl::message.eq(message))) + .execute(&mut conn) + .await?; + + Ok(()) + }*/ + + pub async fn set_name(&mut self, data: &Data, new_name: String) -> Result<(), Error> { + if !CHANNEL_REGEX.is_match(&new_name) { + return Err(Error::BadRequest("Channel name is invalid".to_string())); + } + + let mut conn = data.pool.get().await?; + + use channels::dsl; + update(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::name.eq(&new_name)) + .execute(&mut conn) + .await?; + + self.name = new_name; + + Ok(()) + } + + pub async fn set_description( + &mut self, + data: &Data, + new_description: String, + ) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use channels::dsl; + update(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::description.eq(&new_description)) + .execute(&mut conn) + .await?; + + self.description = Some(new_description); + + Ok(()) + } + + pub async fn move_channel(&mut self, data: &Data, new_is_above: Uuid) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use channels::dsl; + let old_above_uuid: Option = match dsl::channels + .filter(dsl::is_above.eq(self.uuid)) + .select(dsl::uuid) + .get_result(&mut conn) + .await + { + Ok(r) => Ok(Some(r)), + Err(diesel::result::Error::NotFound) => Ok(None), + Err(e) => Err(e), + }?; + + if let Some(uuid) = old_above_uuid { + update(channels::table) + .filter(dsl::uuid.eq(uuid)) + .set(dsl::is_above.eq(None::)) + .execute(&mut conn) + .await?; + } + + match update(channels::table) + .filter(dsl::is_above.eq(new_is_above)) + .set(dsl::is_above.eq(self.uuid)) + .execute(&mut conn) + .await + { + Ok(r) => Ok(r), + Err(diesel::result::Error::NotFound) => Ok(0), + Err(e) => Err(e), + }?; + + update(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::is_above.eq(new_is_above)) + .execute(&mut conn) + .await?; + + if let Some(uuid) = old_above_uuid { + update(channels::table) + .filter(dsl::uuid.eq(uuid)) + .set(dsl::is_above.eq(self.is_above)) + .execute(&mut conn) + .await?; + } + + self.is_above = Some(new_is_above); + + Ok(()) + } +} diff --git a/src/objects/channel.rs b/src/objects/channel.rs index 03a2cf6..3b55137 100644 --- a/src/objects/channel.rs +++ b/src/objects/channel.rs @@ -2,15 +2,15 @@ use diesel::{ ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into, update, }; -use diesel_async::RunQueryDsl; +use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - Conn, + Conn, Data, error::Error, schema::{channel_permissions, channels, messages}, - utils::{CHANNEL_REGEX, CacheFns, order_by_is_above}, + utils::{CHANNEL_REGEX, order_by_is_above}, }; use super::{HasIsAbove, HasUuid, Message, load_or_empty, message::MessageBuilder}; @@ -79,53 +79,56 @@ impl HasIsAbove for Channel { } impl Channel { - pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { + pub async fn fetch_all( + pool: &deadpool::managed::Pool< + AsyncDieselConnectionManager, + Conn, + >, + guild_uuid: Uuid, + ) -> Result, Error> { + let mut conn = pool.get().await?; + use channels::dsl; let channel_builders: Vec = load_or_empty( dsl::channels .filter(dsl::guild_uuid.eq(guild_uuid)) .select(ChannelBuilder::as_select()) - .load(conn) + .load(&mut conn) .await, )?; - let mut channels = vec![]; + let channel_futures = channel_builders.iter().map(async move |c| { + let mut conn = pool.get().await?; + c.clone().build(&mut conn).await + }); - for builder in channel_builders { - channels.push(builder.build(conn).await?); - } - - Ok(channels) + futures::future::try_join_all(channel_futures).await } - pub async fn fetch_one( - conn: &mut Conn, - cache_pool: &redis::Client, - channel_uuid: Uuid, - ) -> Result { - if let Ok(cache_hit) = cache_pool.get_cache_key(channel_uuid.to_string()).await { - return Ok(cache_hit); + pub async fn fetch_one(data: &Data, channel_uuid: Uuid) -> Result { + if let Ok(cache_hit) = data.get_cache_key(channel_uuid.to_string()).await { + return Ok(serde_json::from_str(&cache_hit)?); } + let mut conn = data.pool.get().await?; + use channels::dsl; let channel_builder: ChannelBuilder = dsl::channels .filter(dsl::uuid.eq(channel_uuid)) .select(ChannelBuilder::as_select()) - .get_result(conn) + .get_result(&mut conn) .await?; - let channel = channel_builder.build(conn).await?; + let channel = channel_builder.build(&mut conn).await?; - cache_pool - .set_cache_key(channel_uuid.to_string(), channel.clone(), 60) + data.set_cache_key(channel_uuid.to_string(), channel.clone(), 60) .await?; Ok(channel) } pub async fn new( - conn: &mut Conn, - cache_pool: &redis::Client, + data: actix_web::web::Data, guild_uuid: Uuid, name: String, description: Option, @@ -134,9 +137,11 @@ impl Channel { return Err(Error::BadRequest("Channel name is invalid".to_string())); } + let mut conn = data.pool.get().await?; + let channel_uuid = Uuid::now_v7(); - let channels = Self::fetch_all(conn, guild_uuid).await?; + let channels = Self::fetch_all(&data.pool, guild_uuid).await?; let channels_ordered = order_by_is_above(channels).await?; @@ -152,7 +157,7 @@ impl Channel { insert_into(channels::table) .values(new_channel.clone()) - .execute(conn) + .execute(&mut conn) .await?; if let Some(old_last_channel) = last_channel { @@ -160,7 +165,7 @@ impl Channel { update(channels::table) .filter(dsl::uuid.eq(old_last_channel.uuid)) .set(dsl::is_above.eq(new_channel.uuid)) - .execute(conn) + .execute(&mut conn) .await?; } @@ -174,29 +179,28 @@ impl Channel { permissions: vec![], }; - cache_pool - .set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) + data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) .await?; - if cache_pool - .get_cache_key::>(format!("{guild_uuid}_channels")) + if data + .get_cache_key(format!("{guild_uuid}_channels")) .await .is_ok() { - cache_pool - .del_cache_key(format!("{guild_uuid}_channels")) - .await?; + data.del_cache_key(format!("{guild_uuid}_channels")).await?; } Ok(channel) } - pub async fn delete(self, conn: &mut Conn, cache_pool: &redis::Client) -> Result<(), Error> { + pub async fn delete(self, data: &Data) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + use channels::dsl; match update(channels::table) .filter(dsl::is_above.eq(self.uuid)) .set(dsl::is_above.eq(None::)) - .execute(conn) + .execute(&mut conn) .await { Ok(r) => Ok(r), @@ -206,13 +210,13 @@ impl Channel { delete(channels::table) .filter(dsl::uuid.eq(self.uuid)) - .execute(conn) + .execute(&mut conn) .await?; match update(channels::table) .filter(dsl::is_above.eq(self.uuid)) .set(dsl::is_above.eq(self.is_above)) - .execute(conn) + .execute(&mut conn) .await { Ok(r) => Ok(r), @@ -220,21 +224,16 @@ impl Channel { Err(e) => Err(e), }?; - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await?; + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await?; } - if cache_pool - .get_cache_key::>(format!("{}_channels", self.guild_uuid)) + if data + .get_cache_key(format!("{}_channels", self.guild_uuid)) .await .is_ok() { - cache_pool - .del_cache_key(format!("{}_channels", self.guild_uuid)) + data.del_cache_key(format!("{}_channels", self.guild_uuid)) .await?; } @@ -243,36 +242,32 @@ impl Channel { pub async fn fetch_messages( &self, - conn: &mut Conn, - cache_pool: &redis::Client, + data: &Data, amount: i64, offset: i64, ) -> Result, Error> { + let mut conn = data.pool.get().await?; + use messages::dsl; - let message_builders: Vec = load_or_empty( + let messages: Vec = load_or_empty( dsl::messages .filter(dsl::channel_uuid.eq(self.uuid)) .select(MessageBuilder::as_select()) .order(dsl::uuid.desc()) .limit(amount) .offset(offset) - .load(conn) + .load(&mut conn) .await, )?; - let mut messages = vec![]; + let message_futures = messages.iter().map(async move |b| b.build(data).await); - for builder in message_builders { - messages.push(builder.build(conn, cache_pool).await?); - } - - Ok(messages) + futures::future::try_join_all(message_futures).await } pub async fn new_message( &self, - conn: &mut Conn, - cache_pool: &redis::Client, + data: &Data, user_uuid: Uuid, message: String, reply_to: Option, @@ -285,103 +280,80 @@ impl Channel { user_uuid, message, reply_to, + is_edited: false, }; + let mut conn = data.pool.get().await?; + insert_into(messages::table) .values(message.clone()) - .execute(conn) + .execute(&mut conn) .await?; - message.build(conn, cache_pool).await + message.build(data).await } - pub async fn set_name( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_name: String, - ) -> Result<(), Error> { + /*pub async fn edit_message(&self, data: &Data, user_uuid: Uuid, message_uuid: Uuid, message: String) -> Result { + use messages::dsl; + + let mut conn = data.pool.get().await?; + + update(messages::table) + .filter(dsl::user_uuid.eq(user_uuid)) + .filter(dsl::uuid.eq(message_uuid)) + .set((dsl::is_edited.eq(true), dsl::message.eq(message))) + .execute(&mut conn) + .await?; + + Ok(()) + }*/ + + pub async fn set_name(&mut self, data: &Data, new_name: String) -> Result<(), Error> { if !CHANNEL_REGEX.is_match(&new_name) { return Err(Error::BadRequest("Channel name is invalid".to_string())); } + let mut conn = data.pool.get().await?; + use channels::dsl; update(channels::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::name.eq(&new_name)) - .execute(conn) + .execute(&mut conn) .await?; self.name = new_name; - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await?; - } - - if cache_pool - .get_cache_key::>(format!("{}_channels", self.guild_uuid)) - .await - .is_ok() - { - cache_pool - .del_cache_key(format!("{}_channels", self.guild_uuid)) - .await?; - } - Ok(()) } pub async fn set_description( &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, + data: &Data, new_description: String, ) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + use channels::dsl; update(channels::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::description.eq(&new_description)) - .execute(conn) + .execute(&mut conn) .await?; self.description = Some(new_description); - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await?; - } - - if cache_pool - .get_cache_key::>(format!("{}_channels", self.guild_uuid)) - .await - .is_ok() - { - cache_pool - .del_cache_key(format!("{}_channels", self.guild_uuid)) - .await?; - } - Ok(()) } - pub async fn move_channel( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_is_above: Uuid, - ) -> Result<(), Error> { + pub async fn move_channel(&mut self, data: &Data, new_is_above: Uuid) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + use channels::dsl; let old_above_uuid: Option = match dsl::channels .filter(dsl::is_above.eq(self.uuid)) .select(dsl::uuid) - .get_result(conn) + .get_result(&mut conn) .await { Ok(r) => Ok(Some(r)), @@ -393,14 +365,14 @@ impl Channel { update(channels::table) .filter(dsl::uuid.eq(uuid)) .set(dsl::is_above.eq(None::)) - .execute(conn) + .execute(&mut conn) .await?; } match update(channels::table) .filter(dsl::is_above.eq(new_is_above)) .set(dsl::is_above.eq(self.uuid)) - .execute(conn) + .execute(&mut conn) .await { Ok(r) => Ok(r), @@ -411,37 +383,19 @@ impl Channel { update(channels::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::is_above.eq(new_is_above)) - .execute(conn) + .execute(&mut conn) .await?; if let Some(uuid) = old_above_uuid { update(channels::table) .filter(dsl::uuid.eq(uuid)) .set(dsl::is_above.eq(self.is_above)) - .execute(conn) + .execute(&mut conn) .await?; } self.is_above = Some(new_is_above); - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await?; - } - - if cache_pool - .get_cache_key::>(format!("{}_channels", self.guild_uuid)) - .await - .is_ok() - { - cache_pool - .del_cache_key(format!("{}_channels", self.guild_uuid)) - .await?; - } - Ok(()) } } diff --git a/src/objects/email_token.rs b/src/objects/email_token.rs index c826620..bfd1ef5 100644 --- a/src/objects/email_token.rs +++ b/src/objects/email_token.rs @@ -3,11 +3,7 @@ use lettre::message::MultiPart; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{ - AppState, - error::Error, - utils::{CacheFns, generate_token}, -}; +use crate::{Data, error::Error, utils::generate_token}; use super::Me; @@ -19,16 +15,18 @@ pub struct EmailToken { } impl EmailToken { - pub async fn get(cache_pool: &redis::Client, user_uuid: Uuid) -> Result { - let email_token = cache_pool - .get_cache_key(format!("{user_uuid}_email_verify")) - .await?; + pub async fn get(data: &Data, user_uuid: Uuid) -> Result { + let email_token = serde_json::from_str( + &data + .get_cache_key(format!("{user_uuid}_email_verify")) + .await?, + )?; Ok(email_token) } #[allow(clippy::new_ret_no_self)] - pub async fn new(app_state: &AppState, me: Me) -> Result<(), Error> { + pub async fn new(data: &Data, me: Me) -> Result<(), Error> { let token = generate_token::<32>()?; let email_token = EmailToken { @@ -38,33 +36,30 @@ impl EmailToken { created_at: Utc::now(), }; - app_state - .cache_pool - .set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400) + data.set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400) .await?; - let mut verify_endpoint = app_state.config.web.frontend_url.join("verify-email")?; + let mut verify_endpoint = data.config.web.frontend_url.join("verify-email")?; verify_endpoint.set_query(Some(&format!("token={token}"))); - let email = app_state + let email = data .mail_client .message_builder() .to(me.email.parse()?) - .subject(format!("{} E-mail Verification", app_state.config.instance.name)) + .subject(format!("{} E-mail Verification", data.config.instance.name)) .multipart(MultiPart::alternative_plain_html( - format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", app_state.config.instance.name, me.username, verify_endpoint), - format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, app_state.config.instance.name, me.username, verify_endpoint) + format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", data.config.instance.name, me.username, verify_endpoint), + format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.instance.name, me.username, verify_endpoint) ))?; - app_state.mail_client.send_mail(email).await?; + data.mail_client.send_mail(email).await?; Ok(()) } - pub async fn delete(&self, cache_pool: &redis::Client) -> Result<(), Error> { - cache_pool - .del_cache_key(format!("{}_email_verify", self.user_uuid)) + pub async fn delete(&self, data: &Data) -> Result<(), Error> { + data.del_cache_key(format!("{}_email_verify", self.user_uuid)) .await?; Ok(()) diff --git a/src/objects/guild.rs b/src/objects/guild.rs index 9640e28..aa01f54 100644 --- a/src/objects/guild.rs +++ b/src/objects/guild.rs @@ -1,16 +1,16 @@ -use axum::body::Bytes; +use actix_web::web::BytesMut; use diesel::{ ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, update, }; -use diesel_async::RunQueryDsl; +use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; use serde::Serialize; use tokio::task; use url::Url; use uuid::Uuid; use crate::{ - AppState, Conn, + Conn, error::Error, schema::{guild_members, guilds, invites}, utils::image_check, @@ -68,11 +68,16 @@ impl Guild { } pub async fn fetch_amount( - conn: &mut Conn, + pool: &deadpool::managed::Pool< + AsyncDieselConnectionManager, + Conn, + >, offset: i64, amount: i64, ) -> Result, Error> { // Fetch guild data from database + let mut conn = pool.get().await?; + use guilds::dsl; let guild_builders: Vec = load_or_empty( dsl::guilds @@ -80,17 +85,18 @@ impl Guild { .order_by(dsl::uuid) .offset(offset) .limit(amount) - .load(conn) + .load(&mut conn) .await, )?; - let mut guilds = vec![]; + // Process each guild concurrently + let guild_futures = guild_builders.iter().map(async move |g| { + let mut conn = pool.get().await?; + g.clone().build(&mut conn).await + }); - for builder in guild_builders { - guilds.push(builder.build(conn).await?); - } - - Ok(guilds) + // Execute all futures concurrently and collect results + futures::future::try_join_all(guild_futures).await } pub async fn new(conn: &mut Conn, name: String, owner_uuid: Uuid) -> Result { @@ -182,9 +188,10 @@ impl Guild { // FIXME: Horrible security pub async fn set_icon( &mut self, + bunny_storage: &bunny_api_tokio::EdgeStorageClient, conn: &mut Conn, - app_state: &AppState, - icon: Bytes, + cdn_url: Url, + icon: BytesMut, ) -> Result<(), Error> { let icon_clone = icon.clone(); let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??; @@ -192,14 +199,14 @@ impl Guild { if let Some(icon) = &self.icon { let relative_url = icon.path().trim_start_matches('/'); - app_state.bunny_storage.delete(relative_url).await?; + bunny_storage.delete(relative_url).await?; } let path = format!("icons/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type); - app_state.bunny_storage.upload(path.clone(), icon).await?; + bunny_storage.upload(path.clone(), icon.into()).await?; - let icon_url = app_state.config.bunny.cdn_url.join(&path)?; + let icon_url = cdn_url.join(&path)?; use guilds::dsl; update(guilds::table) diff --git a/src/objects/me.rs b/src/objects/me.rs index 167e61e..37951ab 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -1,4 +1,4 @@ -use axum::body::Bytes; +use actix_web::web::BytesMut; use diesel::{ ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into, update, @@ -10,11 +10,11 @@ use url::Url; use uuid::Uuid; use crate::{ - AppState, Conn, + Conn, Data, error::Error, objects::{Friend, FriendRequest, User}, schema::{friend_requests, friends, guild_members, guilds, users}, - utils::{CacheFns, EMAIL_REGEX, USERNAME_REGEX, image_check}, + utils::{EMAIL_REGEX, USERNAME_REGEX, image_check}, }; use super::{Guild, guild::GuildBuilder, load_or_empty, member::MemberBuilder}; @@ -29,7 +29,6 @@ pub struct Me { avatar: Option, pronouns: Option, about: Option, - online_status: i16, pub email: String, pub email_verified: bool, } @@ -76,44 +75,40 @@ impl Me { pub async fn set_avatar( &mut self, - conn: &mut Conn, - app_state: &AppState, - avatar: Bytes, + data: &Data, + cdn_url: Url, + avatar: BytesMut, ) -> Result<(), Error> { let avatar_clone = avatar.clone(); let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??; + let mut conn = data.pool.get().await?; + if let Some(avatar) = &self.avatar { let avatar_url: Url = avatar.parse()?; let relative_url = avatar_url.path().trim_start_matches('/'); - app_state.bunny_storage.delete(relative_url).await?; + data.bunny_storage.delete(relative_url).await?; } let path = format!("avatar/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type); - app_state.bunny_storage.upload(path.clone(), avatar).await?; + data.bunny_storage + .upload(path.clone(), avatar.into()) + .await?; - let avatar_url = app_state.config.bunny.cdn_url.join(&path)?; + let avatar_url = cdn_url.join(&path)?; use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::avatar.eq(avatar_url.as_str())) - .execute(conn) + .execute(&mut conn) .await?; - if app_state - .cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - app_state - .cache_pool - .del_cache_key(self.uuid.to_string()) - .await? + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? } self.avatar = Some(avatar_url.to_string()); @@ -132,12 +127,7 @@ impl Me { Ok(()) } - pub async fn set_username( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_username: String, - ) -> Result<(), Error> { + pub async fn set_username(&mut self, data: &Data, new_username: String) -> Result<(), Error> { if !USERNAME_REGEX.is_match(&new_username) || new_username.len() < 3 || new_username.len() > 32 @@ -145,19 +135,17 @@ impl Me { return Err(Error::BadRequest("Invalid username".to_string())); } + let mut conn = data.pool.get().await?; + use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::username.eq(new_username.as_str())) - .execute(conn) + .execute(&mut conn) .await?; - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await? + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? } self.username = new_username; @@ -167,10 +155,11 @@ impl Me { pub async fn set_display_name( &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, + data: &Data, new_display_name: String, ) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + let new_display_name_option = if new_display_name.is_empty() { None } else { @@ -181,15 +170,11 @@ impl Me { update(users::table) .filter(dsl::uuid.eq(self.uuid)) .set(dsl::display_name.eq(&new_display_name_option)) - .execute(conn) + .execute(&mut conn) .await?; - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await? + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? } self.display_name = new_display_name_option; @@ -197,16 +182,13 @@ impl Me { Ok(()) } - pub async fn set_email( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_email: String, - ) -> Result<(), Error> { + pub async fn set_email(&mut self, data: &Data, new_email: String) -> Result<(), Error> { if !EMAIL_REGEX.is_match(&new_email) { return Err(Error::BadRequest("Invalid username".to_string())); } + let mut conn = data.pool.get().await?; + use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) @@ -214,15 +196,11 @@ impl Me { dsl::email.eq(new_email.as_str()), dsl::email_verified.eq(false), )) - .execute(conn) + .execute(&mut conn) .await?; - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await? + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? } self.email = new_email; @@ -230,78 +208,35 @@ impl Me { Ok(()) } - pub async fn set_pronouns( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_pronouns: String, - ) -> Result<(), Error> { + pub async fn set_pronouns(&mut self, data: &Data, new_pronouns: String) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) .set((dsl::pronouns.eq(new_pronouns.as_str()),)) - .execute(conn) + .execute(&mut conn) .await?; - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await? + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? } Ok(()) } - pub async fn set_about( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_about: String, - ) -> Result<(), Error> { + pub async fn set_about(&mut self, data: &Data, new_about: String) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.uuid)) .set((dsl::about.eq(new_about.as_str()),)) - .execute(conn) + .execute(&mut conn) .await?; - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await? - } - - Ok(()) - } - - pub async fn set_online_status( - &mut self, - conn: &mut Conn, - cache_pool: &redis::Client, - new_status: i16, - ) -> Result<(), Error> { - if !(0..=4).contains(&new_status) { - return Err(Error::BadRequest("Invalid status code".to_string())); - } - self.online_status = new_status; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::online_status.eq(new_status)) - .execute(conn) - .await?; - - if cache_pool - .get_cache_key::(self.uuid.to_string()) - .await - .is_ok() - { - cache_pool.del_cache_key(self.uuid.to_string()).await? + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? } Ok(()) @@ -417,18 +352,16 @@ impl Me { Ok(()) } - pub async fn get_friends( - &self, - conn: &mut Conn, - cache_pool: &redis::Client, - ) -> Result, Error> { + pub async fn get_friends(&self, data: &Data) -> Result, Error> { use friends::dsl; + let mut conn = data.pool.get().await?; + let friends1 = load_or_empty( dsl::friends .filter(dsl::uuid1.eq(self.uuid)) .select(Friend::as_select()) - .load(conn) + .load(&mut conn) .await, )?; @@ -436,21 +369,21 @@ impl Me { dsl::friends .filter(dsl::uuid2.eq(self.uuid)) .select(Friend::as_select()) - .load(conn) + .load(&mut conn) .await, )?; - let mut friends = vec![]; + let friend_futures = friends1.iter().map(async move |friend| { + User::fetch_one_with_friendship(data, self, friend.uuid2).await + }); - for friend in friends1 { - friends - .push(User::fetch_one_with_friendship(conn, cache_pool, self, friend.uuid2).await?); - } + let mut friends = futures::future::try_join_all(friend_futures).await?; - for friend in friends2 { - friends - .push(User::fetch_one_with_friendship(conn, cache_pool, self, friend.uuid1).await?); - } + let friend_futures = friends2.iter().map(async move |friend| { + User::fetch_one_with_friendship(data, self, friend.uuid1).await + }); + + friends.append(&mut futures::future::try_join_all(friend_futures).await?); Ok(friends) } diff --git a/src/objects/member.rs b/src/objects/member.rs index f7e56da..c2a71d9 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -1,31 +1,21 @@ use diesel::{ - Associations, BoolExpressionMethods, ExpressionMethods, Identifiable, Insertable, JoinOnDsl, - QueryDsl, Queryable, Selectable, SelectableHelper, define_sql_function, delete, insert_into, - sql_types::{Nullable, VarChar}, + ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, }; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - Conn, + Conn, Data, error::Error, - objects::PaginationRequest, - schema::{friends, guild_bans, guild_members, users}, + objects::{Me, Permissions, Role}, + schema::guild_members, }; -use super::{ - Friend, Guild, GuildBan, Me, Pagination, Permissions, Role, User, load_or_empty, - user::UserBuilder, -}; +use super::{User, load_or_empty}; -define_sql_function! { fn coalesce(x: Nullable, y: Nullable, z: VarChar) -> Text; } - -#[derive(Serialize, Queryable, Identifiable, Selectable, Insertable, Associations)] +#[derive(Serialize, Queryable, Selectable, Insertable)] #[diesel(table_name = guild_members)] -#[diesel(belongs_to(UserBuilder, foreign_key = user_uuid))] -#[diesel(belongs_to(Guild, foreign_key = guild_uuid))] -#[diesel(primary_key(uuid))] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct MemberBuilder { pub uuid: Uuid, @@ -36,22 +26,15 @@ pub struct MemberBuilder { } impl MemberBuilder { - pub async fn build( - &self, - conn: &mut Conn, - cache_pool: &redis::Client, - me: Option<&Me>, - ) -> Result { + pub async fn build(&self, data: &Data, me: Option<&Me>) -> Result { let user; if let Some(me) = me { - user = User::fetch_one_with_friendship(conn, cache_pool, me, self.user_uuid).await?; + user = User::fetch_one_with_friendship(data, me, self.user_uuid).await?; } else { - user = User::fetch_one(conn, cache_pool, self.user_uuid).await?; + user = User::fetch_one(data, self.user_uuid).await?; } - let roles = Role::fetch_from_member(conn, cache_pool, self).await?; - Ok(Member { uuid: self.uuid, nickname: self.nickname.clone(), @@ -59,44 +42,16 @@ impl MemberBuilder { guild_uuid: self.guild_uuid, is_owner: self.is_owner, user, - roles, - }) - } - - async fn build_with_parts( - &self, - conn: &mut Conn, - cache_pool: &redis::Client, - user_builder: UserBuilder, - friend: Option, - ) -> Result { - let mut user = user_builder.build(); - - if let Some(friend) = friend { - user.friends_since = Some(friend.accepted_at); - } - - let roles = Role::fetch_from_member(conn, cache_pool, self).await?; - - Ok(Member { - uuid: self.uuid, - nickname: self.nickname.clone(), - user_uuid: self.user_uuid, - guild_uuid: self.guild_uuid, - is_owner: self.is_owner, - user, - roles, }) } pub async fn check_permission( &self, - conn: &mut Conn, - cache_pool: &redis::Client, + data: &Data, permission: Permissions, ) -> Result<(), Error> { if !self.is_owner { - let roles = Role::fetch_from_member(conn, cache_pool, self).await?; + let roles = Role::fetch_from_member(data, self.uuid).await?; let allowed = roles.iter().any(|r| r.permissions & permission as i64 != 0); if !allowed { return Err(Error::Forbidden("Not allowed".to_string())); @@ -107,16 +62,14 @@ impl MemberBuilder { } } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Serialize, Deserialize)] pub struct Member { pub uuid: Uuid, pub nickname: Option, - #[serde(skip)] pub user_uuid: Uuid, pub guild_uuid: Uuid, pub is_owner: bool, user: User, - roles: Vec, } impl Member { @@ -148,173 +101,47 @@ impl Member { } pub async fn fetch_one( - conn: &mut Conn, - cache_pool: &redis::Client, - me: Option<&Me>, + data: &Data, + me: &Me, user_uuid: Uuid, guild_uuid: Uuid, ) -> Result { - let member: MemberBuilder; - let user: UserBuilder; - let friend: Option; - use friends::dsl as fdsl; + let mut conn = data.pool.get().await?; + use guild_members::dsl; - if let Some(me) = me { - (member, user, friend) = dsl::guild_members - .filter(dsl::guild_uuid.eq(guild_uuid)) - .filter(dsl::user_uuid.eq(user_uuid)) - .inner_join(users::table) - .left_join( - fdsl::friends.on(fdsl::uuid1 - .eq(me.uuid) - .and(fdsl::uuid2.eq(users::uuid)) - .or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))), - ) - .select(( - MemberBuilder::as_select(), - UserBuilder::as_select(), - Option::::as_select(), - )) - .get_result(conn) - .await?; - } else { - (member, user) = dsl::guild_members - .filter(dsl::guild_uuid.eq(guild_uuid)) - .filter(dsl::user_uuid.eq(user_uuid)) - .inner_join(users::table) - .select((MemberBuilder::as_select(), UserBuilder::as_select())) - .get_result(conn) - .await?; + let member: MemberBuilder = dsl::guild_members + .filter(dsl::user_uuid.eq(user_uuid)) + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(MemberBuilder::as_select()) + .get_result(&mut conn) + .await?; - friend = None; - } - - member - .build_with_parts(conn, cache_pool, user, friend) - .await + member.build(data, Some(me)).await } - pub async fn fetch_one_with_uuid( - conn: &mut Conn, - cache_pool: &redis::Client, - me: Option<&Me>, - uuid: Uuid, - ) -> Result { - let member: MemberBuilder; - let user: UserBuilder; - let friend: Option; - use friends::dsl as fdsl; + pub async fn fetch_all(data: &Data, me: &Me, guild_uuid: Uuid) -> Result, Error> { + let mut conn = data.pool.get().await?; + use guild_members::dsl; - if let Some(me) = me { - (member, user, friend) = dsl::guild_members - .filter(dsl::uuid.eq(uuid)) - .inner_join(users::table) - .left_join( - fdsl::friends.on(fdsl::uuid1 - .eq(me.uuid) - .and(fdsl::uuid2.eq(users::uuid)) - .or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))), - ) - .select(( - MemberBuilder::as_select(), - UserBuilder::as_select(), - Option::::as_select(), - )) - .get_result(conn) - .await?; - } else { - (member, user) = dsl::guild_members - .filter(dsl::uuid.eq(uuid)) - .inner_join(users::table) - .select((MemberBuilder::as_select(), UserBuilder::as_select())) - .get_result(conn) - .await?; - - friend = None; - } - - member - .build_with_parts(conn, cache_pool, user, friend) - .await - } - - pub async fn fetch_page( - conn: &mut Conn, - cache_pool: &redis::Client, - me: &Me, - guild_uuid: Uuid, - pagination: PaginationRequest, - ) -> Result, Error> { - let per_page = pagination.per_page.unwrap_or(50); - let page_multiplier: i64 = ((pagination.page - 1) * per_page).into(); - - if !(10..=100).contains(&per_page) { - return Err(Error::BadRequest( - "Invalid amount per page requested".to_string(), - )); - } - - use friends::dsl as fdsl; - use guild_members::dsl; - let member_builders: Vec<(MemberBuilder, UserBuilder, Option)> = load_or_empty( + let member_builders: Vec = load_or_empty( dsl::guild_members .filter(dsl::guild_uuid.eq(guild_uuid)) - .inner_join(users::table) - .left_join( - fdsl::friends.on(fdsl::uuid1 - .eq(me.uuid) - .and(fdsl::uuid2.eq(users::uuid)) - .or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))), - ) - .limit(per_page.into()) - .offset(page_multiplier) - .order_by(coalesce( - dsl::nickname, - users::display_name, - users::username, - )) - .select(( - MemberBuilder::as_select(), - UserBuilder::as_select(), - Option::::as_select(), - )) - .load(conn) + .select(MemberBuilder::as_select()) + .load(&mut conn) .await, )?; - let pages = Member::count(conn, guild_uuid).await? as f32 / per_page as f32; + let mut members = vec![]; - let mut members = Pagination:: { - objects: Vec::with_capacity(member_builders.len()), - amount: member_builders.len() as i32, - pages: pages.ceil() as i32, - page: pagination.page, - }; - - for (member, user, friend) in member_builders { - members.objects.push( - member - .build_with_parts(conn, cache_pool, user, friend) - .await?, - ); + for builder in member_builders { + members.push(builder.build(&data, Some(me)).await?); } Ok(members) } - pub async fn new( - conn: &mut Conn, - cache_pool: &redis::Client, - user_uuid: Uuid, - guild_uuid: Uuid, - ) -> Result { - let banned = GuildBan::fetch_one(conn, guild_uuid, user_uuid).await; - - match banned { - Ok(_) => Err(Error::Forbidden("User banned".to_string())), - Err(Error::SqlError(diesel::result::Error::NotFound)) => Ok(()), - Err(e) => Err(e), - }?; + pub async fn new(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { + let mut conn = data.pool.get().await?; let member_uuid = Uuid::now_v7(); @@ -328,51 +155,9 @@ impl Member { insert_into(guild_members::table) .values(&member) - .execute(conn) + .execute(&mut conn) .await?; - member.build(conn, cache_pool, None).await - } - - pub async fn delete(self, conn: &mut Conn) -> Result<(), Error> { - if self.is_owner { - return Err(Error::Forbidden("Can not kick owner".to_string())); - } - delete(guild_members::table) - .filter(guild_members::uuid.eq(self.uuid)) - .execute(conn) - .await?; - - Ok(()) - } - - pub async fn ban(self, conn: &mut Conn, reason: &String) -> Result<(), Error> { - if self.is_owner { - return Err(Error::Forbidden("Can not ban owner".to_string())); - } - - use guild_bans::dsl; - insert_into(guild_bans::table) - .values(( - dsl::guild_uuid.eq(self.guild_uuid), - dsl::user_uuid.eq(self.user_uuid), - dsl::reason.eq(reason), - )) - .execute(conn) - .await?; - - self.delete(conn).await?; - - Ok(()) - } - - pub fn to_builder(&self) -> MemberBuilder { - MemberBuilder { - uuid: self.uuid, - nickname: self.nickname.clone(), - user_uuid: self.user_uuid, - guild_uuid: self.guild_uuid, - is_owner: self.is_owner, - } + member.build(data, None).await } } diff --git a/src/objects/message.rs b/src/objects/message.rs index f30f14d..588886b 100644 --- a/src/objects/message.rs +++ b/src/objects/message.rs @@ -1,15 +1,10 @@ -use diesel::{ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable}; -use diesel_async::RunQueryDsl; +use diesel::{Insertable, Queryable, Selectable}; use serde::Serialize; use uuid::Uuid; -use crate::{ - Conn, - error::Error, - schema::{channels, guilds, messages}, -}; +use crate::{Data, error::Error, schema::messages}; -use super::Member; +use super::User; #[derive(Clone, Queryable, Selectable, Insertable)] #[diesel(table_name = messages)] @@ -20,24 +15,12 @@ pub struct MessageBuilder { pub user_uuid: Uuid, pub message: String, pub reply_to: Option, + pub is_edited: bool, } impl MessageBuilder { - pub async fn build( - &self, - conn: &mut Conn, - cache_pool: &redis::Client, - ) -> Result { - use channels::dsl; - - let guild_uuid = dsl::channels - .filter(dsl::uuid.eq(self.channel_uuid)) - .inner_join(guilds::table) - .select(guilds::uuid) - .get_result(conn) - .await?; - - let member = Member::fetch_one(conn, cache_pool, None, self.user_uuid, guild_uuid).await?; + pub async fn build(&self, data: &Data) -> Result { + let user = User::fetch_one(data, self.user_uuid).await?; Ok(Message { uuid: self.uuid, @@ -45,7 +28,8 @@ impl MessageBuilder { user_uuid: self.user_uuid, message: self.message.clone(), reply_to: self.reply_to, - member, + is_edited: self.is_edited, + user, }) } } @@ -57,5 +41,6 @@ pub struct Message { user_uuid: Uuid, message: String, reply_to: Option, - member: Member, + is_edited: bool, + user: User, } diff --git a/src/objects/mod.rs b/src/objects/mod.rs index 5a013ca..6139eea 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -4,10 +4,10 @@ use lettre::{ transport::smtp::authentication::Credentials, }; use log::debug; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use uuid::Uuid; -mod bans; +mod categories; mod channel; mod email_token; mod friends; @@ -20,7 +20,6 @@ mod password_reset_token; mod role; mod user; -pub use bans::GuildBan; pub use channel::Channel; pub use email_token::EmailToken; pub use friends::Friend; @@ -44,51 +43,6 @@ pub trait HasUuid { pub trait HasIsAbove { fn is_above(&self) -> Option<&Uuid>; } -/* -pub trait Cookies { - fn cookies(&self) -> CookieJar; - fn cookie>(&self, cookie: T) -> Option; -} - -impl Cookies for Request { - fn cookies(&self) -> CookieJar { - let cookies = self.headers() - .get(axum::http::header::COOKIE) - .and_then(|value| value.to_str().ok()) - .map(|s| Cookie::split_parse(s.to_string())) - .and_then(|c| c.collect::, cookie::ParseError>>().ok()) - .unwrap_or(vec![]); - - let mut cookie_jar = CookieJar::new(); - - for cookie in cookies { - cookie_jar.add(cookie) - } - - cookie_jar - } - - fn cookie>(&self, cookie: T) -> Option { - self.cookies() - .get(cookie.as_ref()) - .and_then(|c| Some(c.to_owned())) - } -} -*/ - -#[derive(Serialize)] -pub struct Pagination { - objects: Vec, - amount: i32, - pages: i32, - page: i32, -} - -#[derive(Deserialize)] -pub struct PaginationRequest { - pub page: i32, - pub per_page: Option, -} fn load_or_empty( query_result: Result, diesel::result::Error>, diff --git a/src/objects/password_reset_token.rs b/src/objects/password_reset_token.rs index ca5c62f..7f714ef 100644 --- a/src/objects/password_reset_token.rs +++ b/src/objects/password_reset_token.rs @@ -10,10 +10,10 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - AppState, Conn, + Data, error::Error, schema::users, - utils::{CacheFns, PASSWORD_REGEX, generate_token, global_checks, user_uuid_from_identifier}, + utils::{PASSWORD_REGEX, generate_token, global_checks, user_uuid_from_identifier}, }; #[derive(Serialize, Deserialize)] @@ -24,49 +24,49 @@ pub struct PasswordResetToken { } impl PasswordResetToken { - pub async fn get( - cache_pool: &redis::Client, - token: String, - ) -> Result { - let user_uuid: Uuid = cache_pool.get_cache_key(token.to_string()).await?; - let password_reset_token = cache_pool - .get_cache_key(format!("{user_uuid}_password_reset")) - .await?; + pub async fn get(data: &Data, token: String) -> Result { + let user_uuid: Uuid = serde_json::from_str(&data.get_cache_key(token.to_string()).await?)?; + let password_reset_token = serde_json::from_str( + &data + .get_cache_key(format!("{user_uuid}_password_reset")) + .await?, + )?; Ok(password_reset_token) } pub async fn get_with_identifier( - conn: &mut Conn, - cache_pool: &redis::Client, + data: &Data, identifier: String, ) -> Result { - let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; + let mut conn = data.pool.get().await?; - let password_reset_token = cache_pool - .get_cache_key(format!("{user_uuid}_password_reset")) - .await?; + let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; + + let password_reset_token = serde_json::from_str( + &data + .get_cache_key(format!("{user_uuid}_password_reset")) + .await?, + )?; Ok(password_reset_token) } #[allow(clippy::new_ret_no_self)] - pub async fn new( - conn: &mut Conn, - app_state: &AppState, - identifier: String, - ) -> Result<(), Error> { + pub async fn new(data: &Data, identifier: String) -> Result<(), Error> { let token = generate_token::<32>()?; - let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; + let mut conn = data.pool.get().await?; - global_checks(conn, &app_state.config, user_uuid).await?; + let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; + + global_checks(data, user_uuid).await?; use users::dsl as udsl; let (username, email_address): (String, String) = udsl::users .filter(udsl::uuid.eq(user_uuid)) .select((udsl::username, udsl::email)) - .get_result(conn) + .get_result(&mut conn) .await?; let password_reset_token = PasswordResetToken { @@ -75,44 +75,34 @@ impl PasswordResetToken { created_at: Utc::now(), }; - app_state - .cache_pool - .set_cache_key( - format!("{user_uuid}_password_reset"), - password_reset_token, - 86400, - ) - .await?; - app_state - .cache_pool - .set_cache_key(token.clone(), user_uuid, 86400) - .await?; + data.set_cache_key( + format!("{user_uuid}_password_reset"), + password_reset_token, + 86400, + ) + .await?; + data.set_cache_key(token.clone(), user_uuid, 86400).await?; - let mut reset_endpoint = app_state.config.web.frontend_url.join("reset-password")?; + let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; reset_endpoint.set_query(Some(&format!("token={token}"))); - let email = app_state + let email = data .mail_client .message_builder() .to(email_address.parse()?) - .subject(format!("{} Password Reset", app_state.config.instance.name)) + .subject(format!("{} Password Reset", data.config.instance.name)) .multipart(MultiPart::alternative_plain_html( - format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", app_state.config.instance.name, username, reset_endpoint), - format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, app_state.config.instance.name, username, reset_endpoint) + format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", data.config.instance.name, username, reset_endpoint), + format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, data.config.instance.name, username, reset_endpoint) ))?; - app_state.mail_client.send_mail(email).await?; + data.mail_client.send_mail(email).await?; Ok(()) } - pub async fn set_password( - &self, - conn: &mut Conn, - app_state: &AppState, - password: String, - ) -> Result<(), Error> { + pub async fn set_password(&self, data: &Data, password: String) -> Result<(), Error> { if !PASSWORD_REGEX.is_match(&password) { return Err(Error::BadRequest( "Please provide a valid password".to_string(), @@ -121,46 +111,47 @@ impl PasswordResetToken { let salt = SaltString::generate(&mut OsRng); - let hashed_password = app_state + let hashed_password = data .argon2 .hash_password(password.as_bytes(), &salt) .map_err(|e| Error::PasswordHashError(e.to_string()))?; + let mut conn = data.pool.get().await?; + use users::dsl; update(users::table) .filter(dsl::uuid.eq(self.user_uuid)) .set(dsl::password.eq(hashed_password.to_string())) - .execute(conn) + .execute(&mut conn) .await?; let (username, email_address): (String, String) = dsl::users .filter(dsl::uuid.eq(self.user_uuid)) .select((dsl::username, dsl::email)) - .get_result(conn) + .get_result(&mut conn) .await?; - let login_page = app_state.config.web.frontend_url.join("login")?; + let login_page = data.config.web.frontend_url.join("login")?; - let email = app_state + let email = data .mail_client .message_builder() .to(email_address.parse()?) - .subject(format!("Your {} Password has been Reset", app_state.config.instance.name)) + .subject(format!("Your {} Password has been Reset", data.config.instance.name)) .multipart(MultiPart::alternative_plain_html( - format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password immediately.\n\n{}\n\nThanks, The gorb team.", app_state.config.instance.name, username, login_page), - format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, app_state.config.instance.name, username, login_page) + format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password immediately.\n\n{}\n\nThanks, The gorb team.", data.config.instance.name, username, login_page), + format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, data.config.instance.name, username, login_page) ))?; - app_state.mail_client.send_mail(email).await?; + data.mail_client.send_mail(email).await?; - self.delete(&app_state.cache_pool).await + self.delete(data).await } - pub async fn delete(&self, cache_pool: &redis::Client) -> Result<(), Error> { - cache_pool - .del_cache_key(format!("{}_password_reset", &self.user_uuid)) + pub async fn delete(&self, data: &Data) -> Result<(), Error> { + data.del_cache_key(format!("{}_password_reset", &self.user_uuid)) .await?; - cache_pool.del_cache_key(self.token.to_string()).await?; + data.del_cache_key(self.token.to_string()).await?; Ok(()) } diff --git a/src/objects/role.rs b/src/objects/role.rs index cc71fb2..68e9c27 100644 --- a/src/objects/role.rs +++ b/src/objects/role.rs @@ -1,24 +1,22 @@ -use diesel::query_dsl::BelongingToDsl; use diesel::{ - Associations, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, Selectable, - SelectableHelper, insert_into, update, + ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, + update, }; use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - Conn, + Conn, Data, error::Error, schema::{role_members, roles}, - utils::{CacheFns, order_by_is_above}, + utils::order_by_is_above, }; -use super::{HasIsAbove, HasUuid, load_or_empty, member::MemberBuilder}; +use super::{HasIsAbove, HasUuid, load_or_empty}; -#[derive(Deserialize, Serialize, Clone, Identifiable, Queryable, Selectable, Insertable)] +#[derive(Deserialize, Serialize, Clone, Queryable, Selectable, Insertable)] #[diesel(table_name = roles)] -#[diesel(primary_key(uuid))] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Role { uuid: Uuid, @@ -29,17 +27,27 @@ pub struct Role { pub permissions: i64, } -#[derive(Serialize, Clone, Identifiable, Queryable, Selectable, Insertable, Associations)] +#[derive(Serialize, Clone, Queryable, Selectable, Insertable)] #[diesel(table_name = role_members)] -#[diesel(belongs_to(MemberBuilder, foreign_key = member_uuid))] -#[diesel(belongs_to(Role, foreign_key = role_uuid))] -#[diesel(primary_key(role_uuid, member_uuid))] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct RoleMember { role_uuid: Uuid, member_uuid: Uuid, } +impl RoleMember { + async fn fetch_role(&self, conn: &mut Conn) -> Result { + use roles::dsl; + let role: Role = dsl::roles + .filter(dsl::uuid.eq(self.role_uuid)) + .select(Role::as_select()) + .get_result(conn) + .await?; + + Ok(role) + } +} + impl HasUuid for Role { fn uuid(&self) -> &Uuid { self.uuid.as_ref() @@ -66,28 +74,29 @@ impl Role { Ok(roles) } - pub async fn fetch_from_member( - conn: &mut Conn, - cache_pool: &redis::Client, - member: &MemberBuilder, - ) -> Result, Error> { - if let Ok(roles) = cache_pool - .get_cache_key(format!("{}_roles", member.uuid)) - .await - { - return Ok(roles); + pub async fn fetch_from_member(data: &Data, member_uuid: Uuid) -> Result, Error> { + if let Ok(roles) = data.get_cache_key(format!("{member_uuid}_roles")).await { + return Ok(serde_json::from_str(&roles)?); } - let roles: Vec = load_or_empty( - RoleMember::belonging_to(member) - .inner_join(roles::table) - .select(Role::as_select()) - .load(conn) + let mut conn = data.pool.get().await?; + + use role_members::dsl; + let role_memberships: Vec = load_or_empty( + dsl::role_members + .filter(dsl::member_uuid.eq(member_uuid)) + .select(RoleMember::as_select()) + .load(&mut conn) .await, )?; - cache_pool - .set_cache_key(format!("{}_roles", member.uuid), roles.clone(), 300) + let mut roles = vec![]; + + for membership in role_memberships { + roles.push(membership.fetch_role(&mut conn).await?); + } + + data.set_cache_key(format!("{member_uuid}_roles"), roles.clone(), 300) .await?; Ok(roles) @@ -160,10 +169,6 @@ pub enum Permissions { ManageGuild = 32, /// Lets users change member settings (nickname, etc) ManageMember = 64, - /// Lets users ban members - BanMember = 128, - /// Lets users kick members - KickMember = 256, } impl Permissions { @@ -176,8 +181,6 @@ impl Permissions { Self::ManageInvite, Self::ManageGuild, Self::ManageMember, - Self::BanMember, - Self::KickMember, ]; all_perms diff --git a/src/objects/user.rs b/src/objects/user.rs index c91b809..8e42351 100644 --- a/src/objects/user.rs +++ b/src/objects/user.rs @@ -4,7 +4,7 @@ use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{Conn, error::Error, objects::Me, schema::users, utils::CacheFns}; +use crate::{Conn, Data, error::Error, objects::Me, schema::users}; use super::load_or_empty; @@ -18,11 +18,10 @@ pub struct UserBuilder { avatar: Option, pronouns: Option, about: Option, - online_status: i16, } impl UserBuilder { - pub fn build(self) -> User { + fn build(self) -> User { User { uuid: self.uuid, username: self.username, @@ -30,7 +29,6 @@ impl UserBuilder { avatar: self.avatar, pronouns: self.pronouns, about: self.about, - online_status: self.online_status, friends_since: None, } } @@ -38,51 +36,48 @@ impl UserBuilder { #[derive(Deserialize, Serialize, Clone)] pub struct User { - pub uuid: Uuid, + uuid: Uuid, username: String, display_name: Option, avatar: Option, pronouns: Option, about: Option, - online_status: i16, - pub friends_since: Option>, + friends_since: Option>, } impl User { - pub async fn fetch_one( - conn: &mut Conn, - cache_pool: &redis::Client, - user_uuid: Uuid, - ) -> Result { - if let Ok(cache_hit) = cache_pool.get_cache_key(user_uuid.to_string()).await { - return Ok(cache_hit); + pub async fn fetch_one(data: &Data, user_uuid: Uuid) -> Result { + let mut conn = data.pool.get().await?; + + if let Ok(cache_hit) = data.get_cache_key(user_uuid.to_string()).await { + return Ok(serde_json::from_str(&cache_hit)?); } use users::dsl; let user_builder: UserBuilder = dsl::users .filter(dsl::uuid.eq(user_uuid)) .select(UserBuilder::as_select()) - .get_result(conn) + .get_result(&mut conn) .await?; let user = user_builder.build(); - cache_pool - .set_cache_key(user_uuid.to_string(), user.clone(), 1800) + data.set_cache_key(user_uuid.to_string(), user.clone(), 1800) .await?; Ok(user) } pub async fn fetch_one_with_friendship( - conn: &mut Conn, - cache_pool: &redis::Client, + data: &Data, me: &Me, user_uuid: Uuid, ) -> Result { - let mut user = Self::fetch_one(conn, cache_pool, user_uuid).await?; + let mut conn = data.pool.get().await?; - if let Some(friend) = me.friends_with(conn, user_uuid).await? { + let mut user = Self::fetch_one(data, user_uuid).await?; + + if let Some(friend) = me.friends_with(&mut conn, user_uuid).await? { user.friends_since = Some(friend.accepted_at); } diff --git a/src/schema.rs b/src/schema.rs index 88f6155..ed5e1a5 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -11,6 +11,18 @@ diesel::table! { } } +diesel::table! { + categories (uuid) { + uuid -> Uuid, + guild_uuid -> Uuid, + #[max_length = 32] + name -> Varchar, + #[max_length = 500] + description -> Nullable, + is_above -> Nullable, + } +} + diesel::table! { channel_permissions (channel_uuid, role_uuid) { channel_uuid -> Uuid, @@ -28,6 +40,7 @@ diesel::table! { #[max_length = 500] description -> Nullable, is_above -> Nullable, + in_category -> Nullable, } } @@ -47,16 +60,6 @@ diesel::table! { } } -diesel::table! { - guild_bans (user_uuid, guild_uuid) { - guild_uuid -> Uuid, - user_uuid -> Uuid, - #[max_length = 200] - reason -> Nullable, - banned_since -> Timestamptz, - } -} - diesel::table! { guild_members (uuid) { uuid -> Uuid, @@ -104,6 +107,7 @@ diesel::table! { #[max_length = 4000] message -> Varchar, reply_to -> Nullable, + is_edited -> Bool, } } @@ -113,7 +117,7 @@ diesel::table! { token -> Varchar, uuid -> Uuid, created_at -> Int8, - #[max_length = 64] + #[max_length = 16] device_name -> Varchar, } } @@ -126,7 +130,7 @@ diesel::table! { } diesel::table! { - roles (uuid) { + roles (uuid, guild_uuid) { uuid -> Uuid, guild_uuid -> Uuid, #[max_length = 50] @@ -157,17 +161,15 @@ diesel::table! { pronouns -> Nullable, #[max_length = 200] about -> Nullable, - online_status -> Int2, } } diesel::joinable!(access_tokens -> refresh_tokens (refresh_token)); diesel::joinable!(access_tokens -> users (uuid)); +diesel::joinable!(categories -> guilds (guild_uuid)); diesel::joinable!(channel_permissions -> channels (channel_uuid)); -diesel::joinable!(channel_permissions -> roles (role_uuid)); +diesel::joinable!(channels -> categories (in_category)); diesel::joinable!(channels -> guilds (guild_uuid)); -diesel::joinable!(guild_bans -> guilds (guild_uuid)); -diesel::joinable!(guild_bans -> users (user_uuid)); diesel::joinable!(guild_members -> guilds (guild_uuid)); diesel::joinable!(guild_members -> users (user_uuid)); diesel::joinable!(instance_permissions -> users (uuid)); @@ -177,16 +179,15 @@ diesel::joinable!(messages -> channels (channel_uuid)); diesel::joinable!(messages -> users (user_uuid)); diesel::joinable!(refresh_tokens -> users (uuid)); diesel::joinable!(role_members -> guild_members (member_uuid)); -diesel::joinable!(role_members -> roles (role_uuid)); diesel::joinable!(roles -> guilds (guild_uuid)); diesel::allow_tables_to_appear_in_same_query!( access_tokens, + categories, channel_permissions, channels, friend_requests, friends, - guild_bans, guild_members, guilds, instance_permissions, diff --git a/src/socket.rs b/src/socket.rs deleted file mode 100644 index 3fcae32..0000000 --- a/src/socket.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::sync::Arc; - -use log::info; -use rmpv::Value; -use socketioxide::extract::{AckSender, Data, SocketRef, State}; - -use crate::AppState; - -pub async fn on_connect( - State(_app_state): State>, - socket: SocketRef, - Data(data): Data, -) { - socket.emit("auth", &data).ok(); - - socket.on("message", async |socket: SocketRef, Data::(data)| { - info!("{data}"); - socket.emit("message-back", &data).ok(); - }); - - socket.on( - "message-with-ack", - async |Data::(data), ack: AckSender| { - info!("{data}"); - ack.send(&data).ok(); - }, - ); -} diff --git a/src/utils.rs b/src/utils.rs index 7ef880e..7a5581a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,25 +1,26 @@ -use rand::seq::IndexedRandom; use std::sync::LazyLock; -use axum::body::Bytes; -use axum_extra::extract::cookie::{Cookie, SameSite}; +use actix_web::{ + cookie::{Cookie, SameSite, time::Duration}, + http::header::HeaderMap, + web::BytesMut, +}; use bindet::FileType; use diesel::{ExpressionMethods, QueryDsl}; use diesel_async::RunQueryDsl; use getrandom::fill; use hex::encode; +use redis::RedisError; use regex::Regex; -use serde::{Serialize, de::DeserializeOwned}; -use time::Duration; +use serde::Serialize; use uuid::Uuid; use crate::{ - Conn, + Conn, Data, config::Config, error::Error, objects::{HasIsAbove, HasUuid}, schema::users, - wordlist::{ADJECTIVES, ANIMALS}, }; pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { @@ -32,16 +33,86 @@ pub static USERNAME_REGEX: LazyLock = pub static CHANNEL_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); +// Password is expected to be hashed using SHA3-384 pub static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); -pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cookie { - Cookie::build(("refresh_token", refresh_token)) +pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, Error> { + let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); + + if auth_token.is_none() { + return Err(Error::Unauthorized( + "No authorization header provided".to_string(), + )); + } + + let auth_raw = auth_token.unwrap().to_str()?; + + let mut auth = auth_raw.split_whitespace(); + + let auth_type = auth.next(); + + let auth_value = auth.next(); + + if auth_type.is_none() { + return Err(Error::BadRequest( + "Authorization header is empty".to_string(), + )); + } else if auth_type.is_some_and(|at| at != "Bearer") { + return Err(Error::BadRequest( + "Only token auth is supported".to_string(), + )); + } + + if auth_value.is_none() { + return Err(Error::BadRequest("No token provided".to_string())); + } + + Ok(auth_value.unwrap()) +} + +pub fn get_ws_protocol_header(headers: &HeaderMap) -> Result<&str, Error> { + let auth_token = headers.get(actix_web::http::header::SEC_WEBSOCKET_PROTOCOL); + + if auth_token.is_none() { + return Err(Error::Unauthorized( + "No authorization header provided".to_string(), + )); + } + + let auth_raw = auth_token.unwrap().to_str()?; + + let mut auth = auth_raw.split_whitespace(); + + let response_proto = auth.next(); + + let auth_value = auth.next(); + + if response_proto.is_none() { + return Err(Error::BadRequest( + "Sec-WebSocket-Protocol header is empty".to_string(), + )); + } else if response_proto.is_some_and(|rp| rp != "Authorization,") { + return Err(Error::BadRequest( + "First protocol should be Authorization".to_string(), + )); + } + + if auth_value.is_none() { + return Err(Error::BadRequest("No token provided".to_string())); + } + + Ok(auth_value.unwrap()) +} + +pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cookie<'static> { + Cookie::build("refresh_token", refresh_token) .http_only(true) .secure(true) .same_site(SameSite::None) + //.domain(config.web.backend_url.domain().unwrap().to_string()) .path(config.web.backend_url.path().to_string()) .max_age(Duration::days(30)) - .build() + .finish() } pub fn generate_token() -> Result { @@ -50,7 +121,7 @@ pub fn generate_token() -> Result { Ok(encode(buf)) } -pub fn image_check(icon: Bytes) -> Result { +pub fn image_check(icon: BytesMut) -> Result { let buf = std::io::Cursor::new(icon); let detect = bindet::detect(buf).map_err(|e| e.kind()); @@ -97,30 +168,15 @@ pub async fn user_uuid_from_identifier( } } -pub async fn user_uuid_from_username(conn: &mut Conn, username: &String) -> Result { - if USERNAME_REGEX.is_match(username) { - use users::dsl; - let user_uuid = dsl::users - .filter(dsl::username.eq(username)) - .select(dsl::uuid) - .get_result(conn) - .await?; +pub async fn global_checks(data: &Data, user_uuid: Uuid) -> Result<(), Error> { + if data.config.instance.require_email_verification { + let mut conn = data.pool.get().await?; - Ok(user_uuid) - } else { - Err(Error::BadRequest( - "Please provide a valid username".to_string(), - )) - } -} - -pub async fn global_checks(conn: &mut Conn, config: &Config, user_uuid: Uuid) -> Result<(), Error> { - if config.instance.require_email_verification { use users::dsl; let email_verified: bool = dsl::users .filter(dsl::uuid.eq(user_uuid)) .select(dsl::email_verified) - .get_result(conn) + .get_result(&mut conn) .await?; if !email_verified { @@ -158,28 +214,14 @@ where Ok(ordered) } -#[allow(async_fn_in_trait)] -pub trait CacheFns { - async fn set_cache_key( - &self, - key: String, - value: impl Serialize, - expire: u32, - ) -> Result<(), Error>; - async fn get_cache_key(&self, key: String) -> Result - where - T: DeserializeOwned; - async fn del_cache_key(&self, key: String) -> Result<(), Error>; -} - -impl CacheFns for redis::Client { - async fn set_cache_key( +impl Data { + pub async fn set_cache_key( &self, key: String, value: impl Serialize, expire: u32, ) -> Result<(), Error> { - let mut conn = self.get_multiplexed_tokio_connection().await?; + let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; let key_encoded = encode(key); @@ -198,39 +240,25 @@ impl CacheFns for redis::Client { Ok(()) } - async fn get_cache_key(&self, key: String) -> Result - where - T: DeserializeOwned, - { - let mut conn = self.get_multiplexed_tokio_connection().await?; + pub async fn get_cache_key(&self, key: String) -> Result { + let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; let key_encoded = encode(key); - let res: String = redis::cmd("GET") + redis::cmd("GET") .arg(key_encoded) .query_async(&mut conn) - .await?; - - Ok(serde_json::from_str(&res)?) + .await } - async fn del_cache_key(&self, key: String) -> Result<(), Error> { - let mut conn = self.get_multiplexed_tokio_connection().await?; + pub async fn del_cache_key(&self, key: String) -> Result<(), RedisError> { + let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; let key_encoded = encode(key); - Ok(redis::cmd("DEL") + redis::cmd("DEL") .arg(key_encoded) .query_async(&mut conn) - .await?) + .await } } - -pub fn generate_device_name() -> String { - let mut rng = rand::rng(); - - let adjective = ADJECTIVES.choose(&mut rng).unwrap(); - let animal = ANIMALS.choose(&mut rng).unwrap(); - - [*adjective, *animal].join(" ") -} diff --git a/src/wordlist.rs b/src/wordlist.rs deleted file mode 100644 index 1227c1f..0000000 --- a/src/wordlist.rs +++ /dev/null @@ -1,993 +0,0 @@ -pub const ANIMALS: [&str; 223] = [ - "Aardvark", - "Albatross", - "Alligator", - "Alpaca", - "Ant", - "Anteater", - "Antelope", - "Ape", - "Armadillo", - "Donkey", - "Baboon", - "Badger", - "Barracuda", - "Bat", - "Bear", - "Beaver", - "Bee", - "Bison", - "Boar", - "Buffalo", - "Butterfly", - "Camel", - "Capybara", - "Caribou", - "Cassowary", - "Cat", - "Caterpillar", - "Cattle", - "Chamois", - "Cheetah", - "Chicken", - "Chimpanzee", - "Chinchilla", - "Chough", - "Clam", - "Cobra", - "Cockroach", - "Cod", - "Cormorant", - "Coyote", - "Crab", - "Crane", - "Crocodile", - "Crow", - "Curlew", - "Deer", - "Dinosaur", - "Dog", - "Dogfish", - "Dolphin", - "Dotterel", - "Dove", - "Dragonfly", - "Duck", - "Dugong", - "Dunlin", - "Eagle", - "Echidna", - "Eel", - "Eland", - "Elephant", - "Elk", - "Emu", - "Falcon", - "Ferret", - "Finch", - "Fish", - "Flamingo", - "Fly", - "Fox", - "Frog", - "Gaur", - "Gazelle", - "Gerbil", - "Giraffe", - "Gnat", - "Gnu", - "Goat", - "Goldfinch", - "Goldfish", - "Goose", - "Gorilla", - "Goshawk", - "Grasshopper", - "Grouse", - "Guanaco", - "Gull", - "Hamster", - "Hare", - "Hawk", - "Hedgehog", - "Heron", - "Herring", - "Hippopotamus", - "Hornet", - "Horse", - "Hummingbird", - "Hyena", - "Ibex", - "Ibis", - "Jackal", - "Jaguar", - "Jay", - "Jellyfish", - "Kangaroo", - "Kingfisher", - "Koala", - "Kookabura", - "Kouprey", - "Kudu", - "Lapwing", - "Lark", - "Lemur", - "Leopard", - "Lion", - "Llama", - "Lobster", - "Locust", - "Loris", - "Louse", - "Lyrebird", - "Magpie", - "Mallard", - "Manatee", - "Mandrill", - "Mantis", - "Marten", - "Meerkat", - "Mink", - "Mole", - "Mongoose", - "Monkey", - "Moose", - "Mosquito", - "Mouse", - "Mule", - "Narwhal", - "Newt", - "Nightingale", - "Octopus", - "Okapi", - "Opossum", - "Oryx", - "Ostrich", - "Otter", - "Owl", - "Oyster", - "Panther", - "Parrot", - "Partridge", - "Peafowl", - "Pelican", - "Penguin", - "Pheasant", - "Pig", - "Pigeon", - "Pony", - "Porcupine", - "Porpoise", - "Quail", - "Quelea", - "Quetzal", - "Rabbit", - "Raccoon", - "Rail", - "Ram", - "Rat", - "Raven", - "Red Deer", - "Red Panda", - "Reindeer", - "Rhinoceros", - "Rook", - "Salamander", - "Salmon", - "Sand Dollar", - "Sandpiper", - "Sardine", - "Scorpion", - "Seahorse", - "Seal", - "Shark", - "Sheep", - "Shrew", - "Skunk", - "Snail", - "Snake", - "Sparrow", - "Spider", - "Spoonbill", - "Squid", - "Squirrel", - "Starling", - "Stingray", - "Stinkbug", - "Stork", - "Swallow", - "Swan", - "Tapir", - "Tarsier", - "Termite", - "Tiger", - "Toad", - "Trout", - "Turkey", - "Turtle", - "Viper", - "Vulture", - "Wallaby", - "Walrus", - "Wasp", - "Weasel", - "Whale", - "Wildcat", - "Wolf", - "Wolverine", - "Wombat", - "Woodcock", - "Woodpecker", - "Worm", - "Wren", - "Yak", - "Zebra", -]; - -pub const ADJECTIVES: [&str; 765] = [ - "Other", - "Such", - "First", - "Many", - "New", - "More", - "Same", - "Own", - "Good", - "Different", - "Great", - "Long", - "High", - "Social", - "Little", - "Much", - "Important", - "Small", - "Most", - "Large", - "Old", - "Few", - "General", - "Second", - "Public", - "Last", - "Several", - "Early", - "Certain", - "Economic", - "Least", - "Common", - "Present", - "Next", - "Local", - "Best", - "Particular", - "Young", - "Various", - "Necessary", - "Whole", - "Only", - "True", - "Able", - "Major", - "Full", - "Low", - "Available", - "Real", - "Similar", - "Total", - "Special", - "Less", - "Short", - "Specific", - "Single", - "Self", - "National", - "Individual", - "Clear", - "Personal", - "Higher", - "Better", - "Third", - "Natural", - "Greater", - "Open", - "Difficult", - "Current", - "Further", - "Main", - "Physical", - "Foreign", - "Lower", - "Strong", - "Private", - "Likely", - "International", - "Significant", - "Late", - "Basic", - "Hard", - "Modern", - "Simple", - "Normal", - "Sure", - "Central", - "Original", - "Effective", - "Following", - "Direct", - "Final", - "Cultural", - "Big", - "Recent", - "Complete", - "Financial", - "Positive", - "Primary", - "Appropriate", - "Legal", - "European", - "Equal", - "Larger", - "Average", - "Historical", - "Critical", - "Wide", - "Traditional", - "Additional", - "Active", - "Complex", - "Former", - "Independent", - "Entire", - "Actual", - "Close", - "Constant", - "Previous", - "Easy", - "Serious", - "Potential", - "Fine", - "Industrial", - "Subject", - "Future", - "Internal", - "Initial", - "Well", - "Essential", - "Dark", - "Popular", - "Successful", - "Standard", - "Year", - "Past", - "Ready", - "Professional", - "Wrong", - "Very", - "Proper", - "Separate", - "Heavy", - "Civil", - "Responsible", - "Considerable", - "Light", - "Cold", - "Above", - "Older", - "Practical", - "External", - "Sufficient", - "Interesting", - "Upper", - "Scientific", - "Key", - "Annual", - "Limited", - "Smaller", - "Southern", - "Earlier", - "Commercial", - "Powerful", - "Later", - "Like", - "Clinical", - "Ancient", - "Educational", - "Typical", - "Technical", - "Environmental", - "Formal", - "Aware", - "Beautiful", - "Variable", - "Obvious", - "Secondary", - "Enough", - "Urban", - "Regular", - "Relevant", - "Greatest", - "Spiritual", - "Time", - "Double", - "Happy", - "Term", - "Multiple", - "Dependent", - "Correct", - "Northern", - "Middle", - "Rural", - "Official", - "Fundamental", - "Numerous", - "Overall", - "Usual", - "Native", - "Regional", - "Highest", - "North", - "Agricultural", - "Literary", - "Broad", - "Perfect", - "Experimental", - "Fourth", - "Global", - "Ordinary", - "Related", - "Apparent", - "Daily", - "Principal", - "Contemporary", - "Severe", - "Reasonable", - "Subsequent", - "Worth", - "Longer", - "Emotional", - "Intellectual", - "Unique", - "Pure", - "Familiar", - "American", - "Solid", - "Brief", - "Famous", - "Fresh", - "Day", - "Corresponding", - "Characteristic", - "Maximum", - "Detailed", - "Outside", - "Theoretical", - "Fair", - "Opposite", - "Capable", - "Visual", - "Interested", - "Joint", - "Adequate", - "Based", - "Substantial", - "Unable", - "Structural", - "Soft", - "False", - "Largest", - "Inner", - "Mean", - "Extensive", - "Excellent", - "Rapid", - "Absolute", - "Consistent", - "Continuous", - "Administrative", - "Strange", - "Willing", - "Alternative", - "Slow", - "Distinct", - "Safe", - "Permanent", - "Front", - "Corporate", - "Academic", - "Thin", - "Nineteenth", - "Universal", - "Functional", - "Unknown", - "Careful", - "Narrow", - "Evident", - "Sound", - "Classical", - "Minor", - "Weak", - "Suitable", - "Chief", - "Extreme", - "Yellow", - "Warm", - "Mixed", - "Flat", - "Huge", - "Vast", - "Stable", - "Valuable", - "Rare", - "Visible", - "Sensitive", - "Mechanical", - "State", - "Radical", - "Extra", - "Superior", - "Conventional", - "Thick", - "Dominant", - "Post", - "Collective", - "Younger", - "Efficient", - "Linear", - "Organic", - "Oral", - "Century", - "Creative", - "Vertical", - "Dynamic", - "Empty", - "Minimum", - "Cognitive", - "Logical", - "Afraid", - "Equivalent", - "Quick", - "Near", - "Concrete", - "Mass", - "Acute", - "Sharp", - "Easier", - "Quiet", - "Adult", - "Accurate", - "Ideal", - "Partial", - "Bright", - "Identical", - "Conservative", - "Magnetic", - "Frequent", - "Electronic", - "Fixed", - "Square", - "Cross", - "Clean", - "Back", - "Organizational", - "Constitutional", - "Genetic", - "Ultimate", - "Secret", - "Vital", - "Dramatic", - "Objective", - "Round", - "Alive", - "Straight", - "Unusual", - "Rational", - "Electric", - "Mutual", - "Class", - "Competitive", - "Revolutionary", - "Statistical", - "Random", - "Musical", - "Crucial", - "Racial", - "Sudden", - "Acid", - "Content", - "Temporary", - "Line", - "Remarkable", - "Exact", - "Valid", - "Helpful", - "Nice", - "Comprehensive", - "United", - "Level", - "Fifth", - "Nervous", - "Expensive", - "Prominent", - "Healthy", - "Liquid", - "Institutional", - "Silent", - "Sweet", - "Strategic", - "Molecular", - "Comparative", - "Called", - "Electrical", - "Raw", - "Acceptable", - "Scale", - "Violent", - "All", - "Desirable", - "Tall", - "Steady", - "Wonderful", - "Sub", - "Distant", - "Progressive", - "Enormous", - "Horizontal", - "And", - "Intense", - "Smooth", - "Applicable", - "Over", - "Animal", - "Abstract", - "Wise", - "Worst", - "Gold", - "Precise", - "Legislative", - "Remote", - "Technological", - "Outer", - "Uniform", - "Slight", - "Attractive", - "Evil", - "Tiny", - "Royal", - "Angry", - "Advanced", - "Friendly", - "Dear", - "Busy", - "Spatial", - "Rough", - "Primitive", - "Judicial", - "Systematic", - "Lateral", - "Sorry", - "Plain", - "Off", - "Comfortable", - "Definite", - "Massive", - "Firm", - "Widespread", - "Prior", - "Twentieth", - "Mathematical", - "Verbal", - "Marginal", - "Excessive", - "Stronger", - "Gross", - "World", - "Productive", - "Wider", - "Glad", - "Linguistic", - "Patient", - "Symbolic", - "Earliest", - "Plastic", - "Type", - "Prime", - "Eighteenth", - "Blind", - "Neutral", - "Guilty", - "Hand", - "Extraordinary", - "Metal", - "Surprising", - "Fellow", - "York", - "Grand", - "Thermal", - "Artificial", - "Five", - "Lowest", - "Genuine", - "Dimensional", - "Optical", - "Unlikely", - "Developmental", - "Reliable", - "Executive", - "Comparable", - "Satisfactory", - "Golden", - "Diverse", - "Preliminary", - "Wooden", - "Noble", - "Part", - "Striking", - "Cool", - "Classic", - "Elderly", - "Four", - "Temporal", - "Indirect", - "Romantic", - "Intermediate", - "Differential", - "Passive", - "Life", - "Voluntary", - "Out", - "Adjacent", - "Behavioral", - "Exclusive", - "Closed", - "Inherent", - "Inevitable", - "Complicated", - "Quantitative", - "Respective", - "Artistic", - "Probable", - "Anxious", - "Informal", - "Strict", - "Fiscal", - "Ideological", - "Profound", - "Extended", - "Eternal", - "Known", - "Infinite", - "Proud", - "Honest", - "Peculiar", - "Absent", - "Pleasant", - "Optimal", - "Renal", - "Static", - "Outstanding", - "Presidential", - "Digital", - "Integrated", - "Legitimate", - "Curious", - "Aggressive", - "Deeper", - "Elementary", - "History", - "Surgical", - "Occasional", - "Flexible", - "Convenient", - "Solar", - "Atomic", - "Isolated", - "Latest", - "Sad", - "Conceptual", - "Underlying", - "Everyday", - "Cost", - "Intensive", - "Odd", - "Subjective", - "Mid", - "Worthy", - "Pale", - "Meaningful", - "Therapeutic", - "Making", - "Circular", - "Realistic", - "Multi", - "Child", - "Sophisticated", - "Down", - "Leading", - "Intelligent", - "Governmental", - "Numerical", - "Minimal", - "Diagnostic", - "Indigenous", - "Aesthetic", - "Distinctive", - "Operational", - "Sole", - "Material", - "Fast", - "Bitter", - "Broader", - "Brilliant", - "Peripheral", - "Rigid", - "Automatic", - "Lesser", - "Routine", - "Favorable", - "Cooperative", - "Cardiac", - "Arbitrary", - "Loose", - "Favorite", - "Subtle", - "Uncertain", - "Hostile", - "Monthly", - "Naval", - "Physiological", - "Historic", - "Developed", - "Skilled", - "Anterior", - "Pro", - "Gentle", - "Loud", - "Pulmonary", - "Innocent", - "Provincial", - "Mild", - "Page", - "Specialized", - "Bare", - "Excess", - "Inter", - "Shaped", - "Theological", - "Sensory", - "The", - "Stress", - "Novel", - "Working", - "Shorter", - "Secular", - "Geographical", - "Intimate", - "Liable", - "Selective", - "Influential", - "Modest", - "Successive", - "Continued", - "Water", - "Expert", - "Municipal", - "Marine", - "Thirty", - "Adverse", - "Wacky", - "Closer", - "Virtual", - "Peaceful", - "Mobile", - "Sixth", - "Immune", - "Coastal", - "Representative", - "Lead", - "Forward", - "Faithful", - "Crystal", - "Protective", - "Elaborate", - "Tremendous", - "Welcoming", - "Abnormal", - "Grateful", - "Proportional", - "Dual", - "Operative", - "Precious", - "Sympathetic", - "Accessible", - "Lovely", - "Spinal", - "Even", - "Marked", - "Observed", - "Point", - "Mature", - "Competent", - "Residential", - "Impressive", - "Unexpected", - "Nearby", - "Unnecessary", - "Generous", - "Cerebral", - "Unpublished", - "Delicate", - "Analytical", - "Tropical", - "Statutory", - "Cell", - "Weekly", - "End", - "Online", - "Beneficial", - "Aged", - "Tough", - "Eager", - "Ongoing", - "Silver", - "Persistent", - "Calm", - "Nearest", - "Hidden", - "Magic", - "Pretty", - "Wealthy", - "Exciting", - "Decisive", - "Confident", - "Invisible", - "Notable", - "Medium", - "Manual", - "Select", - "Thorough", - "Causal", - "Giant", - "Bigger", - "Pink", - "Improved", - "Immense", - "Hour", - "Intact", - "Grade", - "Dense", - "Hungry", - "Biggest", - "Abundant", - "Handsome", - "Retail", - "Insufficient", - "Irregular", - "Intrinsic", - "Residual", - "Follow", - "Fluid", - "Mysterious", - "Descriptive", - "Elastic", - "Destructive", - "Architectural", - "Synthetic", - "Continental", - "Evolutionary", - "Lucky", - "Bold", - "Funny", - "Peak", - "Smallest", - "Reluctant", - "Suspicious", - "Smart", - "Mighty", - "Brave", - "Humble", - "Vocal", - "Obscure", - "Innovative", -];