refactor: rewrite entire codebase in axum instead of actix

Replaces actix with axum for web, allows us to use socket.io and gives us access to the tower ecosystem of middleware

breaks compatibility with our current websocket implementation, needs to be reimplemented for socket.io
This commit is contained in:
Radical 2025-07-16 16:36:22 +02:00
parent 3647086adb
commit 324137ce8b
47 changed files with 1381 additions and 1129 deletions

View file

@ -20,7 +20,6 @@ thiserror = "2.0.12"
# CLI # CLI
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
log = "0.4" log = "0.4"
simple_logger = "5.0.0"
# async # async
futures = "0.3" futures = "0.3"
@ -30,19 +29,21 @@ futures-util = "0.3.31"
# Data (de)serialization # Data (de)serialization
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
toml = "0.8" toml = "0.9"
bytes = "1.10.1"
rmpv = { version = "1.3.0", features = ["with-serde"] }
# File Storage # File Storage
bindet = "0.3.2" bindet = "0.3.2"
bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false } bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false }
# Web Server # Web Server
actix-web = "4.11" axum = { version = "0.8.4", features = ["macros", "multipart"] }
actix-cors = "0.7.1" tower-http = { version = "0.6.6", features = ["cors"] }
actix-ws = "0.3.0" axum-extra = { version = "0.10.1", features = ["cookie", "typed-header"] }
actix-multipart = "0.7.2" socketioxide = { version = "0.17.2", features = ["state"] }
url = { version = "2.5", features = ["serde"] } url = { version = "2.5", features = ["serde"] }
tokio-tungstenite = { version = "0.27", features = ["native-tls", "url"] } time = "0.3.41"
# Database # Database
uuid = { version = "1.17", features = ["serde", "v7"] } uuid = { version = "1.17", features = ["serde", "v7"] }
@ -60,5 +61,5 @@ regex = "1.11"
random-string = "1.1" random-string = "1.1"
lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] }
chrono = { version = "0.4.41", features = ["serde"] } chrono = { version = "0.4.41", features = ["serde"] }
tracing-subscriber = "0.3.19"

View file

@ -1,13 +1,16 @@
//! `/api` Contains the entire API //! `/api` Contains the entire API
use actix_web::Scope; use std::sync::Arc;
use actix_web::web;
use axum::{Router, routing::get};
use crate::AppState;
mod v1; mod v1;
mod versions; mod versions;
pub fn web(path: &str) -> Scope { pub fn router() -> Router<Arc<AppState>> {
web::scope(path.trim_end_matches('/')) Router::new()
.service(v1::web()) .route("/versions", get(versions::versions))
.service(versions::get) .nest("/v1", v1::router())
} }

View file

