feat: add users endpoint and add me and uuid under it
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline failed

Adds a users endpoint that returns all users on the server, will require instance permissions in future.
Place previous user requests under users to avoid having multiple endpoints.
This commit is contained in:
Radical 2025-05-02 19:19:59 +02:00
parent cc07d78325
commit 1d7cdf343b
5 changed files with 149 additions and 18 deletions

View file

@ -23,7 +23,7 @@ simple_logger = "5.0.0"
sqlx = { version = "0.8", features = ["runtime-tokio", "tls-native-tls", "postgres"] }
toml = "0.8"
url = { version = "2.5", features = ["serde"] }
uuid = { version = "1.16", features = ["v7"] }
uuid = { version = "1.16", features = ["serde", "v7"] }
[dependencies.tokio]
version = "1.44"

View file

@ -2,11 +2,11 @@ use actix_web::{Scope, web};
mod auth;
mod stats;
mod user;
mod users;
pub fn web() -> Scope {
web::scope("/v1")
.service(stats::res)
.service(auth::web())
.service(user::res)
.service(users::web())
}

66
src/api/v1/users/me.rs Normal file
View file

@ -0,0 +1,66 @@
use actix_web::{Error, HttpResponse, error, post, web};
use futures::StreamExt;
use log::error;
use serde::{Deserialize, Serialize};
use crate::{Data, api::v1::auth::check_access_token};
#[derive(Deserialize)]
struct AuthenticationRequest {
access_token: String,
}
#[derive(Serialize)]
struct Response {
uuid: String,
username: String,
display_name: String,
}
const MAX_SIZE: usize = 262_144;
#[post("/me")]
pub async fn res(
mut payload: web::Payload,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let mut body = web::BytesMut::new();
while let Some(chunk) = payload.next().await {
let chunk = chunk?;
// limit max size of in-memory payload
if (body.len() + chunk.len()) > MAX_SIZE {
return Err(error::ErrorBadRequest("overflow"));
}
body.extend_from_slice(&chunk);
}
let authentication_request = serde_json::from_slice::<AuthenticationRequest>(&body)?;
let authorized = check_access_token(authentication_request.access_token, &data.pool).await;
if let Err(error) = authorized {
return Ok(error);
}
let uuid = authorized.unwrap();
let row = sqlx::query_as(&format!(
"SELECT username, display_name FROM users WHERE uuid = '{}'",
uuid
))
.fetch_one(&data.pool)
.await;
if let Err(error) = row {
error!("{}", error);
return Ok(HttpResponse::InternalServerError().finish());
}
let (username, display_name): (String, Option<String>) = row.unwrap();
Ok(HttpResponse::Ok().json(Response {
uuid: uuid.to_string(),
username,
display_name: display_name.unwrap_or_default(),
}))
}

77
src/api/v1/users/mod.rs Normal file
View file

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

View file

@ -20,10 +20,10 @@ struct Response {
const MAX_SIZE: usize = 262_144;
#[post("/user/{uuid}")]
#[post("/{uuid}")]
pub async fn res(
mut payload: web::Payload,
path: web::Path<(String,)>,
path: web::Path<(Uuid,)>,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let mut body = web::BytesMut::new();
@ -36,7 +36,7 @@ pub async fn res(
body.extend_from_slice(&chunk);
}
let request = path.into_inner().0;
let uuid = path.into_inner().0;
let authentication_request = serde_json::from_slice::<AuthenticationRequest>(&body)?;
@ -46,18 +46,6 @@ pub async fn res(
return Ok(error);
}
let mut uuid = authorized.unwrap();
if request != "me" {
let requested_uuid = Uuid::parse_str(&request);
if requested_uuid.is_err() {
return Ok(HttpResponse::BadRequest().json(r#"{ "error": "UUID is invalid!" }"#));
}
uuid = requested_uuid.unwrap()
}
let row = sqlx::query_as(&format!(
"SELECT username, display_name FROM users WHERE uuid = '{}'",
uuid