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

@ -1,16 +1,21 @@
//! `/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_async::RunQueryDsl;
use serde::Serialize;
use crate::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
schema::refresh_tokens::{self, dsl},
utils::get_auth_header,
};
#[derive(Serialize, Selectable, Queryable)]
@ -18,7 +23,7 @@ use crate::{
#[diesel(check_for_backend(diesel::pg::Pg))]
struct Device {
device_name: String,
created_at: i64
created_at: i64,
}
/// `GET /api/v1/auth/devices` Returns list of logged in devices
@ -35,18 +40,13 @@ struct Device {
///
/// ]);
/// ```
#[get("/devices")]
pub async fn get(
req: HttpRequest,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
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 mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let uuid = check_access_token(auth.token(), &mut conn).await?;
let devices: Vec<Device> = dsl::refresh_tokens
.filter(dsl::uuid.eq(uuid))
@ -54,5 +54,5 @@ pub async fn get(
.get_results(&mut conn)
.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 axum::{
Json,
extract::State,
http::{HeaderValue, StatusCode},
response::IntoResponse,
};
use diesel::{ExpressionMethods, QueryDsl, dsl::insert_into};
use diesel_async::RunQueryDsl;
use serde::Deserialize;
use crate::{
Data,
AppState,
error::Error,
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)]
struct LoginInformation {
pub struct LoginInformation {
username: String,
password: String,
device_name: String,
}
#[post("/login")]
pub async fn response(
login_information: web::Json<LoginInformation>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
State(app_state): State<Arc<AppState>>,
Json(login_information): Json<LoginInformation>,
) -> Result<impl IntoResponse, Error> {
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;
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?;
@ -46,7 +54,7 @@ pub async fn response(
let parsed_hash = PasswordHash::new(&database_password)
.map_err(|e| Error::PasswordHashError(e.to_string()))?;
if data
if app_state
.argon2
.verify_password(login_information.password.as_bytes(), &parsed_hash)
.is_err()
@ -85,7 +93,21 @@ pub async fn response(
.execute(&mut conn)
.await?;
Ok(HttpResponse::Ok()
.cookie(new_refresh_token_cookie(&data.config, refresh_token))
.json(Response { access_token }))
let mut response = StatusCode::OK.into_response();
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_async::RunQueryDsl;
use crate::{
Data,
AppState,
error::Error,
schema::refresh_tokens::{self, dsl},
};
@ -20,28 +27,49 @@ use crate::{
///
/// 401 Unauthorized (no refresh token found)
///
#[get("/logout")]
pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized(
"request has no refresh token".to_string(),
))?;
pub async fn res(
State(app_state): State<Arc<AppState>>,
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(),
))?
.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)
.filter(dsl::token.eq(refresh_token))
.execute(&mut conn)
.await?;
refresh_token_cookie.make_removal();
let mut response;
if deleted == 0 {
return Ok(HttpResponse::NotFound()
.cookie(refresh_token_cookie)
.finish());
response = StatusCode::NOT_FOUND.into_response();
} else {
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_async::RunQueryDsl;
use serde::Serialize;
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 login;
@ -17,23 +22,18 @@ mod reset_password;
mod revoke;
mod verify_email;
#[derive(Serialize)]
struct Response {
access_token: String,
}
pub fn web() -> Scope {
web::scope("/auth")
.service(register::res)
.service(login::response)
.service(logout::res)
.service(refresh::res)
.service(revoke::res)
.service(verify_email::get)
.service(verify_email::post)
.service(reset_password::get)
.service(reset_password::post)
.service(devices::get)
pub fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/register", post(register::post))
.route("/login", post(login::response))
.route("/logout", delete(logout::res))
.route("/refresh", post(refresh::post))
.route("/revoke", post(revoke::post))
.route("/verify-email", get(verify_email::get))
.route("/verify-email", post(verify_email::post))
.route("/reset-password", get(reset_password::get))
.route("/reset-password", post(reset_password::post))
.route("/devices", get(devices::get))
}
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_async::RunQueryDsl;
use log::error;
use std::time::{SystemTime, UNIX_EPOCH};
use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use crate::{
Data,
AppState,
error::Error,
schema::{
access_tokens::{self, dsl},
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>>,
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(),
))?
.to_owned();
#[post("/refresh")]
pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized(
"request has no refresh token".to_string(),
))?;
let access_token_cookie = jar.get("access_token");
let mut refresh_token = String::from(refresh_token_cookie.value());
let refresh_token = String::from(refresh_token_cookie.value_trimmed());
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
.filter(rdsl::token.eq(&refresh_token))
@ -45,15 +58,29 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
error!("{error}");
}
refresh_token_cookie.make_removal();
let mut response = StatusCode::UNAUTHORIZED.into_response();
return Ok(HttpResponse::Unauthorized()
.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())?);
}
return Ok(response);
}
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64;
let mut response = StatusCode::OK.into_response();
if lifetime > 1987200 {
let new_refresh_token = generate_token::<32>()?;
@ -67,7 +94,13 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
.await
{
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) => {
error!("{error}");
@ -86,14 +119,40 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
.execute(&mut conn)
.await?;
return Ok(HttpResponse::Ok()
.cookie(new_refresh_token_cookie(&data.config, refresh_token))
.json(Response { access_token }));
if response.headers().get("Set-Cookie").is_some() {
response.headers_mut().append(
"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(),
)?,
);
}
return Ok(response);
}
refresh_token_cookie.make_removal();
let mut response = StatusCode::UNAUTHORIZED.into_response();
Ok(HttpResponse::Unauthorized()
.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::{
PasswordHasher,
password_hash::{SaltString, rand_core::OsRng},
};
use axum::{
Json,
extract::State,
http::{HeaderValue, StatusCode},
response::IntoResponse,
};
use diesel::{ExpressionMethods, dsl::insert_into};
use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use super::Response;
use crate::{
Data,
AppState,
error::Error,
objects::Member,
schema::{
@ -21,12 +28,13 @@ use crate::{
users::{self, dsl as udsl},
},
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)]
struct AccountInformation {
pub struct AccountInformation {
identifier: String,
email: String,
password: String,
@ -34,17 +42,13 @@ struct AccountInformation {
}
#[derive(Serialize)]
struct ResponseError {
pub struct ResponseError {
signups_enabled: bool,
gorb_id_valid: bool,
gorb_id_available: bool,
email_valid: bool,
email_available: bool,
password_hashed: bool,
password_minimum_length: bool,
password_special_characters: bool,
password_letters: bool,
password_numbers: bool,
password_strength: bool,
}
impl Default for ResponseError {
@ -55,21 +59,16 @@ impl Default for ResponseError {
gorb_id_available: true,
email_valid: true,
email_available: true,
password_hashed: true,
password_minimum_length: true,
password_special_characters: true,
password_letters: true,
password_numbers: true,
password_strength: true,
}
}
}
#[post("/register")]
pub async fn res(
account_information: web::Json<AccountInformation>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
if !data.config.instance.registration {
pub async fn post(
State(app_state): State<Arc<AppState>>,
Json(account_information): Json<AccountInformation>,
) -> Result<impl IntoResponse, Error> {
if !app_state.config.instance.registration {
return Err(Error::Forbidden(
"registration is disabled on this instance".to_string(),
));
@ -78,36 +77,48 @@ pub async fn res(
let uuid = Uuid::now_v7();
if !EMAIL_REGEX.is_match(&account_information.email) {
return Ok(HttpResponse::Forbidden().json(ResponseError {
email_valid: false,
..Default::default()
}));
return Ok((
StatusCode::FORBIDDEN,
Json(ResponseError {
email_valid: false,
..Default::default()
}),
)
.into_response());
}
if !USERNAME_REGEX.is_match(&account_information.identifier)
|| account_information.identifier.len() < 3
|| account_information.identifier.len() > 32
{
return Ok(HttpResponse::Forbidden().json(ResponseError {
gorb_id_valid: false,
..Default::default()
}));
return Ok((
StatusCode::FORBIDDEN,
Json(ResponseError {
gorb_id_valid: false,
..Default::default()
}),
)
.into_response());
}
if !PASSWORD_REGEX.is_match(&account_information.password) {
return Ok(HttpResponse::Forbidden().json(ResponseError {
password_hashed: false,
..Default::default()
}));
return Ok((
StatusCode::FORBIDDEN,
Json(ResponseError {
password_strength: false,
..Default::default()
}),
)
.into_response());
}
let salt = SaltString::generate(&mut OsRng);
if let Ok(hashed_password) = data
if let Ok(hashed_password) = app_state
.argon2
.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
insert_into(users::table)
@ -145,14 +156,27 @@ pub async fn res(
.execute(&mut conn)
.await?;
if let Some(initial_guild) = data.config.instance.initial_guild {
Member::new(&data, uuid, initial_guild).await?;
if let Some(initial_guild) = app_state.config.instance.initial_guild {
Member::new(&app_state, uuid, initial_guild).await?;
}
return Ok(HttpResponse::Ok()
.cookie(new_refresh_token_cookie(&data.config, refresh_token))
.json(Response { access_token }));
let mut response = StatusCode::OK.into_response();
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
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 serde::Deserialize;
use crate::{Data, error::Error, objects::PasswordResetToken};
use crate::{AppState, error::Error, objects::PasswordResetToken};
#[derive(Deserialize)]
struct Query {
pub struct QueryParams {
identifier: String,
}
@ -20,17 +27,22 @@ struct Query {
///
/// ### Responses
/// 200 Email sent
///
/// 429 Too Many Requests
///
/// 404 Not found
///
/// 400 Bad request
///
#[get("/reset-password")]
pub async fn get(query: web::Query<Query>, data: web::Data<Data>) -> Result<HttpResponse, Error> {
pub async fn get(
State(app_state): State<Arc<AppState>>,
query: Query<QueryParams>,
) -> Result<impl IntoResponse, Error> {
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) {
password_reset_token.delete(&data).await?;
password_reset_token.delete(&app_state).await?;
} else {
return Err(Error::TooManyRequests(
"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)]
struct ResetPassword {
pub struct ResetPassword {
password: String,
token: String,
}
@ -63,20 +75,23 @@ struct ResetPassword {
///
/// ### Responses
/// 200 Success
///
/// 410 Token Expired
///
/// 404 Not Found
///
/// 400 Bad Request
///
#[post("/reset-password")]
pub async fn post(
reset_password: web::Json<ResetPassword>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let password_reset_token = PasswordResetToken::get(&data, reset_password.token.clone()).await?;
State(app_state): State<Arc<AppState>>,
reset_password: Json<ResetPassword>,
) -> Result<impl IntoResponse, Error> {
let password_reset_token =
PasswordResetToken::get(&app_state, reset_password.token.clone()).await?;
password_reset_token
.set_password(&data, reset_password.password.clone())
.set_password(&app_state, reset_password.password.clone())
.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 axum::{Json, extract::State, http::StatusCode, response::IntoResponse};
use axum_extra::{
TypedHeader,
headers::authorization::{Authorization, Bearer},
};
use diesel::{ExpressionMethods, QueryDsl, delete};
use diesel_async::RunQueryDsl;
use serde::Deserialize;
use crate::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
schema::refresh_tokens::{self, dsl as rdsl},
schema::users::dsl as udsl,
utils::get_auth_header,
};
#[derive(Deserialize)]
struct RevokeRequest {
pub struct RevokeRequest {
password: String,
device_name: String,
}
// TODO: Should maybe be a delete request?
#[post("/revoke")]
pub async fn res(
req: HttpRequest,
revoke_request: web::Json<RevokeRequest>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
#[axum::debug_handler]
pub async fn post(
State(app_state): State<Arc<AppState>>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
Json(revoke_request): Json<RevokeRequest>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let uuid = check_access_token(auth.token(), &mut conn).await?;
let database_password: String = udsl::users
.filter(udsl::uuid.eq(uuid))
@ -43,7 +44,7 @@ pub async fn res(
let hashed_password = PasswordHash::new(&database_password)
.map_err(|e| Error::PasswordHashError(e.to_string()))?;
if data
if app_state
.argon2
.verify_password(revoke_request.password.as_bytes(), &hashed_password)
.is_err()
@ -59,5 +60,5 @@ pub async fn res(
.execute(&mut conn)
.await?;
Ok(HttpResponse::Ok().finish())
Ok(StatusCode::OK)
}

View file

@ -1,19 +1,28 @@
//! `/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 serde::Deserialize;
use crate::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
objects::{EmailToken, Me},
utils::get_auth_header,
};
#[derive(Deserialize)]
struct Query {
pub struct QueryParams {
token: String,
}
@ -35,37 +44,32 @@ struct Query {
///
/// 401 Unauthorized
///
#[get("/verify-email")]
pub async fn get(
req: HttpRequest,
query: web::Query<Query>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Query(query): Query<QueryParams>,
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 mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let uuid = check_access_token(auth.token(), &mut conn).await?;
let me = Me::get(&mut conn, uuid).await?;
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 {
return Ok(HttpResponse::Unauthorized().finish());
return Ok(StatusCode::UNAUTHORIZED);
}
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
@ -81,25 +85,23 @@ pub async fn get(
///
/// 401 Unauthorized
///
#[post("/verify-email")]
pub async fn post(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let headers = req.headers();
pub async fn post(
State(app_state): State<Arc<AppState>>,
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 mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let uuid = check_access_token(auth.token(), &mut conn).await?;
let me = Me::get(&mut conn, uuid).await?;
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) {
email_token.delete(&data).await?;
email_token.delete(&app_state).await?;
} else {
return Err(Error::TooManyRequests(
"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;
pub fn web() -> Scope {
web::scope("/channels")
.service(uuid::get)
.service(uuid::delete)
.service(uuid::patch)
.service(uuid::messages::get)
.service(uuid::socket::ws)
pub fn router() -> Router<Arc<AppState>> {
//let (layer, io) = SocketIo::new_layer();
//io.ns("/{uuid}/socket", uuid::socket::ws);
Router::new()
.route("/{uuid}", get(uuid::get))
.route("/{uuid}", delete(uuid::delete))
.route("/{uuid}", patch(uuid::patch))
.route("/{uuid}/messages", get(uuid::messages::get))
//.layer(layer)
}

View file

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

View file

@ -1,77 +1,74 @@
//! `/api/v1/channels/{uuid}` Channel specific endpoints
pub mod messages;
pub mod socket;
//pub mod socket;
use std::sync::Arc;
use crate::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
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 uuid::Uuid;
#[get("/{uuid}")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path(channel_uuid): Path<Uuid>,
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 channel_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 channel = Channel::fetch_one(&data, channel_uuid).await?;
let channel = Channel::fetch_one(&app_state, channel_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(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path(channel_uuid): Path<Uuid>,
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 channel_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 channel = Channel::fetch_one(&data, channel_uuid).await?;
let channel = Channel::fetch_one(&app_state, channel_uuid).await?;
let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
member
.check_permission(&data, Permissions::ManageChannel)
.check_permission(&app_state, Permissions::ManageChannel)
.await?;
channel.delete(&data).await?;
channel.delete(&app_state).await?;
Ok(HttpResponse::Ok().finish())
Ok(StatusCode::OK)
}
#[derive(Deserialize)]
struct NewInfo {
pub struct NewInfo {
name: Option<String>,
description: 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
#[patch("/{uuid}")]
pub async fn patch(
req: HttpRequest,
path: web::Path<(Uuid,)>,
new_info: web::Json<NewInfo>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path(channel_uuid): Path<Uuid>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
Json(new_info): Json<NewInfo>,
) -> 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 channel_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 mut channel = Channel::fetch_one(&data, channel_uuid).await?;
let mut channel = Channel::fetch_one(&app_state, channel_uuid).await?;
let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
member
.check_permission(&data, Permissions::ManageChannel)
.check_permission(&app_state, Permissions::ManageChannel)
.await?;
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 {
channel
.set_description(&data, new_description.to_string())
.set_description(&app_state, new_description.to_string())
.await?;
}
if let Some(new_is_above) = &new_info.is_above {
channel
.set_description(&data, new_is_above.to_string())
.set_description(&app_state, new_is_above.to_string())
.await?;
}
Ok(HttpResponse::Ok().json(channel))
Ok((StatusCode::OK, Json(channel)))
}

View file

@ -1,28 +1,40 @@
//! `/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;
mod uuid;
use crate::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
objects::{Guild, StartAmountQuery},
utils::{get_auth_header, global_checks},
utils::global_checks,
};
#[derive(Deserialize)]
struct GuildInfo {
pub struct GuildInfo {
name: String,
}
pub fn web() -> Scope {
web::scope("/guilds")
.service(post)
.service(get)
.service(uuid::web())
pub fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/", post(new))
.route("/", get(get_guilds))
.nest("/{uuid}", uuid::router())
}
/// `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
#[post("")]
pub async fn post(
req: HttpRequest,
guild_info: web::Json<GuildInfo>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
pub async fn new(
State(app_state): State<Arc<AppState>>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
Json(guild_info): Json<GuildInfo>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let uuid = check_access_token(auth.token(), &mut conn).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
@ -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
#[get("")]
pub async fn get(
req: HttpRequest,
request_query: web::Query<StartAmountQuery>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
pub async fn get_guilds(
State(app_state): State<Arc<AppState>>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
Json(request_query): Json<StartAmountQuery>,
) -> Result<impl IntoResponse, Error> {
let start = request_query.start.unwrap_or(0);
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::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
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)]
struct ChannelInfo {
pub struct ChannelInfo {
name: String,
description: Option<String>,
}
#[get("{uuid}/channels")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path(guild_uuid): Path<Uuid>,
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 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?;
global_checks(&app_state, uuid).await?;
Member::check_membership(&mut conn, uuid, guild_uuid).await?;
if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_channels")).await {
return Ok(HttpResponse::Ok()
.content_type("application/json")
.body(cache_hit));
if let Ok(cache_hit) = app_state
.get_cache_key(format!("{guild_uuid}_channels"))
.await
{
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?;
data.set_cache_key(
format!("{guild_uuid}_channels"),
channels_ordered.clone(),
1800,
)
.await?;
app_state
.set_cache_key(
format!("{guild_uuid}_channels"),
channels_ordered.clone(),
1800,
)
.await?;
Ok(HttpResponse::Ok().json(channels_ordered))
Ok((StatusCode::OK, Json(channels_ordered)).into_response())
}
#[post("{uuid}/channels")]
pub async fn create(
req: HttpRequest,
channel_info: web::Json<ChannelInfo>,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path(guild_uuid): Path<Uuid>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
Json(channel_info): Json<ChannelInfo>,
) -> 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 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?;
global_checks(&app_state, uuid).await?;
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member
.check_permission(&data, Permissions::ManageChannel)
.check_permission(&app_state, Permissions::ManageChannel)
.await?;
let channel = Channel::new(
data.clone(),
&app_state,
guild_uuid,
channel_info.name.clone(),
channel_info.description.clone(),
)
.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 uuid::Uuid;
use crate::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
objects::{Guild, Member, Permissions},
utils::{get_auth_header, global_checks},
utils::global_checks,
};
#[derive(Deserialize)]
struct InviteRequest {
pub struct InviteRequest {
custom_id: Option<String>,
}
#[get("{uuid}/invites")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path(guild_uuid): Path<Uuid>,
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 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?;
global_checks(&app_state, 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?;
Ok(HttpResponse::Ok().json(invites))
Ok((StatusCode::OK, Json(invites)))
}
#[post("{uuid}/invites")]
pub async fn create(
req: HttpRequest,
path: web::Path<(Uuid,)>,
invite_request: web::Json<InviteRequest>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path(guild_uuid): Path<Uuid>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
Json(invite_request): Json<InviteRequest>,
) -> 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 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?;
global_checks(&app_state, uuid).await?;
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member
.check_permission(&data, Permissions::CreateInvite)
.check_permission(&app_state, Permissions::CreateInvite)
.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())
.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::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
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(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path(guild_uuid): Path<Uuid>,
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 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?;
global_checks(&app_state, uuid).await?;
Member::check_membership(&mut conn, uuid, guild_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
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;
mod channels;
mod icon;
mod invites;
mod members;
mod roles;
use crate::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
objects::{Guild, Member},
utils::{get_auth_header, global_checks},
objects::{Guild, Member, Permissions},
utils::global_checks,
};
pub fn web() -> Scope {
web::scope("")
pub fn router() -> Router<Arc<AppState>> {
Router::new()
// Servers
.service(get)
.route("/", get(get_guild))
.route("/", patch(edit))
// Channels
.service(channels::get)
.service(channels::create)
.route("/channels", get(channels::get))
.route("/channels", post(channels::create))
// Roles
.service(roles::get)
.service(roles::create)
.service(roles::uuid::get)
.route("/roles", get(roles::get))
.route("/roles", post(roles::create))
.route("/roles/{role_uuid}", get(roles::uuid::get))
// Invites
.service(invites::get)
.service(invites::create)
// Icon
.service(icon::upload)
.route("/invites", get(invites::get))
.route("/invites", post(invites::create))
// Members
.service(members::get)
.route("/members", get(members::get))
}
/// `GET /api/v1/guilds/{uuid}` DESCRIPTION
@ -70,27 +81,69 @@ pub fn web() -> Scope {
/// "member_count": 20
/// });
/// ```
#[get("/{uuid}")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
pub async fn get_guild(
State(app_state): State<Arc<AppState>>,
Path(guild_uuid): Path<Uuid>,
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 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?;
global_checks(&app_state, uuid).await?;
Member::check_membership(&mut conn, uuid, 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 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 crate::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
objects::{Member, Permissions, Role},
utils::{get_auth_header, global_checks, order_by_is_above},
utils::{global_checks, order_by_is_above},
};
pub mod uuid;
#[derive(Deserialize)]
struct RoleInfo {
pub struct RoleInfo {
name: String,
}
#[get("{uuid}/roles")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path(guild_uuid): Path<Uuid>,
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 guild_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let uuid = check_access_token(auth.token(), &mut conn).await?;
Member::check_membership(&mut conn, uuid, guild_uuid).await?;
if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_roles")).await {
return Ok(HttpResponse::Ok()
.content_type("application/json")
.body(cache_hit));
if let Ok(cache_hit) = app_state.get_cache_key(format!("{guild_uuid}_roles")).await {
return Ok((StatusCode::OK, Json(cache_hit)).into_response());
}
let roles = Role::fetch_all(&mut conn, guild_uuid).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?;
Ok(HttpResponse::Ok().json(roles_ordered))
Ok((StatusCode::OK, Json(roles_ordered)).into_response())
}
#[post("{uuid}/roles")]
pub async fn create(
req: HttpRequest,
role_info: web::Json<RoleInfo>,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path(guild_uuid): Path<Uuid>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
Json(role_info): Json<RoleInfo>,
) -> 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 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?;
global_checks(&app_state, uuid).await?;
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member
.check_permission(&data, Permissions::ManageRole)
.check_permission(&app_state, Permissions::ManageRole)
.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::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
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(
req: HttpRequest,
path: web::Path<(Uuid, Uuid)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path((guild_uuid, role_uuid)): Path<(Uuid, Uuid)>,
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 (guild_uuid, role_uuid) = path.into_inner();
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
global_checks(&app_state, uuid).await?;
Member::check_membership(&mut conn, uuid, guild_uuid).await?;
if let Ok(cache_hit) = data.get_cache_key(format!("{role_uuid}")).await {
return Ok(HttpResponse::Ok()
.content_type("application/json")
.body(cache_hit));
if let Ok(cache_hit) = app_state.get_cache_key(format!("{role_uuid}")).await {
return Ok((StatusCode::OK, Json(cache_hit)).into_response());
}
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?;
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::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
objects::{Guild, Invite, Member},
utils::{get_auth_header, global_checks},
utils::global_checks,
};
#[get("{id}")]
pub async fn get(path: web::Path<(String,)>, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let mut conn = data.pool.get().await?;
let invite_id = path.into_inner().0;
pub async fn get(
State(app_state): State<Arc<AppState>>,
Path(invite_id): Path<String>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
let invite = Invite::fetch_one(&mut conn, invite_id).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(
req: HttpRequest,
path: web::Path<(String,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path(invite_id): Path<String>,
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 invite_id = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
global_checks(&app_state, uuid).await?;
let invite = Invite::fetch_one(&mut conn, invite_id).await?;
let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?;
Member::new(&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;
pub fn web() -> Scope {
web::scope("/invites").service(id::get).service(id::join)
pub fn router() -> Router<Arc<AppState>> {
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;
pub mod uuid;
use crate::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
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
#[get("/friends")]
pub async fn get(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let headers = req.headers();
pub async fn get(
State(app_state): State<Arc<AppState>>,
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?;
global_checks(&data, uuid).await?;
global_checks(&app_state, 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)]
struct UserReq {
pub struct UserReq {
username: String,
}
@ -55,26 +59,21 @@ struct UserReq {
///
/// 400 Bad Request (usually means users are already friends)
///
#[post("/friends")]
pub async fn post(
req: HttpRequest,
json: web::Json<UserReq>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
Json(user_request): Json<UserReq>,
) -> 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?;
global_checks(&data, uuid).await?;
global_checks(&app_state, 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?;
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 crate::{
Data,
api::v1::auth::check_access_token,
error::Error,
objects::Me,
utils::{get_auth_header, global_checks},
AppState, api::v1::auth::check_access_token, error::Error, objects::Me, utils::global_checks,
};
#[delete("/friends/{uuid}")]
pub async fn delete(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path(friend_uuid): Path<Uuid>,
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?;
global_checks(&data, uuid).await?;
global_checks(&app_state, 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
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::{
Data,
api::v1::auth::check_access_token,
error::Error,
objects::Me,
utils::{get_auth_header, global_checks},
AppState, api::v1::auth::check_access_token, error::Error, objects::Me, utils::global_checks,
};
/// `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
#[get("/guilds")]
pub async fn get(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let headers = req.headers();
pub async fn get(
State(app_state): State<Arc<AppState>>,
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?;
global_checks(&data, uuid).await?;
global_checks(&app_state, uuid).await?;
let me = Me::get(&mut conn, uuid).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 actix_web::{HttpRequest, HttpResponse, Scope, get, patch, web};
use std::sync::Arc;
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 crate::{
Data,
api::v1::auth::check_access_token,
error::Error,
objects::Me,
utils::{get_auth_header, global_checks},
AppState, api::v1::auth::check_access_token, error::Error, objects::Me, utils::global_checks,
};
mod friends;
mod guilds;
pub fn web() -> Scope {
web::scope("/me")
.service(get)
.service(update)
.service(guilds::get)
.service(friends::get)
.service(friends::post)
.service(friends::uuid::delete)
pub fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/", get(get_me))
.route(
"/",
patch(update).layer(DefaultBodyLimit::max(
100 * 1024 * 1024, /* limit is in bytes */
)),
)
.route("/guilds", get(guilds::get))
.route("/friends", get(friends::get))
.route("/friends", post(friends::post))
.route("/friends/{uuid}", delete(friends::uuid::delete))
}
#[get("")]
pub async fn get(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> {
let headers = req.headers();
pub async fn get_me(
State(app_state): State<Arc<AppState>>,
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 mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let uuid = check_access_token(auth.token(), &mut conn).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 {
username: Option<String>,
display_name: Option<String>,
//password: Option<String>, will probably be handled through a reset password link
email: Option<String>,
pronouns: Option<String>,
about: Option<String>,
}
#[derive(Debug, MultipartForm)]
struct UploadForm {
#[multipart(limit = "100MB")]
avatar: Option<TempFile>,
json: MpJson<NewInfo>,
}
#[patch("")]
pub async fn update(
req: HttpRequest,
MultipartForm(form): MultipartForm<UploadForm>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
mut multipart: Multipart,
) -> 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 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() {
global_checks(&data, uuid).await?;
if name == "avatar" {
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?;
if let Some(avatar) = form.avatar {
let bytes = tokio::fs::read(avatar.file).await?;
let byte_slice: &[u8] = &bytes;
me.set_avatar(&data, data.config.bunny.cdn_url.clone(), byte_slice.into())
if let Some(avatar) = avatar {
me.set_avatar(&app_state, app_state.config.bunny.cdn_url.clone(), avatar)
.await?;
}
if let Some(username) = &form.json.username {
me.set_username(&data, username.clone()).await?;
if let Some(username) = &json.username {
me.set_username(&app_state, username.clone()).await?;
}
if let Some(display_name) = &form.json.display_name {
me.set_display_name(&data, display_name.clone()).await?;
if let Some(display_name) = &json.display_name {
me.set_display_name(&app_state, display_name.clone())
.await?;
}
if let Some(email) = &form.json.email {
me.set_email(&data, email.clone()).await?;
if let Some(email) = &json.email {
me.set_email(&app_state, email.clone()).await?;
}
if let Some(pronouns) = &form.json.pronouns {
me.set_pronouns(&data, pronouns.clone()).await?;
if let Some(pronouns) = &json.pronouns {
me.set_pronouns(&app_state, pronouns.clone()).await?;
}
if let Some(about) = &form.json.about {
me.set_about(&data, about.clone()).await?;
if let Some(about) = &json.about {
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
use actix_web::{Scope, web};
use std::sync::Arc;
use axum::{routing::get, Router};
use crate::AppState;
mod auth;
mod channels;
@ -10,13 +14,13 @@ mod me;
mod stats;
mod users;
pub fn web() -> Scope {
web::scope("/v1")
.service(stats::res)
.service(auth::web())
.service(users::web())
.service(channels::web())
.service(guilds::web())
.service(invites::web())
.service(me::web())
pub fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/stats", get(stats::res))
.nest("/auth", auth::router())
.nest("/users", users::router())
.nest("/channels", channels::router())
.nest("/guilds", guilds::router())
.nest("/invites", invites::router())
.nest("/me", me::router())
}

View file

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

View file

@ -1,19 +1,33 @@
//! `/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::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
objects::{StartAmountQuery, User},
utils::{get_auth_header, global_checks},
utils::global_checks,
};
mod uuid;
pub fn web() -> Scope {
web::scope("/users").service(get).service(uuid::get)
pub fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/", get(users))
.route("/{uuid}", get(uuid::get))
}
/// `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
#[get("")]
pub async fn get(
req: HttpRequest,
request_query: web::Query<StartAmountQuery>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
pub async fn users(
State(app_state): State<Arc<AppState>>,
Query(request_query): Query<StartAmountQuery>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<impl IntoResponse, Error> {
let start = request_query.start.unwrap_or(0);
let amount = request_query.amount.unwrap_or(10);
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?;
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
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 crate::{
Data,
AppState,
api::v1::auth::check_access_token,
error::Error,
objects::{Me, User},
utils::{get_auth_header, global_checks},
utils::global_checks,
};
/// `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
#[get("/{uuid}")]
pub async fn get(
req: HttpRequest,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
State(app_state): State<Arc<AppState>>,
Path(user_uuid): Path<Uuid>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
) -> Result<impl IntoResponse, Error> {
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)?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
global_checks(&app_state, 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)))
}