@ -1,16 +1,21 @@
//! `/api/v1/auth/devices` Returns list of logged in devices //! `/api/v1/auth/devices` Returns list of logged in devices
use actix_web::{HttpRequest, HttpResponse, get, web}; use std::sync::Arc;
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::Serialize; use serde::Serialize;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
schema::refresh_tokens::{self, dsl}, schema::refresh_tokens::{self, dsl},
utils::get_auth_header,
}; };
#[derive(Serialize, Selectable, Queryable)] #[derive(Serialize, Selectable, Queryable)]
@ -18,7 +23,7 @@ use crate::{
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
struct Device { struct Device {
device_name: String, device_name: String,
created_at: i64 created_at: i64,
} }
/// `GET /api/v1/auth/devices` Returns list of logged in devices /// `GET /api/v1/auth/devices` Returns list of logged in devices
@ -35,18 +40,13 @@ struct Device {
/// ///
/// ]); /// ]);
/// ``` /// ```
#[get("/devices")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
data: web::Data<Data>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let devices: Vec<Device> = dsl::refresh_tokens let devices: Vec<Device> = dsl::refresh_tokens
.filter(dsl::uuid.eq(uuid)) .filter(dsl::uuid.eq(uuid))
@ -54,5 +54,5 @@ pub async fn get(
.get_results(&mut conn) .get_results(&mut conn)
.await?; .await?;
Ok(HttpResponse::Ok().json(devices)) Ok((StatusCode::OK, Json(devices)))
} }

View file

@ -1,39 +1,47 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use actix_web::{HttpResponse, post, web};
use argon2::{PasswordHash, PasswordVerifier}; use argon2::{PasswordHash, PasswordVerifier};
use axum::{
Json,
extract::State,
http::{HeaderValue, StatusCode},
response::IntoResponse,
};
use diesel::{ExpressionMethods, QueryDsl, dsl::insert_into}; use diesel::{ExpressionMethods, QueryDsl, dsl::insert_into};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
Data, AppState,
error::Error, error::Error,
schema::*, schema::*,
utils::{PASSWORD_REGEX, generate_token, new_refresh_token_cookie, user_uuid_from_identifier}, utils::{
PASSWORD_REGEX, generate_token, new_access_token_cookie, new_refresh_token_cookie,
user_uuid_from_identifier,
},
}; };
use super::Response;
#[derive(Deserialize)] #[derive(Deserialize)]
struct LoginInformation { pub struct LoginInformation {
username: String, username: String,
password: String, password: String,
device_name: String, device_name: String,
} }
#[post("/login")]
pub async fn response( pub async fn response(
login_information: web::Json<LoginInformation>, State(app_state): State<Arc<AppState>>,
data: web::Data<Data>, Json(login_information): Json<LoginInformation>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
if !PASSWORD_REGEX.is_match(&login_information.password) { if !PASSWORD_REGEX.is_match(&login_information.password) {
return Ok(HttpResponse::Forbidden().json(r#"{ "password_hashed": false }"#)); return Err(Error::BadRequest("Bad password".to_string()));
} }
use users::dsl; use users::dsl;
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
let uuid = user_uuid_from_identifier(&mut conn, &login_information.username).await?; let uuid = user_uuid_from_identifier(&mut conn, &login_information.username).await?;
@ -46,7 +54,7 @@ pub async fn response(
let parsed_hash = PasswordHash::new(&database_password) let parsed_hash = PasswordHash::new(&database_password)
.map_err(|e| Error::PasswordHashError(e.to_string()))?; .map_err(|e| Error::PasswordHashError(e.to_string()))?;
if data if app_state
.argon2 .argon2
.verify_password(login_information.password.as_bytes(), &parsed_hash) .verify_password(login_information.password.as_bytes(), &parsed_hash)
.is_err() .is_err()
@ -85,7 +93,21 @@ pub async fn response(
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
Ok(HttpResponse::Ok() let mut response = StatusCode::OK.into_response();
.cookie(new_refresh_token_cookie(&data.config, refresh_token))
.json(Response { access_token })) response.headers_mut().insert(
"Set-Cookie",
HeaderValue::from_str(
&new_refresh_token_cookie(&app_state.config, refresh_token).to_string(),
)?,
);
response.headers_mut().insert(
"Set-Cookie2",
HeaderValue::from_str(
&new_access_token_cookie(&app_state.config, access_token).to_string(),
)?,
);
Ok(response)
} }

View file

@ -1,9 +1,16 @@
use actix_web::{HttpRequest, HttpResponse, get, web}; use std::sync::Arc;
use axum::{
extract::State,
http::{HeaderValue, StatusCode},
response::IntoResponse,
};
use axum_extra::extract::CookieJar;
use diesel::{ExpressionMethods, delete}; use diesel::{ExpressionMethods, delete};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use crate::{ use crate::{
Data, AppState,
error::Error, error::Error,
schema::refresh_tokens::{self, dsl}, schema::refresh_tokens::{self, dsl},
}; };
@ -20,28 +27,49 @@ use crate::{
/// ///
/// 401 Unauthorized (no refresh token found) /// 401 Unauthorized (no refresh token found)
/// ///
#[get("/logout")] pub async fn res(
pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized( jar: CookieJar,
) -> Result<impl IntoResponse, Error> {
let mut refresh_token_cookie = jar
.get("refresh_token")
.ok_or(Error::Unauthorized(
"request has no refresh token".to_string(), "request has no refresh token".to_string(),
))?; ))?
.to_owned();
let refresh_token = String::from(refresh_token_cookie.value()); let access_token_cookie = jar.get("access_token");
let mut conn = data.pool.get().await?; let refresh_token = String::from(refresh_token_cookie.value_trimmed());
let mut conn = app_state.pool.get().await?;
let deleted = delete(refresh_tokens::table) let deleted = delete(refresh_tokens::table)
.filter(dsl::token.eq(refresh_token)) .filter(dsl::token.eq(refresh_token))
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
refresh_token_cookie.make_removal(); let mut response;
if deleted == 0 { if deleted == 0 {
return Ok(HttpResponse::NotFound() response = StatusCode::NOT_FOUND.into_response();
.cookie(refresh_token_cookie) } else {
.finish()); response = StatusCode::OK.into_response();
} }
Ok(HttpResponse::Ok().cookie(refresh_token_cookie).finish()) refresh_token_cookie.make_removal();
response.headers_mut().append(
"Set-Cookie",
HeaderValue::from_str(&refresh_token_cookie.to_string())?,
);
if let Some(cookie) = access_token_cookie {
let mut cookie = cookie.clone();
cookie.make_removal();
response
.headers_mut()
.append("Set-Cookie2", HeaderValue::from_str(&cookie.to_string())?);
}
Ok(response)
} }

View file

@ -1,12 +1,17 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use actix_web::{Scope, web}; use axum::{
Router,
routing::{delete, get, post},
};
use diesel::{ExpressionMethods, QueryDsl}; use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
use crate::{Conn, error::Error, schema::access_tokens::dsl}; use crate::{AppState, Conn, error::Error, schema::access_tokens::dsl};
mod devices; mod devices;
mod login; mod login;
@ -17,23 +22,18 @@ mod reset_password;
mod revoke; mod revoke;
mod verify_email; mod verify_email;
#[derive(Serialize)] pub fn router() -> Router<Arc<AppState>> {
struct Response { Router::new()
access_token: String, .route("/register", post(register::post))
} .route("/login", post(login::response))
.route("/logout", delete(logout::res))
pub fn web() -> Scope { .route("/refresh", post(refresh::post))
web::scope("/auth") .route("/revoke", post(revoke::post))
.service(register::res) .route("/verify-email", get(verify_email::get))
.service(login::response) .route("/verify-email", post(verify_email::post))
.service(logout::res) .route("/reset-password", get(reset_password::get))
.service(refresh::res) .route("/reset-password", post(reset_password::post))
.service(revoke::res) .route("/devices", get(devices::get))
.service(verify_email::get)
.service(verify_email::post)
.service(reset_password::get)
.service(reset_password::post)
.service(devices::get)
} }
pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result<Uuid, Error> { pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result<Uuid, Error> {

View file

@ -1,32 +1,45 @@
use actix_web::{HttpRequest, HttpResponse, post, web}; use axum::{
extract::State,
http::{HeaderValue, StatusCode},
response::IntoResponse,
};
use axum_extra::extract::CookieJar;
use diesel::{ExpressionMethods, QueryDsl, delete, update}; use diesel::{ExpressionMethods, QueryDsl, delete, update};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use log::error; use log::error;
use std::time::{SystemTime, UNIX_EPOCH}; use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use crate::{ use crate::{
Data, AppState,
error::Error, error::Error,
schema::{ schema::{
access_tokens::{self, dsl}, access_tokens::{self, dsl},
refresh_tokens::{self, dsl as rdsl}, refresh_tokens::{self, dsl as rdsl},
}, },
utils::{generate_token, new_refresh_token_cookie}, utils::{generate_token, new_access_token_cookie, new_refresh_token_cookie},
}; };
use super::Response; pub async fn post(
State(app_state): State<Arc<AppState>>,
#[post("/refresh")] jar: CookieJar,
pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized( let mut refresh_token_cookie = jar
.get("refresh_token")
.ok_or(Error::Unauthorized(
"request has no refresh token".to_string(), "request has no refresh token".to_string(),
))?; ))?
.to_owned();
let mut refresh_token = String::from(refresh_token_cookie.value()); let access_token_cookie = jar.get("access_token");
let refresh_token = String::from(refresh_token_cookie.value_trimmed());
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64;
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
if let Ok(created_at) = rdsl::refresh_tokens if let Ok(created_at) = rdsl::refresh_tokens
.filter(rdsl::token.eq(&refresh_token)) .filter(rdsl::token.eq(&refresh_token))
@ -45,15 +58,29 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
error!("{error}"); error!("{error}");
} }
refresh_token_cookie.make_removal(); let mut response = StatusCode::UNAUTHORIZED.into_response();
return Ok(HttpResponse::Unauthorized() refresh_token_cookie.make_removal();
.cookie(refresh_token_cookie) response.headers_mut().append(
.finish()); "Set-Cookie",
HeaderValue::from_str(&refresh_token_cookie.to_string())?,
);
if let Some(cookie) = access_token_cookie {
let mut cookie = cookie.clone();
cookie.make_removal();
response
.headers_mut()
.append("Set-Cookie2", HeaderValue::from_str(&cookie.to_string())?);
}
return Ok(response);
} }
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64;
let mut response = StatusCode::OK.into_response();
if lifetime > 1987200 { if lifetime > 1987200 {
let new_refresh_token = generate_token::<32>()?; let new_refresh_token = generate_token::<32>()?;
@ -67,7 +94,13 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
.await .await
{ {
Ok(_) => { Ok(_) => {
refresh_token = new_refresh_token; response.headers_mut().append(
"Set-Cookie",
HeaderValue::from_str(
&new_refresh_token_cookie(&app_state.config, new_refresh_token)
.to_string(),
)?,
);
} }
Err(error) => { Err(error) => {
error!("{error}"); error!("{error}");
@ -86,14 +119,40 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
return Ok(HttpResponse::Ok() if response.headers().get("Set-Cookie").is_some() {
.cookie(new_refresh_token_cookie(&data.config, refresh_token)) response.headers_mut().append(
.json(Response { access_token })); "Set-Cookie2",
HeaderValue::from_str(
&new_access_token_cookie(&app_state.config, access_token).to_string(),
)?,
);
} else {
response.headers_mut().append(
"Set-Cookie",
HeaderValue::from_str(
&new_access_token_cookie(&app_state.config, access_token).to_string(),
)?,
);
} }
refresh_token_cookie.make_removal(); return Ok(response);
}
Ok(HttpResponse::Unauthorized() let mut response = StatusCode::UNAUTHORIZED.into_response();
.cookie(refresh_token_cookie)
.finish()) refresh_token_cookie.make_removal();
response.headers_mut().append(
"Set-Cookie",
HeaderValue::from_str(&refresh_token_cookie.to_string())?,
);
if let Some(cookie) = access_token_cookie {
let mut cookie = cookie.clone();
cookie.make_removal();
response
.headers_mut()
.append("Set-Cookie2", HeaderValue::from_str(&cookie.to_string())?);
}
Ok(response)
} }

View file

@ -1,18 +1,25 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use actix_web::{HttpResponse, post, web};
use argon2::{ use argon2::{
PasswordHasher, PasswordHasher,
password_hash::{SaltString, rand_core::OsRng}, password_hash::{SaltString, rand_core::OsRng},
}; };
use axum::{
Json,
extract::State,
http::{HeaderValue, StatusCode},
response::IntoResponse,
};
use diesel::{ExpressionMethods, dsl::insert_into}; use diesel::{ExpressionMethods, dsl::insert_into};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use super::Response;
use crate::{ use crate::{
Data, AppState,
error::Error, error::Error,
objects::Member, objects::Member,
schema::{ schema::{
@ -21,12 +28,13 @@ use crate::{
users::{self, dsl as udsl}, users::{self, dsl as udsl},
}, },
utils::{ utils::{
EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_token, new_refresh_token_cookie, EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_token, new_access_token_cookie,
new_refresh_token_cookie,
}, },
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct AccountInformation { pub struct AccountInformation {
identifier: String, identifier: String,
email: String, email: String,
password: String, password: String,
@ -34,17 +42,13 @@ struct AccountInformation {
} }
#[derive(Serialize)] #[derive(Serialize)]
struct ResponseError { pub struct ResponseError {
signups_enabled: bool, signups_enabled: bool,
gorb_id_valid: bool, gorb_id_valid: bool,
gorb_id_available: bool, gorb_id_available: bool,
email_valid: bool, email_valid: bool,
email_available: bool, email_available: bool,
password_hashed: bool, password_strength: bool,
password_minimum_length: bool,
password_special_characters: bool,
password_letters: bool,
password_numbers: bool,
} }
impl Default for ResponseError { impl Default for ResponseError {
@ -55,21 +59,16 @@ impl Default for ResponseError {
gorb_id_available: true, gorb_id_available: true,
email_valid: true, email_valid: true,
email_available: true, email_available: true,
password_hashed: true, password_strength: true,
password_minimum_length: true,
password_special_characters: true,
password_letters: true,
password_numbers: true,
} }
} }
} }
#[post("/register")] pub async fn post(
pub async fn res( State(app_state): State<Arc<AppState>>,
account_information: web::Json<AccountInformation>, Json(account_information): Json<AccountInformation>,
data: web::Data<Data>, ) -> Result<impl IntoResponse, Error> {
) -> Result<HttpResponse, Error> { if !app_state.config.instance.registration {
if !data.config.instance.registration {
return Err(Error::Forbidden( return Err(Error::Forbidden(
"registration is disabled on this instance".to_string(), "registration is disabled on this instance".to_string(),
)); ));
@ -78,36 +77,48 @@ pub async fn res(
let uuid = Uuid::now_v7(); let uuid = Uuid::now_v7();
if !EMAIL_REGEX.is_match(&account_information.email) { if !EMAIL_REGEX.is_match(&account_information.email) {
return Ok(HttpResponse::Forbidden().json(ResponseError { return Ok((
StatusCode::FORBIDDEN,
Json(ResponseError {
email_valid: false, email_valid: false,
..Default::default() ..Default::default()
})); }),
)
.into_response());
} }
if !USERNAME_REGEX.is_match(&account_information.identifier) if !USERNAME_REGEX.is_match(&account_information.identifier)
|| account_information.identifier.len() < 3 || account_information.identifier.len() < 3
|| account_information.identifier.len() > 32 || account_information.identifier.len() > 32
{ {
return Ok(HttpResponse::Forbidden().json(ResponseError { return Ok((
StatusCode::FORBIDDEN,
Json(ResponseError {
gorb_id_valid: false, gorb_id_valid: false,
..Default::default() ..Default::default()
})); }),
)
.into_response());
} }
if !PASSWORD_REGEX.is_match(&account_information.password) { if !PASSWORD_REGEX.is_match(&account_information.password) {
return Ok(HttpResponse::Forbidden().json(ResponseError { return Ok((
password_hashed: false, StatusCode::FORBIDDEN,
Json(ResponseError {
password_strength: false,
..Default::default() ..Default::default()
})); }),
)
.into_response());
} }
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
if let Ok(hashed_password) = data if let Ok(hashed_password) = app_state
.argon2 .argon2
.hash_password(account_information.password.as_bytes(), &salt) .hash_password(account_information.password.as_bytes(), &salt)
{ {
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
// TODO: Check security of this implementation // TODO: Check security of this implementation
insert_into(users::table) insert_into(users::table)
@ -145,14 +156,27 @@ pub async fn res(
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
if let Some(initial_guild) = data.config.instance.initial_guild { if let Some(initial_guild) = app_state.config.instance.initial_guild {
Member::new(&data, uuid, initial_guild).await?; Member::new(&app_state, uuid, initial_guild).await?;
} }
return Ok(HttpResponse::Ok() let mut response = StatusCode::OK.into_response();
.cookie(new_refresh_token_cookie(&data.config, refresh_token))
.json(Response { access_token })); response.headers_mut().append(
"Set-Cookie",
HeaderValue::from_str(
&new_refresh_token_cookie(&app_state.config, refresh_token).to_string(),
)?,
);
response.headers_mut().append(
"Set-Cookie2",
HeaderValue::from_str(
&new_access_token_cookie(&app_state.config, access_token).to_string(),
)?,
);
return Ok(response);
} }
Ok(HttpResponse::InternalServerError().finish()) Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response())
} }

View file

@ -1,13 +1,20 @@
//! `/api/v1/auth/reset-password` Endpoints for resetting user password //! `/api/v1/auth/reset-password` Endpoints for resetting user password
use actix_web::{HttpResponse, get, post, web}; use std::sync::Arc;
use axum::{
Json,
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use serde::Deserialize; use serde::Deserialize;
use crate::{Data, error::Error, objects::PasswordResetToken}; use crate::{AppState, error::Error, objects::PasswordResetToken};
#[derive(Deserialize)] #[derive(Deserialize)]
struct Query { pub struct QueryParams {
identifier: String, identifier: String,
} }
@ -20,17 +27,22 @@ struct Query {
/// ///
/// ### Responses /// ### Responses
/// 200 Email sent /// 200 Email sent
///
/// 429 Too Many Requests /// 429 Too Many Requests
///
/// 404 Not found /// 404 Not found
///
/// 400 Bad request /// 400 Bad request
/// ///
#[get("/reset-password")] pub async fn get(
pub async fn get(query: web::Query<Query>, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
query: Query<QueryParams>,
) -> Result<impl IntoResponse, Error> {
if let Ok(password_reset_token) = if let Ok(password_reset_token) =
PasswordResetToken::get_with_identifier(&data, query.identifier.clone()).await PasswordResetToken::get_with_identifier(&app_state, query.identifier.clone()).await
{ {
if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) { if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) {
password_reset_token.delete(&data).await?; password_reset_token.delete(&app_state).await?;
} else { } else {
return Err(Error::TooManyRequests( return Err(Error::TooManyRequests(
"Please allow 1 hour before sending a new email".to_string(), "Please allow 1 hour before sending a new email".to_string(),
@ -38,13 +50,13 @@ pub async fn get(query: web::Query<Query>, data: web::Data<Data>) -> Result<Http
} }
} }
PasswordResetToken::new(&data, query.identifier.clone()).await?; PasswordResetToken::new(&app_state, query.identifier.clone()).await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct ResetPassword { pub struct ResetPassword {
password: String, password: String,
token: String, token: String,
} }
@ -63,20 +75,23 @@ struct ResetPassword {
/// ///
/// ### Responses /// ### Responses
/// 200 Success /// 200 Success
///
/// 410 Token Expired /// 410 Token Expired
///
/// 404 Not Found /// 404 Not Found
///
/// 400 Bad Request /// 400 Bad Request
/// ///
#[post("/reset-password")]
pub async fn post( pub async fn post(
reset_password: web::Json<ResetPassword>, State(app_state): State<Arc<AppState>>,
data: web::Data<Data>, reset_password: Json<ResetPassword>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let password_reset_token = PasswordResetToken::get(&data, reset_password.token.clone()).await?; let password_reset_token =
PasswordResetToken::get(&app_state, reset_password.token.clone()).await?;
password_reset_token password_reset_token
.set_password(&data, reset_password.password.clone()) .set_password(&app_state, reset_password.password.clone())
.await?; .await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }

View file

@ -1,38 +1,39 @@
use actix_web::{HttpRequest, HttpResponse, post, web}; use std::sync::Arc;
use argon2::{PasswordHash, PasswordVerifier}; use argon2::{PasswordHash, PasswordVerifier};
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
use axum_extra::{
TypedHeader,
headers::authorization::{Authorization, Bearer},
};
use diesel::{ExpressionMethods, QueryDsl, delete}; use diesel::{ExpressionMethods, QueryDsl, delete};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
schema::refresh_tokens::{self, dsl as rdsl}, schema::refresh_tokens::{self, dsl as rdsl},
schema::users::dsl as udsl, schema::users::dsl as udsl,
utils::get_auth_header,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct RevokeRequest { pub struct RevokeRequest {
password: String, password: String,
device_name: String, device_name: String,
} }
// TODO: Should maybe be a delete request? // TODO: Should maybe be a delete request?
#[post("/revoke")] #[axum::debug_handler]
pub async fn res( pub async fn post(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
revoke_request: web::Json<RevokeRequest>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
data: web::Data<Data>, Json(revoke_request): Json<RevokeRequest>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let database_password: String = udsl::users let database_password: String = udsl::users
.filter(udsl::uuid.eq(uuid)) .filter(udsl::uuid.eq(uuid))
@ -43,7 +44,7 @@ pub async fn res(
let hashed_password = PasswordHash::new(&database_password) let hashed_password = PasswordHash::new(&database_password)
.map_err(|e| Error::PasswordHashError(e.to_string()))?; .map_err(|e| Error::PasswordHashError(e.to_string()))?;
if data if app_state
.argon2 .argon2
.verify_password(revoke_request.password.as_bytes(), &hashed_password) .verify_password(revoke_request.password.as_bytes(), &hashed_password)
.is_err() .is_err()
@ -59,5 +60,5 @@ pub async fn res(
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }

View file

@ -1,19 +1,28 @@
//! `/api/v1/auth/verify-email` Endpoints for verifying user emails //! `/api/v1/auth/verify-email` Endpoints for verifying user emails
use actix_web::{HttpRequest, HttpResponse, get, post, web}; use std::sync::Arc;
use axum::{
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{EmailToken, Me}, objects::{EmailToken, Me},
utils::get_auth_header,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct Query { pub struct QueryParams {
token: String, token: String,
} }
@ -35,37 +44,32 @@ struct Query {
/// ///
/// 401 Unauthorized /// 401 Unauthorized
/// ///
#[get("/verify-email")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
query: web::Query<Query>, Query(query): Query<QueryParams>,
data: web::Data<Data>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
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?; let me = Me::get(&mut conn, uuid).await?;
if me.email_verified { if me.email_verified {
return Ok(HttpResponse::NoContent().finish()); return Ok(StatusCode::NO_CONTENT);
} }
let email_token = EmailToken::get(&data, me.uuid).await?; let email_token = EmailToken::get(&app_state, me.uuid).await?;
if query.token != email_token.token { if query.token != email_token.token {
return Ok(HttpResponse::Unauthorized().finish()); return Ok(StatusCode::UNAUTHORIZED);
} }
me.verify_email(&mut conn).await?; me.verify_email(&mut conn).await?;
email_token.delete(&data).await?; email_token.delete(&app_state).await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }
/// `POST /api/v1/auth/verify-email` Sends user verification email /// `POST /api/v1/auth/verify-email` Sends user verification email
@ -81,25 +85,23 @@ pub async fn get(
/// ///
/// 401 Unauthorized /// 401 Unauthorized
/// ///
#[post("/verify-email")] pub async fn post(
pub async fn post(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
let headers = req.headers(); TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
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?; let me = Me::get(&mut conn, uuid).await?;
if me.email_verified { if me.email_verified {
return Ok(HttpResponse::NoContent().finish()); return Ok(StatusCode::NO_CONTENT);
} }
if let Ok(email_token) = EmailToken::get(&data, me.uuid).await { if let Ok(email_token) = EmailToken::get(&app_state, me.uuid).await {
if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(1) { if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(1) {
email_token.delete(&data).await?; email_token.delete(&app_state).await?;
} else { } else {
return Err(Error::TooManyRequests( return Err(Error::TooManyRequests(
"Please allow 1 hour before sending a new email".to_string(), "Please allow 1 hour before sending a new email".to_string(),
@ -107,7 +109,7 @@ pub async fn post(req: HttpRequest, data: web::Data<Data>) -> Result<HttpRespons
} }
} }
EmailToken::new(&data, me).await?; EmailToken::new(&app_state, me).await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }

View file

@ -1,12 +1,24 @@
use actix_web::{Scope, web}; use std::sync::Arc;
use axum::{
Router,
routing::{delete, get, patch},
};
//use socketioxide::SocketIo;
use crate::AppState;
mod uuid; mod uuid;
pub fn web() -> Scope { pub fn router() -> Router<Arc<AppState>> {
web::scope("/channels") //let (layer, io) = SocketIo::new_layer();
.service(uuid::get)
.service(uuid::delete) //io.ns("/{uuid}/socket", uuid::socket::ws);
.service(uuid::patch)
.service(uuid::messages::get) Router::new()
.service(uuid::socket::ws) .route("/{uuid}", get(uuid::get))
.route("/{uuid}", delete(uuid::delete))
.route("/{uuid}", patch(uuid::patch))
.route("/{uuid}/messages", get(uuid::messages::get))
//.layer(layer)
} }

View file

@ -1,18 +1,29 @@
//! `/api/v1/channels/{uuid}/messages` Endpoints related to channel messages //! `/api/v1/channels/{uuid}/messages` Endpoints related to channel messages
use std::sync::Arc;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{Channel, Member}, objects::{Channel, Member},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
use ::uuid::Uuid; use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, web}; use axum::{
Json,
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
struct MessageRequest { pub struct MessageRequest {
amount: i64, amount: i64,
offset: i64, offset: i64,
} }
@ -47,32 +58,25 @@ struct MessageRequest {
/// }); /// });
/// ``` /// ```
/// ///
#[get("/{uuid}/messages")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(channel_uuid): Path<Uuid>,
message_request: web::Query<MessageRequest>, Query(message_request): Query<MessageRequest>,
data: web::Data<Data>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let channel_uuid = path.into_inner().0; global_checks(&app_state, uuid).await?;
let mut conn = data.pool.get().await?; let channel = Channel::fetch_one(&app_state, channel_uuid).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?; Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
let messages = channel let messages = channel
.fetch_messages(&data, message_request.amount, message_request.offset) .fetch_messages(&app_state, message_request.amount, message_request.offset)
.await?; .await?;
Ok(HttpResponse::Ok().json(messages)) Ok((StatusCode::OK, Json(messages)))
} }

View file

@ -1,77 +1,74 @@
//! `/api/v1/channels/{uuid}` Channel specific endpoints //! `/api/v1/channels/{uuid}` Channel specific endpoints
pub mod messages; pub mod messages;
pub mod socket; //pub mod socket;
use std::sync::Arc;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{Channel, Member, Permissions}, objects::{Channel, Member, Permissions},
utils::{get_auth_header, global_checks}, utils::global_checks,
};
use axum::{
Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
}; };
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
#[get("/{uuid}")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(channel_uuid): Path<Uuid>,
data: web::Data<Data>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let channel_uuid = path.into_inner().0; global_checks(&app_state, uuid).await?;
let mut conn = data.pool.get().await?; let channel = Channel::fetch_one(&app_state, channel_uuid).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?; Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
Ok(HttpResponse::Ok().json(channel)) Ok((StatusCode::OK, Json(channel)))
} }
#[delete("/{uuid}")]
pub async fn delete( pub async fn delete(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(channel_uuid): Path<Uuid>,
data: web::Data<Data>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let channel_uuid = path.into_inner().0; global_checks(&app_state, uuid).await?;
let mut conn = data.pool.get().await?; let channel = Channel::fetch_one(&app_state, channel_uuid).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?; let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
member member
.check_permission(&data, Permissions::ManageChannel) .check_permission(&app_state, Permissions::ManageChannel)
.await?; .await?;
channel.delete(&data).await?; channel.delete(&app_state).await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct NewInfo { pub struct NewInfo {
name: Option<String>, name: Option<String>,
description: Option<String>, description: Option<String>,
is_above: Option<String>, is_above: Option<String>,
@ -108,48 +105,41 @@ struct NewInfo {
/// }); /// });
/// ``` /// ```
/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps /// 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( pub async fn patch(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(channel_uuid): Path<Uuid>,
new_info: web::Json<NewInfo>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
data: web::Data<Data>, Json(new_info): Json<NewInfo>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let channel_uuid = path.into_inner().0; global_checks(&app_state, uuid).await?;
let mut conn = data.pool.get().await?; let mut channel = Channel::fetch_one(&app_state, channel_uuid).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?; let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
member member
.check_permission(&data, Permissions::ManageChannel) .check_permission(&app_state, Permissions::ManageChannel)
.await?; .await?;
if let Some(new_name) = &new_info.name { if let Some(new_name) = &new_info.name {
channel.set_name(&data, new_name.to_string()).await?; channel.set_name(&app_state, new_name.to_string()).await?;
} }
if let Some(new_description) = &new_info.description { if let Some(new_description) = &new_info.description {
channel channel
.set_description(&data, new_description.to_string()) .set_description(&app_state, new_description.to_string())
.await?; .await?;
} }
if let Some(new_is_above) = &new_info.is_above { if let Some(new_is_above) = &new_info.is_above {
channel channel
.set_description(&data, new_is_above.to_string()) .set_description(&app_state, new_is_above.to_string())
.await?; .await?;
} }
Ok(HttpResponse::Ok().json(channel)) Ok((StatusCode::OK, Json(channel)))
} }

View file

@ -1,28 +1,40 @@
//! `/api/v1/guilds` Guild related endpoints //! `/api/v1/guilds` Guild related endpoints
use actix_web::{HttpRequest, HttpResponse, Scope, get, post, web}; use std::sync::Arc;
use axum::{
Json, Router,
extract::State,
http::StatusCode,
response::IntoResponse,
routing::{get, post},
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use serde::Deserialize; use serde::Deserialize;
mod uuid; mod uuid;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{Guild, StartAmountQuery}, objects::{Guild, StartAmountQuery},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct GuildInfo { pub struct GuildInfo {
name: String, name: String,
} }
pub fn web() -> Scope { pub fn router() -> Router<Arc<AppState>> {
web::scope("/guilds") Router::new()
.service(post) .route("/", post(new))
.service(get) .route("/", get(get_guilds))
.service(uuid::web()) .nest("/{uuid}", uuid::router())
} }
/// `POST /api/v1/guilds` Creates a new guild /// `POST /api/v1/guilds` Creates a new guild
@ -49,23 +61,18 @@ pub fn web() -> Scope {
/// }); /// });
/// ``` /// ```
/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps
#[post("")] pub async fn new(
pub async fn post( State(app_state): State<Arc<AppState>>,
req: HttpRequest, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
guild_info: web::Json<GuildInfo>, Json(guild_info): Json<GuildInfo>,
data: web::Data<Data>, ) -> Result<impl IntoResponse, Error> {
) -> Result<HttpResponse, Error> { let mut conn = app_state.pool.get().await?;
let headers = req.headers();
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
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?; let guild = Guild::new(&mut conn, guild_info.name.clone(), uuid).await?;
Ok(HttpResponse::Ok().json(guild)) Ok((StatusCode::OK, Json(guild)))
} }
/// `GET /api/v1/servers` Fetches all guilds /// `GET /api/v1/servers` Fetches all guilds
@ -115,25 +122,20 @@ pub async fn post(
/// ]); /// ]);
/// ``` /// ```
/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps
#[get("")] pub async fn get_guilds(
pub async fn get( State(app_state): State<Arc<AppState>>,
req: HttpRequest, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
request_query: web::Query<StartAmountQuery>, Json(request_query): Json<StartAmountQuery>,
data: web::Data<Data>, ) -> Result<impl IntoResponse, Error> {
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let start = request_query.start.unwrap_or(0); let start = request_query.start.unwrap_or(0);
let amount = request_query.amount.unwrap_or(10); let amount = request_query.amount.unwrap_or(10);
let uuid = check_access_token(auth_header, &mut data.pool.get().await?).await?; let uuid = check_access_token(auth.token(), &mut app_state.pool.get().await?).await?;
global_checks(&data, uuid).await?; global_checks(&app_state, uuid).await?;
let guilds = Guild::fetch_amount(&data.pool, start, amount).await?; let guilds = Guild::fetch_amount(&app_state.pool, start, amount).await?;
Ok(HttpResponse::Ok().json(guilds)) Ok((StatusCode::OK, Json(guilds)))
} }

View file

@ -1,92 +1,92 @@
use std::sync::Arc;
use ::uuid::Uuid;
use axum::{
Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use serde::Deserialize;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{Channel, Member, Permissions}, objects::{Channel, Member, Permissions},
utils::{get_auth_header, global_checks, order_by_is_above}, utils::{global_checks, order_by_is_above},
}; };
use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, post, web};
use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
struct ChannelInfo { pub struct ChannelInfo {
name: String, name: String,
description: Option<String>, description: Option<String>,
} }
#[get("{uuid}/channels")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(guild_uuid): Path<Uuid>,
data: web::Data<Data>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let guild_uuid = path.into_inner().0; global_checks(&app_state, uuid).await?;
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?; Member::check_membership(&mut conn, uuid, guild_uuid).await?;
if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_channels")).await { if let Ok(cache_hit) = app_state
return Ok(HttpResponse::Ok() .get_cache_key(format!("{guild_uuid}_channels"))
.content_type("application/json") .await
.body(cache_hit)); {
return Ok((StatusCode::OK, Json(cache_hit)).into_response());
} }
let channels = Channel::fetch_all(&data.pool, guild_uuid).await?; let channels = Channel::fetch_all(&app_state.pool, guild_uuid).await?;
let channels_ordered = order_by_is_above(channels).await?; let channels_ordered = order_by_is_above(channels).await?;
data.set_cache_key( app_state
.set_cache_key(
format!("{guild_uuid}_channels"), format!("{guild_uuid}_channels"),
channels_ordered.clone(), channels_ordered.clone(),
1800, 1800,
) )
.await?; .await?;
Ok(HttpResponse::Ok().json(channels_ordered)) Ok((StatusCode::OK, Json(channels_ordered)).into_response())
} }
#[post("{uuid}/channels")]
pub async fn create( pub async fn create(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
channel_info: web::Json<ChannelInfo>, Path(guild_uuid): Path<Uuid>,
path: web::Path<(Uuid,)>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
data: web::Data<Data>, Json(channel_info): Json<ChannelInfo>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let guild_uuid = path.into_inner().0; global_checks(&app_state, uuid).await?;
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?; let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member member
.check_permission(&data, Permissions::ManageChannel) .check_permission(&app_state, Permissions::ManageChannel)
.await?; .await?;
let channel = Channel::new( let channel = Channel::new(
data.clone(), &app_state,
guild_uuid, guild_uuid,
channel_info.name.clone(), channel_info.name.clone(),
channel_info.description.clone(), channel_info.description.clone(),
) )
.await?; .await?;
Ok(HttpResponse::Ok().json(channel)) Ok((StatusCode::OK, Json(channel)))
} }

View file

@ -1,62 +0,0 @@
//! `/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<Data>,
) -> Result<HttpResponse, Error> {
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())
}

View file

@ -1,37 +1,41 @@
use actix_web::{HttpRequest, HttpResponse, get, post, web}; use std::sync::Arc;
use axum::{
Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{Guild, Member, Permissions}, objects::{Guild, Member, Permissions},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct InviteRequest { pub struct InviteRequest {
custom_id: Option<String>, custom_id: Option<String>,
} }
#[get("{uuid}/invites")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(guild_uuid): Path<Uuid>,
data: web::Data<Data>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let guild_uuid = path.into_inner().0; global_checks(&app_state, uuid).await?;
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?; Member::check_membership(&mut conn, uuid, guild_uuid).await?;
@ -39,32 +43,25 @@ pub async fn get(
let invites = guild.get_invites(&mut conn).await?; let invites = guild.get_invites(&mut conn).await?;
Ok(HttpResponse::Ok().json(invites)) Ok((StatusCode::OK, Json(invites)))
} }
#[post("{uuid}/invites")]
pub async fn create( pub async fn create(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(guild_uuid): Path<Uuid>,
invite_request: web::Json<InviteRequest>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
data: web::Data<Data>, Json(invite_request): Json<InviteRequest>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let guild_uuid = path.into_inner().0; global_checks(&app_state, uuid).await?;
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?; let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member member
.check_permission(&data, Permissions::CreateInvite) .check_permission(&app_state, Permissions::CreateInvite)
.await?; .await?;
let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?;
@ -73,5 +70,5 @@ pub async fn create(
.create_invite(&mut conn, uuid, invite_request.custom_id.clone()) .create_invite(&mut conn, uuid, invite_request.custom_id.clone())
.await?; .await?;
Ok(HttpResponse::Ok().json(invite)) Ok((StatusCode::OK, Json(invite)))
} }

View file

@ -1,36 +1,41 @@
use std::sync::Arc;
use ::uuid::Uuid;
use axum::{
Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{Me, Member}, objects::{Me, Member},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, web};
#[get("{uuid}/members")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(guild_uuid): Path<Uuid>,
data: web::Data<Data>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let guild_uuid = path.into_inner().0; global_checks(&app_state, uuid).await?;
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?; Member::check_membership(&mut conn, uuid, guild_uuid).await?;
let me = Me::get(&mut conn, uuid).await?; let me = Me::get(&mut conn, uuid).await?;
let members = Member::fetch_all(&data, &me, guild_uuid).await?; let members = Member::fetch_all(&app_state, &me, guild_uuid).await?;
Ok(HttpResponse::Ok().json(members)) Ok((StatusCode::OK, Json(members)))
} }

View file

@ -1,40 +1,51 @@
//! `/api/v1/guilds/{uuid}` Specific server endpoints //! `/api/v1/guilds/{uuid}` Specific server endpoints
use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use std::sync::Arc;
use axum::{
Json, Router,
extract::{Multipart, Path, State},
http::StatusCode,
response::IntoResponse,
routing::{get, patch, post},
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use bytes::Bytes;
use uuid::Uuid; use uuid::Uuid;
mod channels; mod channels;
mod icon;
mod invites; mod invites;
mod members; mod members;
mod roles; mod roles;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{Guild, Member}, objects::{Guild, Member, Permissions},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
pub fn web() -> Scope { pub fn router() -> Router<Arc<AppState>> {
web::scope("") Router::new()
// Servers // Servers
.service(get) .route("/", get(get_guild))
.route("/", patch(edit))
// Channels // Channels
.service(channels::get) .route("/channels", get(channels::get))
.service(channels::create) .route("/channels", post(channels::create))
// Roles // Roles
.service(roles::get) .route("/roles", get(roles::get))
.service(roles::create) .route("/roles", post(roles::create))
.service(roles::uuid::get) .route("/roles/{role_uuid}", get(roles::uuid::get))
// Invites // Invites
.service(invites::get) .route("/invites", get(invites::get))
.service(invites::create) .route("/invites", post(invites::create))
// Icon
.service(icon::upload)
// Members // Members
.service(members::get) .route("/members", get(members::get))
} }
/// `GET /api/v1/guilds/{uuid}` DESCRIPTION /// `GET /api/v1/guilds/{uuid}` DESCRIPTION
@ -70,27 +81,69 @@ pub fn web() -> Scope {
/// "member_count": 20 /// "member_count": 20
/// }); /// });
/// ``` /// ```
#[get("/{uuid}")] pub async fn get_guild(
pub async fn get( State(app_state): State<Arc<AppState>>,
req: HttpRequest, Path(guild_uuid): Path<Uuid>,
path: web::Path<(Uuid,)>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
data: web::Data<Data>, ) -> Result<impl IntoResponse, Error> {
) -> Result<HttpResponse, Error> { let mut conn = app_state.pool.get().await?;
let headers = req.headers();
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let guild_uuid = path.into_inner().0; global_checks(&app_state, uuid).await?;
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?; Member::check_membership(&mut conn, uuid, guild_uuid).await?;
let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?;
Ok(HttpResponse::Ok().json(guild)) Ok((StatusCode::OK, Json(guild)))
}
/// `PATCH /api/v1/guilds/{uuid}` change guild settings
///
/// requires auth: yes
pub async fn edit(
State(app_state): State<Arc<AppState>>,
Path(guild_uuid): Path<Uuid>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
mut multipart: Multipart,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
let uuid = check_access_token(auth.token(), &mut conn).await?;
global_checks(&app_state, uuid).await?;
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member
.check_permission(&app_state, Permissions::ManageGuild)
.await?;
let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?;
let mut icon: Option<Bytes> = 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(
&app_state.bunny_storage,
&mut conn,
app_state.config.bunny.cdn_url.clone(),
icon,
)
.await?;
}
Ok(StatusCode::OK)
} }

View file

@ -1,82 +1,78 @@
use std::sync::Arc;
use ::uuid::Uuid; use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, post, web}; use axum::{
Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{Member, Permissions, Role}, objects::{Member, Permissions, Role},
utils::{get_auth_header, global_checks, order_by_is_above}, utils::{global_checks, order_by_is_above},
}; };
pub mod uuid; pub mod uuid;
#[derive(Deserialize)] #[derive(Deserialize)]
struct RoleInfo { pub struct RoleInfo {
name: String, name: String,
} }
#[get("{uuid}/roles")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(guild_uuid): Path<Uuid>,
data: web::Data<Data>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
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?; Member::check_membership(&mut conn, uuid, guild_uuid).await?;
if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_roles")).await { if let Ok(cache_hit) = app_state.get_cache_key(format!("{guild_uuid}_roles")).await {
return Ok(HttpResponse::Ok() return Ok((StatusCode::OK, Json(cache_hit)).into_response());
.content_type("application/json")
.body(cache_hit));
} }
let roles = Role::fetch_all(&mut conn, guild_uuid).await?; let roles = Role::fetch_all(&mut conn, guild_uuid).await?;
let roles_ordered = order_by_is_above(roles).await?; let roles_ordered = order_by_is_above(roles).await?;
data.set_cache_key(format!("{guild_uuid}_roles"), roles_ordered.clone(), 1800) app_state
.set_cache_key(format!("{guild_uuid}_roles"), roles_ordered.clone(), 1800)
.await?; .await?;
Ok(HttpResponse::Ok().json(roles_ordered)) Ok((StatusCode::OK, Json(roles_ordered)).into_response())
} }
#[post("{uuid}/roles")]
pub async fn create( pub async fn create(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
role_info: web::Json<RoleInfo>, Path(guild_uuid): Path<Uuid>,
path: web::Path<(Uuid,)>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
data: web::Data<Data>, Json(role_info): Json<RoleInfo>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let guild_uuid = path.into_inner().0; global_checks(&app_state, uuid).await?;
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?; let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member member
.check_permission(&data, Permissions::ManageRole) .check_permission(&app_state, Permissions::ManageRole)
.await?; .await?;
let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?; let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?;
Ok(HttpResponse::Ok().json(role)) Ok((StatusCode::OK, Json(role)).into_response())
} }

View file

@ -1,43 +1,47 @@
use std::sync::Arc;
use ::uuid::Uuid;
use axum::{
Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{Member, Role}, objects::{Member, Role},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, web};
#[get("{uuid}/roles/{role_uuid}")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid, Uuid)>, Path((guild_uuid, role_uuid)): Path<(Uuid, Uuid)>,
data: web::Data<Data>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let (guild_uuid, role_uuid) = path.into_inner(); global_checks(&app_state, uuid).await?;
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?; Member::check_membership(&mut conn, uuid, guild_uuid).await?;
if let Ok(cache_hit) = data.get_cache_key(format!("{role_uuid}")).await { if let Ok(cache_hit) = app_state.get_cache_key(format!("{role_uuid}")).await {
return Ok(HttpResponse::Ok() return Ok((StatusCode::OK, Json(cache_hit)).into_response());
.content_type("application/json")
.body(cache_hit));
} }
let role = Role::fetch_one(&mut conn, role_uuid).await?; let role = Role::fetch_one(&mut conn, role_uuid).await?;
data.set_cache_key(format!("{role_uuid}"), role.clone(), 60) app_state
.set_cache_key(format!("{role_uuid}"), role.clone(), 60)
.await?; .await?;
Ok(HttpResponse::Ok().json(role)) Ok((StatusCode::OK, Json(role)).into_response())
} }

View file

@ -1,49 +1,53 @@
use actix_web::{HttpRequest, HttpResponse, get, post, web}; use std::sync::Arc;
use axum::{
Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{Guild, Invite, Member}, objects::{Guild, Invite, Member},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
#[get("{id}")] pub async fn get(
pub async fn get(path: web::Path<(String,)>, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
let mut conn = data.pool.get().await?; Path(invite_id): Path<String>,
) -> Result<impl IntoResponse, Error> {
let invite_id = path.into_inner().0; let mut conn = app_state.pool.get().await?;
let invite = Invite::fetch_one(&mut conn, invite_id).await?; let invite = Invite::fetch_one(&mut conn, invite_id).await?;
let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?;
Ok(HttpResponse::Ok().json(guild)) Ok((StatusCode::OK, Json(guild)))
} }
#[post("{id}")]
pub async fn join( pub async fn join(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(String,)>, Path(invite_id): Path<String>,
data: web::Data<Data>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let invite_id = path.into_inner().0; global_checks(&app_state, uuid).await?;
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 invite = Invite::fetch_one(&mut conn, invite_id).await?;
let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?;
Member::new(&data, uuid, guild.uuid).await?; Member::new(&app_state, uuid, guild.uuid).await?;
Ok(HttpResponse::Ok().json(guild)) Ok((StatusCode::OK, Json(guild)))
} }

View file

@ -1,7 +1,16 @@
use actix_web::{Scope, web}; use std::sync::Arc;
use axum::{
Router,
routing::{get, post},
};
use crate::AppState;
mod id; mod id;
pub fn web() -> Scope { pub fn router() -> Router<Arc<AppState>> {
web::scope("/invites").service(id::get).service(id::join) Router::new()
.route("/{id}", get(id::get))
.route("/{id}", post(id::join))
} }

View file

@ -1,38 +1,42 @@
use actix_web::{HttpRequest, HttpResponse, get, post, web}; use std::sync::Arc;
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use serde::Deserialize; use serde::Deserialize;
pub mod uuid; pub mod uuid;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::Me, objects::Me,
utils::{get_auth_header, global_checks, user_uuid_from_username} utils::{global_checks, user_uuid_from_username},
}; };
/// Returns a list of users that are your friends /// Returns a list of users that are your friends
#[get("/friends")] pub async fn get(
pub async fn get(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
let headers = req.headers(); TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let mut conn = data.pool.get().await?; global_checks(&app_state, uuid).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 me = Me::get(&mut conn, uuid).await?;
let friends = me.get_friends(&data).await?; let friends = me.get_friends(&app_state).await?;
Ok(HttpResponse::Ok().json(friends)) Ok((StatusCode::OK, Json(friends)))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct UserReq { pub struct UserReq {
username: String, username: String,
} }
@ -55,26 +59,21 @@ struct UserReq {
/// ///
/// 400 Bad Request (usually means users are already friends) /// 400 Bad Request (usually means users are already friends)
/// ///
#[post("/friends")]
pub async fn post( pub async fn post(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
json: web::Json<UserReq>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
data: web::Data<Data>, Json(user_request): Json<UserReq>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let mut conn = data.pool.get().await?; global_checks(&app_state, uuid).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 me = Me::get(&mut conn, uuid).await?;
let target_uuid = user_uuid_from_username(&mut conn, &json.username).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, target_uuid).await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }

View file

@ -1,33 +1,34 @@
use actix_web::{HttpRequest, HttpResponse, delete, web}; use std::sync::Arc;
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Data, AppState, api::v1::auth::check_access_token, error::Error, objects::Me, utils::global_checks,
api::v1::auth::check_access_token,
error::Error,
objects::Me,
utils::{get_auth_header, global_checks},
}; };
#[delete("/friends/{uuid}")]
pub async fn delete( pub async fn delete(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(friend_uuid): Path<Uuid>,
data: web::Data<Data>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let mut conn = data.pool.get().await?; global_checks(&app_state, uuid).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 me = Me::get(&mut conn, uuid).await?;
me.remove_friend(&mut conn, path.0).await?; me.remove_friend(&mut conn, friend_uuid).await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }

View file

@ -1,13 +1,15 @@
//! `/api/v1/me/guilds` Contains endpoint related to guild memberships //! `/api/v1/me/guilds` Contains endpoint related to guild memberships
use actix_web::{HttpRequest, HttpResponse, get, web}; use std::sync::Arc;
use axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use crate::{ use crate::{
Data, AppState, api::v1::auth::check_access_token, error::Error, objects::Me, utils::global_checks,
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 /// `GET /api/v1/me/guilds` Returns all guild memberships in a list
@ -55,21 +57,19 @@ use crate::{
/// ]); /// ]);
/// ``` /// ```
/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps
#[get("/guilds")] pub async fn get(
pub async fn get(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
let headers = req.headers(); TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let mut conn = data.pool.get().await?; global_checks(&app_state, uuid).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 me = Me::get(&mut conn, uuid).await?;
let memberships = me.fetch_memberships(&mut conn).await?; let memberships = me.fetch_memberships(&mut conn).await?;
Ok(HttpResponse::Ok().json(memberships)) Ok((StatusCode::OK, Json(memberships)))
} }

View file

@ -1,108 +1,120 @@
use actix_multipart::form::{MultipartForm, json::Json as MpJson, tempfile::TempFile}; use std::sync::Arc;
use actix_web::{HttpRequest, HttpResponse, Scope, get, patch, web};
use axum::{
Json, Router,
extract::{DefaultBodyLimit, Multipart, State},
http::StatusCode,
response::IntoResponse,
routing::{delete, get, patch, post},
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use bytes::Bytes;
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
Data, AppState, api::v1::auth::check_access_token, error::Error, objects::Me, utils::global_checks,
api::v1::auth::check_access_token,
error::Error,
objects::Me,
utils::{get_auth_header, global_checks},
}; };
mod friends; mod friends;
mod guilds; mod guilds;
pub fn web() -> Scope { pub fn router() -> Router<Arc<AppState>> {
web::scope("/me") Router::new()
.service(get) .route("/", get(get_me))
.service(update) .route(
.service(guilds::get) "/",
.service(friends::get) patch(update).layer(DefaultBodyLimit::max(
.service(friends::post) 100 * 1024 * 1024, /* limit is in bytes */
.service(friends::uuid::delete) )),
)
.route("/guilds", get(guilds::get))
.route("/friends", get(friends::get))
.route("/friends", post(friends::post))
.route("/friends/{uuid}", delete(friends::uuid::delete))
} }
#[get("")] pub async fn get_me(
pub async fn get(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
let headers = req.headers(); TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
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?; let me = Me::get(&mut conn, uuid).await?;
Ok(HttpResponse::Ok().json(me)) Ok((StatusCode::OK, Json(me)))
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Default, Debug, Deserialize, Clone)]
struct NewInfo { struct NewInfo {
username: Option<String>, username: Option<String>,
display_name: Option<String>, display_name: Option<String>,
//password: Option<String>, will probably be handled through a reset password link
email: Option<String>, email: Option<String>,
pronouns: Option<String>, pronouns: Option<String>,
about: Option<String>, about: Option<String>,
} }
#[derive(Debug, MultipartForm)]
struct UploadForm {
#[multipart(limit = "100MB")]
avatar: Option<TempFile>,
json: MpJson<NewInfo>,
}
#[patch("")]
pub async fn update( pub async fn update(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
MultipartForm(form): MultipartForm<UploadForm>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
data: web::Data<Data>, mut multipart: Multipart,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; let uuid = check_access_token(auth.token(), &mut conn).await?;
let mut conn = data.pool.get().await?; let mut json_raw: Option<NewInfo> = None;
let mut avatar: Option<Bytes> = None;
let uuid = check_access_token(auth_header, &mut conn).await?; while let Some(field) = multipart.next_field().await.unwrap() {
let name = field
.name()
.ok_or(Error::BadRequest("Field has no name".to_string()))?;
if form.avatar.is_some() || form.json.username.is_some() || form.json.display_name.is_some() { if name == "avatar" {
global_checks(&data, uuid).await?; avatar = Some(field.bytes().await?);
} else if name == "json" {
json_raw = Some(serde_json::from_str(&field.text().await?)?)
}
}
let json = json_raw.unwrap_or_default();
if avatar.is_some() || json.username.is_some() || json.display_name.is_some() {
global_checks(&app_state, uuid).await?;
} }
let mut me = Me::get(&mut conn, uuid).await?; let mut me = Me::get(&mut conn, uuid).await?;
if let Some(avatar) = form.avatar { if let Some(avatar) = avatar {
let bytes = tokio::fs::read(avatar.file).await?; me.set_avatar(&app_state, app_state.config.bunny.cdn_url.clone(), avatar)
let byte_slice: &[u8] = &bytes;
me.set_avatar(&data, data.config.bunny.cdn_url.clone(), byte_slice.into())
.await?; .await?;
} }
if let Some(username) = &form.json.username { if let Some(username) = &json.username {
me.set_username(&data, username.clone()).await?; me.set_username(&app_state, username.clone()).await?;
} }
if let Some(display_name) = &form.json.display_name { if let Some(display_name) = &json.display_name {
me.set_display_name(&data, display_name.clone()).await?; me.set_display_name(&app_state, display_name.clone())
.await?;
} }
if let Some(email) = &form.json.email { if let Some(email) = &json.email {
me.set_email(&data, email.clone()).await?; me.set_email(&app_state, email.clone()).await?;
} }
if let Some(pronouns) = &form.json.pronouns { if let Some(pronouns) = &json.pronouns {
me.set_pronouns(&data, pronouns.clone()).await?; me.set_pronouns(&app_state, pronouns.clone()).await?;
} }
if let Some(about) = &form.json.about { if let Some(about) = &json.about {
me.set_about(&data, about.clone()).await?; me.set_about(&app_state, about.clone()).await?;
} }
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }

View file

@ -1,6 +1,10 @@
//! `/api/v1` Contains version 1 of the api //! `/api/v1` Contains version 1 of the api
use actix_web::{Scope, web}; use std::sync::Arc;
use axum::{routing::get, Router};
use crate::AppState;
mod auth; mod auth;
mod channels; mod channels;
@ -10,13 +14,13 @@ mod me;
mod stats; mod stats;
mod users; mod users;
pub fn web() -> Scope { pub fn router() -> Router<Arc<AppState>> {
web::scope("/v1") Router::new()
.service(stats::res) .route("/stats", get(stats::res))
.service(auth::web()) .nest("/auth", auth::router())
.service(users::web()) .nest("/users", users::router())
.service(channels::web()) .nest("/channels", channels::router())
.service(guilds::web()) .nest("/guilds", guilds::router())
.service(invites::web()) .nest("/invites", invites::router())
.service(me::web()) .nest("/me", me::router())
} }

View file

@ -1,13 +1,17 @@
//! `/api/v1/stats` Returns stats about the server //! `/api/v1/stats` Returns stats about the server
use std::sync::Arc;
use std::time::SystemTime; use std::time::SystemTime;
use actix_web::{HttpResponse, get, web}; use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use diesel::QueryDsl; use diesel::QueryDsl;
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::Serialize; use serde::Serialize;
use crate::Data; use crate::AppState;
use crate::error::Error; use crate::error::Error;
use crate::schema::users::dsl::{users, uuid}; use crate::schema::users::dsl::{users, uuid};
@ -39,27 +43,26 @@ struct Response {
/// "build_number": "39d01bb" /// "build_number": "39d01bb"
/// }); /// });
/// ``` /// ```
#[get("/stats")] pub async fn res(State(app_state): State<Arc<AppState>>) -> Result<impl IntoResponse, Error> {
pub async fn res(data: web::Data<Data>) -> Result<HttpResponse, Error> {
let accounts: i64 = users let accounts: i64 = users
.select(uuid) .select(uuid)
.count() .count()
.get_result(&mut data.pool.get().await?) .get_result(&mut app_state.pool.get().await?)
.await?; .await?;
let response = Response { let response = Response {
// TODO: Get number of accounts from db // TODO: Get number of accounts from db
accounts, accounts,
uptime: SystemTime::now() uptime: SystemTime::now()
.duration_since(data.start_time) .duration_since(app_state.start_time)
.expect("Seriously why dont you have time??") .expect("Seriously why dont you have time??")
.as_secs(), .as_secs(),
version: String::from(VERSION.unwrap_or("UNKNOWN")), version: String::from(VERSION.unwrap_or("UNKNOWN")),
registration_enabled: data.config.instance.registration, registration_enabled: app_state.config.instance.registration,
email_verification_required: data.config.instance.require_email_verification, email_verification_required: app_state.config.instance.require_email_verification,
// TODO: Get build number from git hash or remove this from the spec // TODO: Get build number from git hash or remove this from the spec
build_number: String::from(GIT_SHORT_HASH), build_number: String::from(GIT_SHORT_HASH),
}; };
Ok(HttpResponse::Ok().json(response)) Ok((StatusCode::OK, Json(response)))
} }

View file

@ -1,19 +1,33 @@
//! `/api/v1/users` Contains endpoints related to all users //! `/api/v1/users` Contains endpoints related to all users
use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use std::sync::Arc;
use axum::{
Json, Router,
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
routing::get,
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{StartAmountQuery, User}, objects::{StartAmountQuery, User},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
mod uuid; mod uuid;
pub fn web() -> Scope { pub fn router() -> Router<Arc<AppState>> {
web::scope("/users").service(get).service(uuid::get) Router::new()
.route("/", get(users))
.route("/{uuid}", get(uuid::get))
} }
/// `GET /api/v1/users` Returns all users on this instance /// `GET /api/v1/users` Returns all users on this instance
@ -46,31 +60,26 @@ pub fn web() -> Scope {
/// ]); /// ]);
/// ``` /// ```
/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps
#[get("")] pub async fn users(
pub async fn get( State(app_state): State<Arc<AppState>>,
req: HttpRequest, Query(request_query): Query<StartAmountQuery>,
request_query: web::Query<StartAmountQuery>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
data: web::Data<Data>, ) -> Result<impl IntoResponse, Error> {
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let start = request_query.start.unwrap_or(0); let start = request_query.start.unwrap_or(0);
let amount = request_query.amount.unwrap_or(10); let amount = request_query.amount.unwrap_or(10);
if amount > 100 { if amount > 100 {
return Ok(HttpResponse::BadRequest().finish()); return Ok(StatusCode::BAD_REQUEST.into_response());
} }
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?; let uuid = check_access_token(auth.token(), &mut conn).await?;
global_checks(&data, uuid).await?; global_checks(&app_state, uuid).await?;
let users = User::fetch_amount(&mut conn, start, amount).await?; let users = User::fetch_amount(&mut conn, start, amount).await?;
Ok(HttpResponse::Ok().json(users)) Ok((StatusCode::OK, Json(users)).into_response())
} }

View file

@ -1,14 +1,25 @@
//! `/api/v1/users/{uuid}` Specific user endpoints //! `/api/v1/users/{uuid}` Specific user endpoints
use actix_web::{HttpRequest, HttpResponse, get, web}; use std::sync::Arc;
use axum::{
Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error, error::Error,
objects::{Me, User}, objects::{Me, User},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
/// `GET /api/v1/users/{uuid}` Returns user with the given UUID /// `GET /api/v1/users/{uuid}` Returns user with the given UUID
@ -27,27 +38,20 @@ use crate::{
/// }); /// });
/// ``` /// ```
/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps /// 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( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(user_uuid): Path<Uuid>,
data: web::Data<Data>, TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let user_uuid = path.into_inner().0; let uuid = check_access_token(auth.token(), &mut conn).await?;
let auth_header = get_auth_header(headers)?; global_checks(&app_state, uuid).await?;
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 me = Me::get(&mut conn, uuid).await?;
let user = User::fetch_one_with_friendship(&data, &me, user_uuid).await?; let user = User::fetch_one_with_friendship(&app_state, &me, user_uuid).await?;
Ok(HttpResponse::Ok().json(user)) Ok((StatusCode::OK, Json(user)))
} }

View file

@ -1,5 +1,5 @@
//! `/api/v1/versions` Returns info about api versions //! `/api/v1/versions` Returns info about api versions
use actix_web::{HttpResponse, Responder, get}; use axum::{Json, http::StatusCode, response::IntoResponse};
use serde::Serialize; use serde::Serialize;
#[derive(Serialize)] #[derive(Serialize)]
@ -24,13 +24,12 @@ struct UnstableFeatures;
/// ] /// ]
/// }); /// });
/// ``` /// ```
#[get("/versions")] pub async fn versions() -> impl IntoResponse {
pub async fn get() -> impl Responder {
let response = Response { let response = Response {
unstable_features: UnstableFeatures, unstable_features: UnstableFeatures,
// TODO: Find a way to dynamically update this possibly? // TODO: Find a way to dynamically update this possibly?
versions: vec![String::from("1")], versions: vec![String::from("1")],
}; };
HttpResponse::Ok().json(response) (StatusCode::OK, Json(response))
} }

View file

@ -1,12 +1,16 @@
use std::{io, time::SystemTimeError}; use std::{io, time::SystemTimeError};
use actix_web::{ use axum::{
HttpResponse, Json,
error::{PayloadError, ResponseError}, extract::{
multipart::MultipartError,
rejection::{JsonRejection, QueryRejection},
},
http::{ http::{
StatusCode, StatusCode,
header::{ContentType, ToStrError}, header::{InvalidHeaderValue, ToStrError},
}, },
response::IntoResponse,
}; };
use bunny_api_tokio::error::Error as BunnyError; use bunny_api_tokio::error::Error as BunnyError;
use deadpool::managed::{BuildError, PoolError}; use deadpool::managed::{BuildError, PoolError};
@ -54,9 +58,13 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
UrlParseError(#[from] url::ParseError), UrlParseError(#[from] url::ParseError),
#[error(transparent)] #[error(transparent)]
PayloadError(#[from] PayloadError), JsonRejection(#[from] JsonRejection),
#[error(transparent)] #[error(transparent)]
WsClosed(#[from] actix_ws::Closed), QueryRejection(#[from] QueryRejection),
#[error(transparent)]
MultipartError(#[from] MultipartError),
#[error(transparent)]
InvalidHeaderValue(#[from] InvalidHeaderValue),
#[error(transparent)] #[error(transparent)]
EmailError(#[from] EmailError), EmailError(#[from] EmailError),
#[error(transparent)] #[error(transparent)]
@ -77,26 +85,40 @@ pub enum Error {
InternalServerError(String), InternalServerError(String),
} }
impl ResponseError for Error { impl IntoResponse for Error {
fn error_response(&self) -> HttpResponse { 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;
debug!("{self:?}"); debug!("{self:?}");
error!("{}: {}", self.status_code(), self); error!("{code}: {self}");
HttpResponse::build(self.status_code()) error.into_response()
.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,
}
} }
} }

View file

@ -1,13 +1,13 @@
use actix_cors::Cors;
use actix_web::{App, HttpServer, web};
use argon2::Argon2; use argon2::Argon2;
use axum::Router;
use clap::Parser; use clap::Parser;
use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::AsyncDieselConnectionManager;
use diesel_async::pooled_connection::deadpool::Pool; use diesel_async::pooled_connection::deadpool::Pool;
use error::Error; use error::Error;
use objects::MailClient; use objects::MailClient;
use simple_logger::SimpleLogger; use socketioxide::SocketIo;
use std::time::SystemTime; use std::{sync::Arc, time::SystemTime};
use tower_http::cors::{Any, CorsLayer};
mod config; mod config;
use config::{Config, ConfigBuilder}; use config::{Config, ConfigBuilder};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
@ -22,6 +22,7 @@ pub mod error;
pub mod objects; pub mod objects;
pub mod schema; pub mod schema;
pub mod utils; pub mod utils;
mod socket;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@ -31,7 +32,7 @@ struct Args {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct Data { pub struct AppState {
pub pool: deadpool::managed::Pool< pub pool: deadpool::managed::Pool<
AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>, AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>,
Conn, Conn,
@ -46,12 +47,14 @@ pub struct Data {
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Error> { async fn main() -> Result<(), Error> {
SimpleLogger::new() tracing_subscriber::fmt::init();
.with_level(log::LevelFilter::Info)
.with_colors(true) //SimpleLogger::new()
.env() // .with_level(log::LevelFilter::Info)
.init() // .with_colors(true)
.unwrap(); // .env()
// .init()
// .unwrap();
let args = Args::parse(); let args = Args::parse();
let config = ConfigBuilder::load(args.config).await?.build(); let config = ConfigBuilder::load(args.config).await?.build();
@ -112,7 +115,7 @@ async fn main() -> Result<(), Error> {
) )
*/ */
let data = Data { let app_state = Arc::new(AppState {
pool, pool,
cache_pool, cache_pool,
config, config,
@ -121,42 +124,31 @@ async fn main() -> Result<(), Error> {
start_time: SystemTime::now(), start_time: SystemTime::now(),
bunny_storage, bunny_storage,
mail_client, mail_client,
}; });
HttpServer::new(move || { let cors = CorsLayer::new()
// Set CORS headers // Allow any origin (equivalent to allowed_origin_fn returning true)
let cors = Cors::default() .allow_origin(Any)
/* // Allow any method
Set Allowed-Control-Allow-Origin header to whatever .allow_methods(Any)
the request's Origin header is. Must be done like this // Allow any headers
rather than setting it to "*" due to CORS not allowing .allow_headers(Any);
sending of credentials (cookies) with wildcard origin.
*/
.allowed_origin_fn(|_origin, _req_head| true)
/*
Allows any request method in CORS preflight requests.
This will be restricted to only ones actually in use later.
*/
.allow_any_method()
/*
Allows any header(s) in request in CORS preflight requests.
This wll be restricted to only ones actually in use later.
*/
.allow_any_header()
/*
Allows browser to include cookies in requests.
This is needed for receiving the secure HttpOnly refresh_token cookie.
*/
.supports_credentials();
App::new() let (socket_io, io) = SocketIo::builder().with_state(app_state.clone()).build_layer();
.app_data(web::Data::new(data.clone()))
.wrap(cors) io.ns("/", socket::on_connect);
.service(api::web(data.config.web.backend_url.path()))
}) // build our application with a route
.bind((web.ip, web.port))? let app = Router::new()
.run() // `GET /` goes to `root`
.await?; .nest(web.backend_url.path(), api::router())
.with_state(app_state)
.layer(cors)
.layer(socket_io);
// 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?;
Ok(()) Ok(())
} }

View file

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Conn, Data, AppState, Conn,
error::Error, error::Error,
schema::{channel_permissions, channels, messages}, schema::{channel_permissions, channels, messages},
utils::{CHANNEL_REGEX, order_by_is_above}, utils::{CHANNEL_REGEX, order_by_is_above},
@ -105,12 +105,12 @@ impl Channel {
futures::future::try_join_all(channel_futures).await futures::future::try_join_all(channel_futures).await
} }
pub async fn fetch_one(data: &Data, channel_uuid: Uuid) -> Result<Self, Error> { pub async fn fetch_one(app_state: &AppState, channel_uuid: Uuid) -> Result<Self, Error> {
if let Ok(cache_hit) = data.get_cache_key(channel_uuid.to_string()).await { if let Ok(cache_hit) = app_state.get_cache_key(channel_uuid.to_string()).await {
return Ok(serde_json::from_str(&cache_hit)?); return Ok(serde_json::from_str(&cache_hit)?);
} }
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
use channels::dsl; use channels::dsl;
let channel_builder: ChannelBuilder = dsl::channels let channel_builder: ChannelBuilder = dsl::channels
@ -121,14 +121,15 @@ impl Channel {
let channel = channel_builder.build(&mut conn).await?; let channel = channel_builder.build(&mut conn).await?;
data.set_cache_key(channel_uuid.to_string(), channel.clone(), 60) app_state
.set_cache_key(channel_uuid.to_string(), channel.clone(), 60)
.await?; .await?;
Ok(channel) Ok(channel)
} }
pub async fn new( pub async fn new(
data: actix_web::web::Data<Data>, app_state: &AppState,
guild_uuid: Uuid, guild_uuid: Uuid,
name: String, name: String,
description: Option<String>, description: Option<String>,
@ -137,11 +138,11 @@ impl Channel {
return Err(Error::BadRequest("Channel name is invalid".to_string())); return Err(Error::BadRequest("Channel name is invalid".to_string()));
} }
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
let channel_uuid = Uuid::now_v7(); let channel_uuid = Uuid::now_v7();
let channels = Self::fetch_all(&data.pool, guild_uuid).await?; let channels = Self::fetch_all(&app_state.pool, guild_uuid).await?;
let channels_ordered = order_by_is_above(channels).await?; let channels_ordered = order_by_is_above(channels).await?;
@ -179,22 +180,25 @@ impl Channel {
permissions: vec![], permissions: vec![],
}; };
data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) app_state
.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800)
.await?; .await?;
if data if app_state
.get_cache_key(format!("{guild_uuid}_channels")) .get_cache_key(format!("{guild_uuid}_channels"))
.await .await
.is_ok() .is_ok()
{ {
data.del_cache_key(format!("{guild_uuid}_channels")).await?; app_state
.del_cache_key(format!("{guild_uuid}_channels"))
.await?;
} }
Ok(channel) Ok(channel)
} }
pub async fn delete(self, data: &Data) -> Result<(), Error> { pub async fn delete(self, app_state: &AppState) -> Result<(), Error> {
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
use channels::dsl; use channels::dsl;
match update(channels::table) match update(channels::table)
@ -224,16 +228,17 @@ impl Channel {
Err(e) => Err(e), Err(e) => Err(e),
}?; }?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() {
data.del_cache_key(self.uuid.to_string()).await?; app_state.del_cache_key(self.uuid.to_string()).await?;
} }
if data if app_state
.get_cache_key(format!("{}_channels", self.guild_uuid)) .get_cache_key(format!("{}_channels", self.guild_uuid))
.await .await
.is_ok() .is_ok()
{ {
data.del_cache_key(format!("{}_channels", self.guild_uuid)) app_state
.del_cache_key(format!("{}_channels", self.guild_uuid))
.await?; .await?;
} }
@ -242,11 +247,11 @@ impl Channel {
pub async fn fetch_messages( pub async fn fetch_messages(
&self, &self,
data: &Data, app_state: &AppState,
amount: i64, amount: i64,
offset: i64, offset: i64,
) -> Result<Vec<Message>, Error> { ) -> Result<Vec<Message>, Error> {
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
use messages::dsl; use messages::dsl;
let messages: Vec<MessageBuilder> = load_or_empty( let messages: Vec<MessageBuilder> = load_or_empty(
@ -260,14 +265,14 @@ impl Channel {
.await, .await,
)?; )?;
let message_futures = messages.iter().map(async move |b| b.build(data).await); let message_futures = messages.iter().map(async move |b| b.build(app_state).await);
futures::future::try_join_all(message_futures).await futures::future::try_join_all(message_futures).await
} }
pub async fn new_message( pub async fn new_message(
&self, &self,
data: &Data, app_state: &AppState,
user_uuid: Uuid, user_uuid: Uuid,
message: String, message: String,
reply_to: Option<Uuid>, reply_to: Option<Uuid>,
@ -282,22 +287,22 @@ impl Channel {
reply_to, reply_to,
}; };
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
insert_into(messages::table) insert_into(messages::table)
.values(message.clone()) .values(message.clone())
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
message.build(data).await message.build(app_state).await
} }
pub async fn set_name(&mut self, data: &Data, new_name: String) -> Result<(), Error> { pub async fn set_name(&mut self, app_state: &AppState, new_name: String) -> Result<(), Error> {
if !CHANNEL_REGEX.is_match(&new_name) { if !CHANNEL_REGEX.is_match(&new_name) {
return Err(Error::BadRequest("Channel name is invalid".to_string())); return Err(Error::BadRequest("Channel name is invalid".to_string()));
} }
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
use channels::dsl; use channels::dsl;
update(channels::table) update(channels::table)
@ -313,10 +318,10 @@ impl Channel {
pub async fn set_description( pub async fn set_description(
&mut self, &mut self,
data: &Data, app_state: &AppState,
new_description: String, new_description: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
use channels::dsl; use channels::dsl;
update(channels::table) update(channels::table)
@ -330,8 +335,12 @@ impl Channel {
Ok(()) Ok(())
} }
pub async fn move_channel(&mut self, data: &Data, new_is_above: Uuid) -> Result<(), Error> { pub async fn move_channel(
let mut conn = data.pool.get().await?; &mut self,
app_state: &AppState,
new_is_above: Uuid,
) -> Result<(), Error> {
let mut conn = app_state.pool.get().await?;
use channels::dsl; use channels::dsl;
let old_above_uuid: Option<Uuid> = match dsl::channels let old_above_uuid: Option<Uuid> = match dsl::channels

View file

@ -3,7 +3,7 @@ use lettre::message::MultiPart;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{Data, error::Error, utils::generate_token}; use crate::{AppState, error::Error, utils::generate_token};
use super::Me; use super::Me;
@ -15,9 +15,9 @@ pub struct EmailToken {
} }
impl EmailToken { impl EmailToken {
pub async fn get(data: &Data, user_uuid: Uuid) -> Result<EmailToken, Error> { pub async fn get(app_state: &AppState, user_uuid: Uuid) -> Result<EmailToken, Error> {
let email_token = serde_json::from_str( let email_token = serde_json::from_str(
&data &app_state
.get_cache_key(format!("{user_uuid}_email_verify")) .get_cache_key(format!("{user_uuid}_email_verify"))
.await?, .await?,
)?; )?;
@ -26,7 +26,7 @@ impl EmailToken {
} }
#[allow(clippy::new_ret_no_self)] #[allow(clippy::new_ret_no_self)]
pub async fn new(data: &Data, me: Me) -> Result<(), Error> { pub async fn new(app_state: &AppState, me: Me) -> Result<(), Error> {
let token = generate_token::<32>()?; let token = generate_token::<32>()?;
let email_token = EmailToken { let email_token = EmailToken {
@ -36,30 +36,32 @@ impl EmailToken {
created_at: Utc::now(), created_at: Utc::now(),
}; };
data.set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400) app_state
.set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400)
.await?; .await?;
let mut verify_endpoint = data.config.web.frontend_url.join("verify-email")?; let mut verify_endpoint = app_state.config.web.frontend_url.join("verify-email")?;
verify_endpoint.set_query(Some(&format!("token={token}"))); verify_endpoint.set_query(Some(&format!("token={token}")));
let email = data let email = app_state
.mail_client .mail_client
.message_builder() .message_builder()
.to(me.email.parse()?) .to(me.email.parse()?)
.subject(format!("{} E-mail Verification", data.config.instance.name)) .subject(format!("{} E-mail Verification", app_state.config.instance.name))
.multipart(MultiPart::alternative_plain_html( .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.", data.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.", app_state.config.instance.name, me.username, verify_endpoint),
format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root{{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark){{:root{{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px){{.container{{width: 100%;}}}}body{{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header{{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button{{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover{{background-color: var(--secondary-colour);}}.content{{padding: 20px 30px;}}.footer{{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>Verify your {} Account</h1></div><div class="content"><h2>Hello, {}!</h2><p>Thanks for creating a new account on Gorb.</p><p>The final step to create your account is to verify your email address by clicking the button below, within 24 hours.</p><a href="{}" class="verify-button">VERIFY ACCOUNT</a><p>If you didn't ask to verify this address, you can safely ignore this email.</p><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, data.config.instance.name, me.username, verify_endpoint) format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root{{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark){{:root{{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px){{.container{{width: 100%;}}}}body{{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header{{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button{{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover{{background-color: var(--secondary-colour);}}.content{{padding: 20px 30px;}}.footer{{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>Verify your {} Account</h1></div><div class="content"><h2>Hello, {}!</h2><p>Thanks for creating a new account on Gorb.</p><p>The final step to create your account is to verify your email address by clicking the button below, within 24 hours.</p><a href="{}" class="verify-button">VERIFY ACCOUNT</a><p>If you didn't ask to verify this address, you can safely ignore this email.</p><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, app_state.config.instance.name, me.username, verify_endpoint)
))?; ))?;
data.mail_client.send_mail(email).await?; app_state.mail_client.send_mail(email).await?;
Ok(()) Ok(())
} }
pub async fn delete(&self, data: &Data) -> Result<(), Error> { pub async fn delete(&self, app_state: &AppState) -> Result<(), Error> {
data.del_cache_key(format!("{}_email_verify", self.user_uuid)) app_state
.del_cache_key(format!("{}_email_verify", self.user_uuid))
.await?; .await?;
Ok(()) Ok(())

View file

@ -1,4 +1,4 @@
use actix_web::web::BytesMut; use axum::body::Bytes;
use diesel::{ use diesel::{
ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into,
update, update,
@ -191,7 +191,7 @@ impl Guild {
bunny_storage: &bunny_api_tokio::EdgeStorageClient, bunny_storage: &bunny_api_tokio::EdgeStorageClient,
conn: &mut Conn, conn: &mut Conn,
cdn_url: Url, cdn_url: Url,
icon: BytesMut, icon: Bytes,
) -> Result<(), Error> { ) -> Result<(), Error> {
let icon_clone = icon.clone(); let icon_clone = icon.clone();
let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??; let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??;
@ -204,7 +204,7 @@ impl Guild {
let path = format!("icons/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type); let path = format!("icons/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type);
bunny_storage.upload(path.clone(), icon.into()).await?; bunny_storage.upload(path.clone(), icon).await?;
let icon_url = cdn_url.join(&path)?; let icon_url = cdn_url.join(&path)?;

View file

@ -1,4 +1,4 @@
use actix_web::web::BytesMut; use axum::body::Bytes;
use diesel::{ use diesel::{
ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into, ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into,
update, update,
@ -10,7 +10,7 @@ use url::Url;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Conn, Data, AppState, Conn,
error::Error, error::Error,
objects::{Friend, FriendRequest, User}, objects::{Friend, FriendRequest, User},
schema::{friend_requests, friends, guild_members, guilds, users}, schema::{friend_requests, friends, guild_members, guilds, users},
@ -75,28 +75,26 @@ impl Me {
pub async fn set_avatar( pub async fn set_avatar(
&mut self, &mut self,
data: &Data, app_state: &AppState,
cdn_url: Url, cdn_url: Url,
avatar: BytesMut, avatar: Bytes,
) -> Result<(), Error> { ) -> Result<(), Error> {
let avatar_clone = avatar.clone(); let avatar_clone = avatar.clone();
let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??; let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??;
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
if let Some(avatar) = &self.avatar { if let Some(avatar) = &self.avatar {
let avatar_url: Url = avatar.parse()?; let avatar_url: Url = avatar.parse()?;
let relative_url = avatar_url.path().trim_start_matches('/'); let relative_url = avatar_url.path().trim_start_matches('/');
data.bunny_storage.delete(relative_url).await?; app_state.bunny_storage.delete(relative_url).await?;
} }
let path = format!("avatar/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type); let path = format!("avatar/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type);
data.bunny_storage app_state.bunny_storage.upload(path.clone(), avatar).await?;
.upload(path.clone(), avatar.into())
.await?;
let avatar_url = cdn_url.join(&path)?; let avatar_url = cdn_url.join(&path)?;
@ -107,8 +105,8 @@ impl Me {
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() {
data.del_cache_key(self.uuid.to_string()).await? app_state.del_cache_key(self.uuid.to_string()).await?
} }
self.avatar = Some(avatar_url.to_string()); self.avatar = Some(avatar_url.to_string());
@ -127,7 +125,11 @@ impl Me {
Ok(()) Ok(())
} }
pub async fn set_username(&mut self, data: &Data, new_username: String) -> Result<(), Error> { pub async fn set_username(
&mut self,
app_state: &AppState,
new_username: String,
) -> Result<(), Error> {
if !USERNAME_REGEX.is_match(&new_username) if !USERNAME_REGEX.is_match(&new_username)
|| new_username.len() < 3 || new_username.len() < 3
|| new_username.len() > 32 || new_username.len() > 32
@ -135,7 +137,7 @@ impl Me {
return Err(Error::BadRequest("Invalid username".to_string())); return Err(Error::BadRequest("Invalid username".to_string()));
} }
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
use users::dsl; use users::dsl;
update(users::table) update(users::table)
@ -144,8 +146,8 @@ impl Me {
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() {
data.del_cache_key(self.uuid.to_string()).await? app_state.del_cache_key(self.uuid.to_string()).await?
} }
self.username = new_username; self.username = new_username;
@ -155,10 +157,10 @@ impl Me {
pub async fn set_display_name( pub async fn set_display_name(
&mut self, &mut self,
data: &Data, app_state: &AppState,
new_display_name: String, new_display_name: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
let new_display_name_option = if new_display_name.is_empty() { let new_display_name_option = if new_display_name.is_empty() {
None None
@ -173,8 +175,8 @@ impl Me {
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() {
data.del_cache_key(self.uuid.to_string()).await? app_state.del_cache_key(self.uuid.to_string()).await?
} }
self.display_name = new_display_name_option; self.display_name = new_display_name_option;
@ -182,12 +184,16 @@ impl Me {
Ok(()) Ok(())
} }
pub async fn set_email(&mut self, data: &Data, new_email: String) -> Result<(), Error> { pub async fn set_email(
&mut self,
app_state: &AppState,
new_email: String,
) -> Result<(), Error> {
if !EMAIL_REGEX.is_match(&new_email) { if !EMAIL_REGEX.is_match(&new_email) {
return Err(Error::BadRequest("Invalid username".to_string())); return Err(Error::BadRequest("Invalid username".to_string()));
} }
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
use users::dsl; use users::dsl;
update(users::table) update(users::table)
@ -199,8 +205,8 @@ impl Me {
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() {
data.del_cache_key(self.uuid.to_string()).await? app_state.del_cache_key(self.uuid.to_string()).await?
} }
self.email = new_email; self.email = new_email;
@ -208,8 +214,12 @@ impl Me {
Ok(()) Ok(())
} }
pub async fn set_pronouns(&mut self, data: &Data, new_pronouns: String) -> Result<(), Error> { pub async fn set_pronouns(
let mut conn = data.pool.get().await?; &mut self,
app_state: &AppState,
new_pronouns: String,
) -> Result<(), Error> {
let mut conn = app_state.pool.get().await?;
use users::dsl; use users::dsl;
update(users::table) update(users::table)
@ -218,15 +228,19 @@ impl Me {
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() {
data.del_cache_key(self.uuid.to_string()).await? app_state.del_cache_key(self.uuid.to_string()).await?
} }
Ok(()) Ok(())
} }
pub async fn set_about(&mut self, data: &Data, new_about: String) -> Result<(), Error> { pub async fn set_about(
let mut conn = data.pool.get().await?; &mut self,
app_state: &AppState,
new_about: String,
) -> Result<(), Error> {
let mut conn = app_state.pool.get().await?;
use users::dsl; use users::dsl;
update(users::table) update(users::table)
@ -235,8 +249,8 @@ impl Me {
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if app_state.get_cache_key(self.uuid.to_string()).await.is_ok() {
data.del_cache_key(self.uuid.to_string()).await? app_state.del_cache_key(self.uuid.to_string()).await?
} }
Ok(()) Ok(())
@ -352,10 +366,10 @@ impl Me {
Ok(()) Ok(())
} }
pub async fn get_friends(&self, data: &Data) -> Result<Vec<User>, Error> { pub async fn get_friends(&self, app_state: &AppState) -> Result<Vec<User>, Error> {
use friends::dsl; use friends::dsl;
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
let friends1 = load_or_empty( let friends1 = load_or_empty(
dsl::friends dsl::friends
@ -374,13 +388,13 @@ impl Me {
)?; )?;
let friend_futures = friends1.iter().map(async move |friend| { let friend_futures = friends1.iter().map(async move |friend| {
User::fetch_one_with_friendship(data, self, friend.uuid2).await User::fetch_one_with_friendship(app_state, self, friend.uuid2).await
}); });
let mut friends = futures::future::try_join_all(friend_futures).await?; let mut friends = futures::future::try_join_all(friend_futures).await?;
let friend_futures = friends2.iter().map(async move |friend| { let friend_futures = friends2.iter().map(async move |friend| {
User::fetch_one_with_friendship(data, self, friend.uuid1).await User::fetch_one_with_friendship(app_state, self, friend.uuid1).await
}); });
friends.append(&mut futures::future::try_join_all(friend_futures).await?); friends.append(&mut futures::future::try_join_all(friend_futures).await?);

View file

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Conn, Data, AppState, Conn,
error::Error, error::Error,
objects::{Me, Permissions, Role}, objects::{Me, Permissions, Role},
schema::guild_members, schema::guild_members,
@ -26,13 +26,13 @@ pub struct MemberBuilder {
} }
impl MemberBuilder { impl MemberBuilder {
pub async fn build(&self, data: &Data, me: Option<&Me>) -> Result<Member, Error> { pub async fn build(&self, app_state: &AppState, me: Option<&Me>) -> Result<Member, Error> {
let user; let user;
if let Some(me) = me { if let Some(me) = me {
user = User::fetch_one_with_friendship(data, me, self.user_uuid).await?; user = User::fetch_one_with_friendship(app_state, me, self.user_uuid).await?;
} else { } else {
user = User::fetch_one(data, self.user_uuid).await?; user = User::fetch_one(app_state, self.user_uuid).await?;
} }
Ok(Member { Ok(Member {
@ -47,11 +47,11 @@ impl MemberBuilder {
pub async fn check_permission( pub async fn check_permission(
&self, &self,
data: &Data, app_state: &AppState,
permission: Permissions, permission: Permissions,
) -> Result<(), Error> { ) -> Result<(), Error> {
if !self.is_owner { if !self.is_owner {
let roles = Role::fetch_from_member(data, self.uuid).await?; let roles = Role::fetch_from_member(app_state, self.uuid).await?;
let allowed = roles.iter().any(|r| r.permissions & permission as i64 != 0); let allowed = roles.iter().any(|r| r.permissions & permission as i64 != 0);
if !allowed { if !allowed {
return Err(Error::Forbidden("Not allowed".to_string())); return Err(Error::Forbidden("Not allowed".to_string()));
@ -101,12 +101,12 @@ impl Member {
} }
pub async fn fetch_one( pub async fn fetch_one(
data: &Data, app_state: &AppState,
me: &Me, me: &Me,
user_uuid: Uuid, user_uuid: Uuid,
guild_uuid: Uuid, guild_uuid: Uuid,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
use guild_members::dsl; use guild_members::dsl;
let member: MemberBuilder = dsl::guild_members let member: MemberBuilder = dsl::guild_members
@ -116,11 +116,15 @@ impl Member {
.get_result(&mut conn) .get_result(&mut conn)
.await?; .await?;
member.build(data, Some(me)).await member.build(app_state, Some(me)).await
} }
pub async fn fetch_all(data: &Data, me: &Me, guild_uuid: Uuid) -> Result<Vec<Self>, Error> { pub async fn fetch_all(
let mut conn = data.pool.get().await?; app_state: &AppState,
me: &Me,
guild_uuid: Uuid,
) -> Result<Vec<Self>, Error> {
let mut conn = app_state.pool.get().await?;
use guild_members::dsl; use guild_members::dsl;
let member_builders: Vec<MemberBuilder> = load_or_empty( let member_builders: Vec<MemberBuilder> = load_or_empty(
@ -134,14 +138,18 @@ impl Member {
let mut members = vec![]; let mut members = vec![];
for builder in member_builders { for builder in member_builders {
members.push(builder.build(&data, Some(me)).await?); members.push(builder.build(app_state, Some(me)).await?);
} }
Ok(members) Ok(members)
} }
pub async fn new(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result<Self, Error> { pub async fn new(
let mut conn = data.pool.get().await?; app_state: &AppState,
user_uuid: Uuid,
guild_uuid: Uuid,
) -> Result<Self, Error> {
let mut conn = app_state.pool.get().await?;
let member_uuid = Uuid::now_v7(); let member_uuid = Uuid::now_v7();
@ -158,6 +166,6 @@ impl Member {
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
member.build(data, None).await member.build(app_state, None).await
} }
} }

View file

@ -2,7 +2,7 @@ use diesel::{Insertable, Queryable, Selectable};
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
use crate::{Data, error::Error, schema::messages}; use crate::{AppState, error::Error, schema::messages};
use super::User; use super::User;
@ -18,8 +18,8 @@ pub struct MessageBuilder {
} }
impl MessageBuilder { impl MessageBuilder {
pub async fn build(&self, data: &Data) -> Result<Message, Error> { pub async fn build(&self, app_state: &AppState) -> Result<Message, Error> {
let user = User::fetch_one(data, self.user_uuid).await?; let user = User::fetch_one(app_state, self.user_uuid).await?;
Ok(Message { Ok(Message {
uuid: self.uuid, uuid: self.uuid,

View file

@ -42,6 +42,37 @@ pub trait HasUuid {
pub trait HasIsAbove { pub trait HasIsAbove {
fn is_above(&self) -> Option<&Uuid>; fn is_above(&self) -> Option<&Uuid>;
} }
/*
pub trait Cookies {
fn cookies(&self) -> CookieJar;
fn cookie<T: AsRef<str>>(&self, cookie: T) -> Option<Cookie>;
}
impl Cookies for Request<Body> {
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::<Result<Vec<Cookie>, cookie::ParseError>>().ok())
.unwrap_or(vec![]);
let mut cookie_jar = CookieJar::new();
for cookie in cookies {
cookie_jar.add(cookie)
}
cookie_jar
}
fn cookie<T: AsRef<str>>(&self, cookie: T) -> Option<Cookie> {
self.cookies()
.get(cookie.as_ref())
.and_then(|c| Some(c.to_owned()))
}
}
*/
fn load_or_empty<T>( fn load_or_empty<T>(
query_result: Result<Vec<T>, diesel::result::Error>, query_result: Result<Vec<T>, diesel::result::Error>,

View file

@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Data, AppState,
error::Error, error::Error,
schema::users, schema::users,
utils::{PASSWORD_REGEX, generate_token, global_checks, user_uuid_from_identifier}, utils::{PASSWORD_REGEX, generate_token, global_checks, user_uuid_from_identifier},
@ -24,10 +24,11 @@ pub struct PasswordResetToken {
} }
impl PasswordResetToken { impl PasswordResetToken {
pub async fn get(data: &Data, token: String) -> Result<PasswordResetToken, Error> { pub async fn get(app_state: &AppState, token: String) -> Result<PasswordResetToken, Error> {
let user_uuid: Uuid = serde_json::from_str(&data.get_cache_key(token.to_string()).await?)?; let user_uuid: Uuid =
serde_json::from_str(&app_state.get_cache_key(token.to_string()).await?)?;
let password_reset_token = serde_json::from_str( let password_reset_token = serde_json::from_str(
&data &app_state
.get_cache_key(format!("{user_uuid}_password_reset")) .get_cache_key(format!("{user_uuid}_password_reset"))
.await?, .await?,
)?; )?;
@ -36,15 +37,15 @@ impl PasswordResetToken {
} }
pub async fn get_with_identifier( pub async fn get_with_identifier(
data: &Data, app_state: &AppState,
identifier: String, identifier: String,
) -> Result<PasswordResetToken, Error> { ) -> Result<PasswordResetToken, Error> {
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?;
let password_reset_token = serde_json::from_str( let password_reset_token = serde_json::from_str(
&data &app_state
.get_cache_key(format!("{user_uuid}_password_reset")) .get_cache_key(format!("{user_uuid}_password_reset"))
.await?, .await?,
)?; )?;
@ -53,14 +54,14 @@ impl PasswordResetToken {
} }
#[allow(clippy::new_ret_no_self)] #[allow(clippy::new_ret_no_self)]
pub async fn new(data: &Data, identifier: String) -> Result<(), Error> { pub async fn new(app_state: &AppState, identifier: String) -> Result<(), Error> {
let token = generate_token::<32>()?; let token = generate_token::<32>()?;
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?;
global_checks(data, user_uuid).await?; global_checks(app_state, user_uuid).await?;
use users::dsl as udsl; use users::dsl as udsl;
let (username, email_address): (String, String) = udsl::users let (username, email_address): (String, String) = udsl::users
@ -75,34 +76,37 @@ impl PasswordResetToken {
created_at: Utc::now(), created_at: Utc::now(),
}; };
data.set_cache_key( app_state
.set_cache_key(
format!("{user_uuid}_password_reset"), format!("{user_uuid}_password_reset"),
password_reset_token, password_reset_token,
86400, 86400,
) )
.await?; .await?;
data.set_cache_key(token.clone(), user_uuid, 86400).await?; app_state
.set_cache_key(token.clone(), user_uuid, 86400)
.await?;
let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; let mut reset_endpoint = app_state.config.web.frontend_url.join("reset-password")?;
reset_endpoint.set_query(Some(&format!("token={token}"))); reset_endpoint.set_query(Some(&format!("token={token}")));
let email = data let email = app_state
.mail_client .mail_client
.message_builder() .message_builder()
.to(email_address.parse()?) .to(email_address.parse()?)
.subject(format!("{} Password Reset", data.config.instance.name)) .subject(format!("{} Password Reset", app_state.config.instance.name))
.multipart(MultiPart::alternative_plain_html( .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.", data.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.", app_state.config.instance.name, username, reset_endpoint),
format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root {{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark) {{:root {{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px) {{.container {{width: 100%;}}}}body {{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header {{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button {{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover {{background-color: var(--secondary-colour);}}.content {{padding: 20px 30px;}}.footer {{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>{} Password Reset</h1></div><div class="content"><h2>Hello, {}!</h2><p>Someone requested a password reset for your Gorb account.</p><p>Click the button below within 24 hours to reset your password.</p><a href="{}" class="verify-button">RESET PASSWORD</a><p>If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.</p><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, data.config.instance.name, username, reset_endpoint) format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root {{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark) {{:root {{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px) {{.container {{width: 100%;}}}}body {{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header {{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button {{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover {{background-color: var(--secondary-colour);}}.content {{padding: 20px 30px;}}.footer {{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>{} Password Reset</h1></div><div class="content"><h2>Hello, {}!</h2><p>Someone requested a password reset for your Gorb account.</p><p>Click the button below within 24 hours to reset your password.</p><a href="{}" class="verify-button">RESET PASSWORD</a><p>If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.</p><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, app_state.config.instance.name, username, reset_endpoint)
))?; ))?;
data.mail_client.send_mail(email).await?; app_state.mail_client.send_mail(email).await?;
Ok(()) Ok(())
} }
pub async fn set_password(&self, data: &Data, password: String) -> Result<(), Error> { pub async fn set_password(&self, app_state: &AppState, password: String) -> Result<(), Error> {
if !PASSWORD_REGEX.is_match(&password) { if !PASSWORD_REGEX.is_match(&password) {
return Err(Error::BadRequest( return Err(Error::BadRequest(
"Please provide a valid password".to_string(), "Please provide a valid password".to_string(),
@ -111,12 +115,12 @@ impl PasswordResetToken {
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let hashed_password = data let hashed_password = app_state
.argon2 .argon2
.hash_password(password.as_bytes(), &salt) .hash_password(password.as_bytes(), &salt)
.map_err(|e| Error::PasswordHashError(e.to_string()))?; .map_err(|e| Error::PasswordHashError(e.to_string()))?;
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
use users::dsl; use users::dsl;
update(users::table) update(users::table)
@ -131,27 +135,28 @@ impl PasswordResetToken {
.get_result(&mut conn) .get_result(&mut conn)
.await?; .await?;
let login_page = data.config.web.frontend_url.join("login")?; let login_page = app_state.config.web.frontend_url.join("login")?;
let email = data let email = app_state
.mail_client .mail_client
.message_builder() .message_builder()
.to(email_address.parse()?) .to(email_address.parse()?)
.subject(format!("Your {} Password has been Reset", data.config.instance.name)) .subject(format!("Your {} Password has been Reset", app_state.config.instance.name))
.multipart(MultiPart::alternative_plain_html( .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 <strong>immediately</strong>.\n\n{}\n\nThanks, The gorb team.", data.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 <strong>immediately</strong>.\n\n{}\n\nThanks, The gorb team.", app_state.config.instance.name, username, login_page),
format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root {{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark) {{:root {{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px) {{.container {{width: 100%;}}}}body {{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header {{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button {{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover {{background-color: var(--secondary-colour);}}.content {{padding: 20px 30px;}}.footer {{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>{} Password Reset Confirmation</h1></div><div class="content"><h2>Hello, {}!</h2><p>Your password has been successfully reset for your Gorb account.</p><p>If you did not initiate this change, please click the button below to reset your password <strong>immediately</strong>.</p><a href="{}" class="verify-button">RESET PASSWORD</a><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, data.config.instance.name, username, login_page) format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root {{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark) {{:root {{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px) {{.container {{width: 100%;}}}}body {{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header {{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button {{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover {{background-color: var(--secondary-colour);}}.content {{padding: 20px 30px;}}.footer {{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>{} Password Reset Confirmation</h1></div><div class="content"><h2>Hello, {}!</h2><p>Your password has been successfully reset for your Gorb account.</p><p>If you did not initiate this change, please click the button below to reset your password <strong>immediately</strong>.</p><a href="{}" class="verify-button">RESET PASSWORD</a><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, app_state.config.instance.name, username, login_page)
))?; ))?;
data.mail_client.send_mail(email).await?; app_state.mail_client.send_mail(email).await?;
self.delete(data).await self.delete(app_state).await
} }
pub async fn delete(&self, data: &Data) -> Result<(), Error> { pub async fn delete(&self, app_state: &AppState) -> Result<(), Error> {
data.del_cache_key(format!("{}_password_reset", &self.user_uuid)) app_state
.del_cache_key(format!("{}_password_reset", &self.user_uuid))
.await?; .await?;
data.del_cache_key(self.token.to_string()).await?; app_state.del_cache_key(self.token.to_string()).await?;
Ok(()) Ok(())
} }

View file

@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Conn, Data, AppState, Conn,
error::Error, error::Error,
schema::{role_members, roles}, schema::{role_members, roles},
utils::order_by_is_above, utils::order_by_is_above,
@ -74,12 +74,18 @@ impl Role {
Ok(roles) Ok(roles)
} }
pub async fn fetch_from_member(data: &Data, member_uuid: Uuid) -> Result<Vec<Self>, Error> { pub async fn fetch_from_member(
if let Ok(roles) = data.get_cache_key(format!("{member_uuid}_roles")).await { app_state: &AppState,
member_uuid: Uuid,
) -> Result<Vec<Self>, Error> {
if let Ok(roles) = app_state
.get_cache_key(format!("{member_uuid}_roles"))
.await
{
return Ok(serde_json::from_str(&roles)?); return Ok(serde_json::from_str(&roles)?);
} }
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
use role_members::dsl; use role_members::dsl;
let role_memberships: Vec<RoleMember> = load_or_empty( let role_memberships: Vec<RoleMember> = load_or_empty(
@ -96,7 +102,8 @@ impl Role {
roles.push(membership.fetch_role(&mut conn).await?); roles.push(membership.fetch_role(&mut conn).await?);
} }
data.set_cache_key(format!("{member_uuid}_roles"), roles.clone(), 300) app_state
.set_cache_key(format!("{member_uuid}_roles"), roles.clone(), 300)
.await?; .await?;
Ok(roles) Ok(roles)

View file

@ -4,7 +4,7 @@ use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{Conn, Data, error::Error, objects::Me, schema::users}; use crate::{AppState, Conn, error::Error, objects::Me, schema::users};
use super::load_or_empty; use super::load_or_empty;
@ -46,10 +46,10 @@ pub struct User {
} }
impl User { impl User {
pub async fn fetch_one(data: &Data, user_uuid: Uuid) -> Result<Self, Error> { pub async fn fetch_one(app_state: &AppState, user_uuid: Uuid) -> Result<Self, Error> {
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
if let Ok(cache_hit) = data.get_cache_key(user_uuid.to_string()).await { if let Ok(cache_hit) = app_state.get_cache_key(user_uuid.to_string()).await {
return Ok(serde_json::from_str(&cache_hit)?); return Ok(serde_json::from_str(&cache_hit)?);
} }
@ -62,20 +62,21 @@ impl User {
let user = user_builder.build(); let user = user_builder.build();
data.set_cache_key(user_uuid.to_string(), user.clone(), 1800) app_state
.set_cache_key(user_uuid.to_string(), user.clone(), 1800)
.await?; .await?;
Ok(user) Ok(user)
} }
pub async fn fetch_one_with_friendship( pub async fn fetch_one_with_friendship(
data: &Data, app_state: &AppState,
me: &Me, me: &Me,
user_uuid: Uuid, user_uuid: Uuid,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
let mut user = Self::fetch_one(data, user_uuid).await?; let mut user = Self::fetch_one(app_state, user_uuid).await?;
if let Some(friend) = me.friends_with(&mut conn, user_uuid).await? { if let Some(friend) = me.friends_with(&mut conn, user_uuid).await? {
user.friends_since = Some(friend.accepted_at); user.friends_since = Some(friend.accepted_at);

26
src/socket.rs Normal file
View file

@ -0,0 +1,26 @@
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<Arc<AppState>>, socket: SocketRef, Data(data): Data<Value>) {
socket.emit("auth", &data).ok();
socket.on("message", async |socket: SocketRef, Data::<Value>(data)| {
info!("{}", data);
socket.emit("message-back", &data).ok();
});
socket.on(
"message-with-ack",
async |Data::<Value>(data), ack: AckSender| {
info!("{}", data);
ack.send(&data).ok();
},
);
}

View file

@ -1,10 +1,7 @@
use std::sync::LazyLock; use std::sync::LazyLock;
use actix_web::{ use axum::body::Bytes;
cookie::{Cookie, SameSite, time::Duration}, use axum_extra::extract::cookie::{Cookie, SameSite};
http::header::HeaderMap,
web::BytesMut,
};
use bindet::FileType; use bindet::FileType;
use diesel::{ExpressionMethods, QueryDsl}; use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
@ -13,10 +10,11 @@ use hex::encode;
use redis::RedisError; use redis::RedisError;
use regex::Regex; use regex::Regex;
use serde::Serialize; use serde::Serialize;
use time::Duration;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Conn, Data, AppState, Conn,
config::Config, config::Config,
error::Error, error::Error,
objects::{HasIsAbove, HasUuid}, objects::{HasIsAbove, HasUuid},
@ -33,86 +31,26 @@ pub static USERNAME_REGEX: LazyLock<Regex> =
pub static CHANNEL_REGEX: LazyLock<Regex> = pub static CHANNEL_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap());
// Password is expected to be hashed using SHA3-384
pub static PASSWORD_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); pub static PASSWORD_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap());
pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, Error> { pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cookie {
let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); Cookie::build(("refresh_token", refresh_token))
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) .http_only(true)
.secure(true) .secure(true)
.same_site(SameSite::None) .same_site(SameSite::None)
//.domain(config.web.backend_url.domain().unwrap().to_string())
.path(config.web.backend_url.path().to_string()) .path(config.web.backend_url.path().to_string())
.max_age(Duration::days(30)) .max_age(Duration::days(30))
.finish() .build()
}
pub fn new_access_token_cookie(config: &Config, access_token: String) -> Cookie {
Cookie::build(("access_token", access_token))
.http_only(false)
.secure(true)
.same_site(SameSite::None)
.path(config.web.backend_url.path().to_string())
.max_age(Duration::hours(1))
.build()
} }
pub fn generate_token<const N: usize>() -> Result<String, getrandom::Error> { pub fn generate_token<const N: usize>() -> Result<String, getrandom::Error> {
@ -121,7 +59,7 @@ pub fn generate_token<const N: usize>() -> Result<String, getrandom::Error> {
Ok(encode(buf)) Ok(encode(buf))
} }
pub fn image_check(icon: BytesMut) -> Result<String, Error> { pub fn image_check(icon: Bytes) -> Result<String, Error> {
let buf = std::io::Cursor::new(icon); let buf = std::io::Cursor::new(icon);
let detect = bindet::detect(buf).map_err(|e| e.kind()); let detect = bindet::detect(buf).map_err(|e| e.kind());
@ -168,10 +106,7 @@ pub async fn user_uuid_from_identifier(
} }
} }
pub async fn user_uuid_from_username( pub async fn user_uuid_from_username(conn: &mut Conn, username: &String) -> Result<Uuid, Error> {
conn: &mut Conn,
username: &String,
) -> Result<Uuid, Error> {
if USERNAME_REGEX.is_match(username) { if USERNAME_REGEX.is_match(username) {
use users::dsl; use users::dsl;
let user_uuid = dsl::users let user_uuid = dsl::users
@ -188,9 +123,9 @@ pub async fn user_uuid_from_username(
} }
} }
pub async fn global_checks(data: &Data, user_uuid: Uuid) -> Result<(), Error> { pub async fn global_checks(app_state: &AppState, user_uuid: Uuid) -> Result<(), Error> {
if data.config.instance.require_email_verification { if app_state.config.instance.require_email_verification {
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
use users::dsl; use users::dsl;
let email_verified: bool = dsl::users let email_verified: bool = dsl::users
@ -234,7 +169,7 @@ where
Ok(ordered) Ok(ordered)
} }
impl Data { impl AppState {
pub async fn set_cache_key( pub async fn set_cache_key(
&self, &self,
key: String, key: String,