Files
modrinth/apps/labrinth/src/routes/v3/users.rs
T
4a6fa9fc3d feat: better api docs (#6586)
* feat: docs

* fix tombi

* chore: fix some of the routes rendering with missing /

* response schemas

* fix: restore labrinth docs routes

* Fix path parameter docs in routes

* remove utoipa-actix-web

* consistency

* improve version intros

* improve formatting, examples

* better hash examples, move openapi stuff to openapi.rs

* more utoipa param fixes

* request body docs

* chore: remove moderation route from v2, remove ingest & webhooks from v3 spec

* fixes

* chore: tweak sources titles

* fix

* fix test

* improve examples

* increase compiler spawned thread stack size

* remove unused tests & script

* test

* bro what

* fix

---------

Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com>
2026-07-04 14:19:21 +00:00

1249 lines
35 KiB
Rust

use std::{
cmp::Reverse,
collections::{HashMap, HashSet},
};
use super::{ApiError, oauth_clients::get_user_clients};
use crate::database::PgPool;
use crate::util::error::Context;
use crate::{
auth::{
check_is_moderator_from_headers, checks::is_visible_organization,
filter_visible_collections, filter_visible_projects,
get_user_from_headers,
},
database::{
models::{DBModerationNote, DBOrganization, DBProjectId, DBUser},
redis::RedisPool,
},
file_hosting::{FileHost, FileHostPublicity},
models::{
ids::OrganizationId,
notifications::Notification,
organizations::Organization,
pats::Scopes,
projects::Project,
users::{Badges, Role},
},
queue::session::AuthQueue,
util::{
img::delete_old_images, routes::read_limited_from_payload,
validate::validation_errors_to_string,
},
};
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web};
use ariadne::ids::UserId;
use serde::{Deserialize, Serialize};
use validator::Validate;
pub fn config(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(user_auth_get_route)
.service(users_get_route)
.service(users_search)
.service(admin_user_email)
.service(all_projects)
.service(projects_list_route)
.service(user_notes_edit)
.service(user_get_route)
.service(collections_list)
.service(orgs_list)
.service(user_edit_route)
.service(user_icon_edit_route)
.service(user_icon_delete_route)
.service(user_delete_route)
.service(user_follows_route)
.service(user_notifications_route)
.service(get_user_clients);
}
#[derive(Serialize)]
pub struct AllProjectsResponse {
pub projects: Vec<Project>,
pub organizations: HashMap<OrganizationId, Organization>,
}
#[derive(Deserialize)]
pub struct UserEmailQuery {
pub email: String,
}
#[utoipa::path(tag = "users", responses((status = OK)))]
#[get("/user/{user_id}/all-projects")]
pub async fn all_projects(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<AllProjectsResponse>, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_READ,
)
.await
.map(|x| x.1)
.ok();
let target_user = DBUser::get(&info.into_inner().0, &**pool, &redis)
.await?
.ok_or(ApiError::NotFound)?;
let user_project_ids =
DBUser::get_projects(target_user.id, &**pool, &redis).await?;
let organization_ids =
DBUser::get_organizations(target_user.id, &**pool).await?;
let organizations_data =
DBOrganization::get_many_ids(&organization_ids, &**pool, &redis)
.await?;
let team_ids = organizations_data
.iter()
.map(|organization| organization.team_id)
.collect::<Vec<_>>();
let teams_data =
crate::database::models::DBTeamMember::get_from_team_full_many(
&team_ids, &**pool, &redis,
)
.await?;
let users = DBUser::get_many_ids(
&teams_data
.iter()
.map(|member| member.user_id)
.collect::<Vec<_>>(),
&**pool,
&redis,
)
.await?;
let mut team_groups = HashMap::new();
for member in teams_data {
team_groups
.entry(member.team_id)
.or_insert(vec![])
.push(member);
}
let mut organizations = HashMap::new();
let mut visible_organization_ids = Vec::new();
for data in organizations_data {
if !is_visible_organization(&data, &user, &pool, &redis).await? {
continue;
}
visible_organization_ids.push(data.id);
let members_data = team_groups.remove(&data.team_id).unwrap_or(vec![]);
let logged_in = user
.as_ref()
.and_then(|user| {
members_data
.iter()
.find(|x| x.user_id == user.id.into() && x.accepted)
})
.is_some();
let team_members = members_data
.into_iter()
.filter(|x| logged_in || x.accepted || target_user.id == x.user_id)
.filter_map(|data| {
users.iter().find(|x| x.id == data.user_id).map(|member| {
crate::models::teams::TeamMember::from(
data,
member.clone(),
!logged_in,
)
})
})
.collect();
organizations.insert(
OrganizationId::from(data.id),
Organization::from(data, team_members),
);
}
let organization_id_values = visible_organization_ids
.iter()
.map(|id| id.0)
.collect::<Vec<_>>();
let organization_project_ids = sqlx::query!(
"
SELECT m.id
FROM mods m
WHERE m.organization_id = ANY($1)
",
&organization_id_values,
)
.fetch_all(&**pool)
.await?
.into_iter()
.map(|row| DBProjectId(row.id))
.collect::<Vec<_>>();
let project_ids = user_project_ids
.into_iter()
.chain(organization_project_ids)
.collect::<HashSet<_>>()
.into_iter()
.collect::<Vec<_>>();
let projects_data =
crate::database::DBProject::get_many_ids(&project_ids, &**pool, &redis)
.await?;
let projects =
filter_visible_projects(projects_data, &user, &pool, true).await?;
Ok(web::Json(AllProjectsResponse {
projects,
organizations,
}))
}
#[utoipa::path(
tag = "users",
params(("email" = String, Query)),
responses((status = OK))
)]
#[get("/user_email")]
pub async fn admin_user_email(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
email: web::Query<UserEmailQuery>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::SESSION_ACCESS,
)
.await
.map(|x| x.1)?;
if !user.role.is_admin() {
return Err(ApiError::CustomAuthentication(
"You do not have permission to get a user from their email!"
.to_string(),
));
}
let user_id = sqlx::query!(
"
SELECT id FROM users
WHERE LOWER(email) = LOWER($1)
",
email.email
)
.fetch_optional(&**pool)
.await?
.map(|x| x.id)
.ok_or_else(|| {
ApiError::InvalidInput(
"The email provided is not associated with a user!".to_string(),
)
})?;
let user = DBUser::get_id(
crate::database::models::DBUserId(user_id),
&**pool,
&redis,
)
.await?;
if let Some(user) = user {
Ok(HttpResponse::Ok().json(user))
} else {
Err(ApiError::NotFound)
}
}
#[utoipa::path(tag = "users", responses((status = OK)))]
#[get("/user/{user_id}/projects")]
pub async fn projects_list_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
projects_list(req, info, pool, redis, session_queue).await
}
pub async fn projects_list(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_READ,
)
.await
.map(|x| x.1)
.ok();
let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(id) = id_option.map(|x| x.id) {
let project_data = DBUser::get_projects(id, &**pool, &redis).await?;
let projects: Vec<_> = crate::database::DBProject::get_many_ids(
&project_data,
&**pool,
&redis,
)
.await?;
let projects =
filter_visible_projects(projects, &user, &pool, true).await?;
Ok(HttpResponse::Ok().json(projects))
} else {
Err(ApiError::NotFound)
}
}
#[utoipa::path(tag = "users", responses((status = OK)))]
#[get("/user")]
pub async fn user_auth_get_route(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
user_auth_get(req, pool, redis, session_queue).await
}
pub async fn user_auth_get(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let (scopes, mut user) = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::USER_READ,
)
.await?;
if !scopes.contains(Scopes::USER_READ_EMAIL) {
user.email = None;
}
if !scopes.contains(Scopes::PAYOUTS_READ) {
user.payout_data = None;
}
if user.role.is_mod() {
let note =
DBModerationNote::get_user(user.id.into(), &**pool, &redis).await?;
user.moderation_notes = Some(note.map(Into::into));
}
Ok(HttpResponse::Ok().json(user))
}
#[derive(Serialize, Deserialize)]
pub struct UserIds {
pub ids: String,
}
#[derive(Deserialize)]
pub struct UserSearchQuery {
pub query: String,
}
#[utoipa::path(
tag = "users",
params(("query" = String, Query)),
responses((status = OK))
)]
#[get("/users/search")]
pub async fn users_search(
web::Query(query): web::Query<UserSearchQuery>,
pool: web::Data<PgPool>,
) -> Result<web::Json<Vec<crate::models::users::SearchUser>>, ApiError> {
let query = query.query.trim();
let users = DBUser::search(query, &**pool)
.await
.wrap_internal_err("failed to search users")?
.into_iter()
.map(Into::into)
.collect();
Ok(web::Json(users))
}
#[utoipa::path(
tag = "users",
params(("ids" = String, Query)),
responses((status = OK))
)]
#[get("/users")]
pub async fn users_get_route(
req: HttpRequest,
ids: web::Query<UserIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
users_get(req, ids, pool, redis, session_queue).await
}
pub async fn users_get(
req: HttpRequest,
web::Query(ids): web::Query<UserIds>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user_ids = serde_json::from_str::<Vec<String>>(&ids.ids)?;
let users_data = DBUser::get_many(&user_ids, &**pool, &redis).await?;
let auth_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::SESSION_ACCESS,
)
.await
.map(|x| x.1)
.ok();
let notes = if auth_user.as_ref().is_some_and(|x| x.role.is_mod()) {
DBModerationNote::get_many_users(
&users_data.iter().map(|x| x.id).collect::<Vec<_>>(),
&**pool,
&redis,
)
.await?
} else {
HashMap::new()
};
let users: Vec<crate::models::users::User> = users_data
.into_iter()
.map(|data| {
let mut user = crate::models::users::User::from(data.clone());
if auth_user.as_ref().is_some_and(|x| x.role.is_mod()) {
user.moderation_notes =
Some(notes.get(&data.id).cloned().map(Into::into));
}
user
})
.collect();
Ok(HttpResponse::Ok().json(users))
}
#[utoipa::path(tag = "users", responses((status = OK)))]
#[get("/user/{id}")]
pub async fn user_get_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
user_get(req, info, pool, redis, session_queue).await
}
pub async fn user_get(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user_data = DBUser::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(data) = user_data {
let auth_user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::SESSION_ACCESS,
)
.await
.map(|x| x.1)
.ok();
let is_admin = auth_user.as_ref().is_some_and(|x| x.role.is_admin());
let is_mod = auth_user.as_ref().is_some_and(|x| x.role.is_mod());
let user_id = data.id;
let mut response: crate::models::users::User = if is_admin {
crate::models::users::User::from_full(data)
} else {
data.into()
};
if is_mod {
let note =
DBModerationNote::get_user(user_id, &**pool, &redis).await?;
response.moderation_notes = Some(note.map(Into::into));
}
Ok(HttpResponse::Ok().json(response))
} else {
Err(ApiError::NotFound)
}
}
#[utoipa::path(tag = "users", responses((status = NO_CONTENT)))]
#[patch("/user/{id}/notes")]
pub async fn user_notes_edit(
req: HttpRequest,
info: web::Path<(String,)>,
new_note: web::Json<crate::models::moderation_notes::PatchModerationNote>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = check_is_moderator_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::SESSION_ACCESS,
)
.await?;
new_note.validate_not_empty()?;
let expected_version =
crate::models::moderation_notes::parse_if_match_header(&req)?;
let user_data = DBUser::get(&info.into_inner().0, &**pool, &redis)
.await?
.ok_or(ApiError::NotFound)?;
let mut transaction = pool.begin().await?;
if let Some(expected) = expected_version {
let updated = DBModerationNote::update(
Some(user_data.id),
None,
user.id.into(),
expected,
new_note.notes.as_deref(),
new_note.user_rating,
&mut transaction,
)
.await?;
if updated.is_none() {
return Err(ApiError::PreconditionFailed(
"moderation note version does not match".to_string(),
));
}
} else {
let updated = DBModerationNote::insert(
Some(user_data.id),
None,
user.id.into(),
new_note.notes.as_deref(),
new_note.user_rating,
&mut transaction,
)
.await?;
if updated.is_none() {
return Err(ApiError::PreconditionRequired(
"moderation note version does not match".to_string(),
));
}
};
transaction.commit().await?;
DBModerationNote::clear_user_cache(user_data.id, &redis).await?;
Ok(HttpResponse::NoContent().finish())
}
#[utoipa::path(tag = "users", responses((status = OK)))]
#[get("/user/{user_id}/collections")]
pub async fn collections_list(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::COLLECTION_READ,
)
.await
.map(|x| x.1)
.ok();
let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(id) = id_option.map(|x| x.id) {
let collection_data = DBUser::get_collections(id, &**pool).await?;
let response: Vec<_> = crate::database::models::DBCollection::get_many(
&collection_data,
&**pool,
&redis,
)
.await?;
let collections =
filter_visible_collections(response, &user, true).await?;
Ok(HttpResponse::Ok().json(collections))
} else {
Err(ApiError::NotFound)
}
}
#[utoipa::path(tag = "users", responses((status = OK)))]
#[get("/user/{user_id}/organizations")]
pub async fn orgs_list(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::PROJECT_READ,
)
.await
.map(|x| x.1)
.ok();
let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(id) = id_option.map(|x| x.id) {
let org_data = DBUser::get_organizations(id, &**pool).await?;
let organizations_data =
crate::database::models::organization_item::DBOrganization::get_many_ids(
&org_data, &**pool, &redis,
)
.await?;
let team_ids = organizations_data
.iter()
.map(|x| x.team_id)
.collect::<Vec<_>>();
let teams_data =
crate::database::models::DBTeamMember::get_from_team_full_many(
&team_ids, &**pool, &redis,
)
.await?;
let users = DBUser::get_many_ids(
&teams_data.iter().map(|x| x.user_id).collect::<Vec<_>>(),
&**pool,
&redis,
)
.await?;
let mut organizations = vec![];
let mut team_groups = HashMap::new();
for item in teams_data {
team_groups.entry(item.team_id).or_insert(vec![]).push(item);
}
for data in organizations_data {
if !is_visible_organization(&data, &user, &pool, &redis).await? {
continue;
}
let members_data =
team_groups.remove(&data.team_id).unwrap_or(vec![]);
let logged_in = user
.as_ref()
.and_then(|user| {
members_data
.iter()
.find(|x| x.user_id == user.id.into() && x.accepted)
})
.is_some();
let team_members: Vec<_> = members_data
.into_iter()
.filter(|x| logged_in || x.accepted || id == x.user_id)
.filter_map(|data| {
users.iter().find(|x| x.id == data.user_id).map(|user| {
crate::models::teams::TeamMember::from(
data,
user.clone(),
!logged_in,
)
})
})
.collect();
let organization = crate::models::organizations::Organization::from(
data,
team_members,
);
organizations.push(organization);
}
Ok(HttpResponse::Ok().json(organizations))
} else {
Err(ApiError::NotFound)
}
}
#[derive(Serialize, Deserialize, Validate, utoipa::ToSchema)]
pub struct EditUser {
#[validate(length(min = 1, max = 39), regex(path = *crate::util::validate::RE_URL_SAFE))]
pub username: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "::serde_with::rust::double_option"
)]
#[validate(length(max = 160))]
pub bio: Option<Option<String>>,
pub role: Option<Role>,
pub badges: Option<Badges>,
#[validate(length(max = 160))]
pub venmo_handle: Option<String>,
pub allow_friend_requests: Option<bool>,
}
#[utoipa::path(tag = "users", responses((status = NO_CONTENT)))]
#[patch("/user/{id}")]
pub async fn user_edit_route(
req: HttpRequest,
info: web::Path<(String,)>,
new_user: web::Json<EditUser>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
user_edit(req, info, new_user, pool, redis, session_queue).await
}
pub async fn user_edit(
req: HttpRequest,
info: web::Path<(String,)>,
new_user: web::Json<EditUser>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let (scopes, user) = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::USER_WRITE,
)
.await?;
new_user.validate().map_err(|err| {
ApiError::Validation(validation_errors_to_string(err, None))
})?;
let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(actual_user) = id_option {
let id = actual_user.id;
let user_id: UserId = id.into();
if user.id == user_id || user.role.is_mod() {
let mut transaction = pool.begin().await?;
if let Some(username) = &new_user.username {
let existing_user_id_option =
DBUser::get(username, &**pool, &redis).await?;
if existing_user_id_option
.map(|x| UserId::from(x.id))
.is_none_or(|id| id == user.id)
{
sqlx::query!(
"
UPDATE users
SET username = $1
WHERE (id = $2)
",
username,
id as crate::database::models::ids::DBUserId,
)
.execute(&mut transaction)
.await?;
} else {
return Err(ApiError::InvalidInput(format!(
"Username {username} is taken!"
)));
}
}
if let Some(bio) = &new_user.bio {
sqlx::query!(
"
UPDATE users
SET bio = $1
WHERE (id = $2)
",
bio.as_deref(),
id as crate::database::models::ids::DBUserId,
)
.execute(&mut transaction)
.await?;
}
if let Some(role) = &new_user.role {
if !user.role.is_admin() {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the role of this user!"
.to_string(),
));
}
let role = role.to_string();
sqlx::query!(
"
UPDATE users
SET role = $1
WHERE (id = $2)
",
role,
id as crate::database::models::ids::DBUserId,
)
.execute(&mut transaction)
.await?;
}
if let Some(badges) = &new_user.badges {
if !user.role.is_admin() {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the badges of this user!"
.to_string(),
));
}
sqlx::query!(
"
UPDATE users
SET badges = $1
WHERE (id = $2)
",
badges.bits() as i64,
id as crate::database::models::ids::DBUserId,
)
.execute(&mut transaction)
.await?;
}
if let Some(venmo_handle) = &new_user.venmo_handle {
if !scopes.contains(Scopes::PAYOUTS_WRITE) {
return Err(ApiError::CustomAuthentication(
"You do not have the permissions to edit the venmo handle of this user!"
.to_string(),
));
}
sqlx::query!(
"
UPDATE users
SET venmo_handle = $1
WHERE (id = $2)
",
venmo_handle,
id as crate::database::models::ids::DBUserId,
)
.execute(&mut transaction)
.await?;
}
if let Some(allow_friend_requests) = &user.allow_friend_requests {
sqlx::query!(
"
UPDATE users
SET allow_friend_requests = $1
WHERE (id = $2)
",
allow_friend_requests,
id as crate::database::models::ids::DBUserId,
)
.execute(&mut transaction)
.await?;
}
transaction.commit().await?;
DBUser::clear_caches(&[(id, Some(actual_user.username))], &redis)
.await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::CustomAuthentication(
"You do not have permission to edit this user!".to_string(),
))
}
} else {
Err(ApiError::NotFound)
}
}
#[derive(Serialize, Deserialize)]
pub struct Extension {
pub ext: String,
}
#[allow(clippy::too_many_arguments)]
#[utoipa::path(
tag = "users",
params(("ext" = String, Query)),
request_body(content = Vec<u8>, content_type = "application/octet-stream"),
responses((status = NO_CONTENT))
)]
#[patch("/user/{id}/icon")]
pub async fn user_icon_edit_route(
ext: web::Query<Extension>,
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<dyn FileHost>,
payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
user_icon_edit(
ext,
req,
info,
pool,
redis,
file_host,
payload,
session_queue,
)
.await
}
pub async fn user_icon_edit(
web::Query(ext): web::Query<Extension>,
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<dyn FileHost>,
mut payload: web::Payload,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::USER_WRITE,
)
.await?
.1;
let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(actual_user) = id_option {
if user.id != actual_user.id.into() && !user.role.is_mod() {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this user's icon."
.to_string(),
));
}
delete_old_images(
actual_user.avatar_url,
actual_user.raw_avatar_url,
FileHostPublicity::Public,
&**file_host,
)
.await?;
let bytes = read_limited_from_payload(
&mut payload,
262144,
"Icons must be smaller than 256KiB",
)
.await?;
let user_id: UserId = actual_user.id.into();
let upload_result = crate::util::img::upload_image_optimized(
&format!("data/{user_id}"),
FileHostPublicity::Public,
bytes.freeze(),
&ext.ext,
Some(96),
Some(1.0),
&**file_host,
)
.await?;
sqlx::query!(
"
UPDATE users
SET avatar_url = $1, raw_avatar_url = $2
WHERE (id = $3)
",
upload_result.url,
upload_result.raw_url,
actual_user.id as crate::database::models::ids::DBUserId,
)
.execute(&**pool)
.await?;
DBUser::clear_caches(&[(actual_user.id, None)], &redis).await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::NotFound)
}
}
#[utoipa::path(tag = "users", responses((status = NO_CONTENT)))]
#[delete("/user/{id}/icon")]
pub async fn user_icon_delete_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<dyn FileHost>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
user_icon_delete(req, info, pool, redis, file_host, session_queue).await
}
pub async fn user_icon_delete(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
file_host: web::Data<dyn FileHost>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::USER_WRITE,
)
.await?
.1;
let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(actual_user) = id_option {
if user.id != actual_user.id.into() && !user.role.is_mod() {
return Err(ApiError::CustomAuthentication(
"You don't have permission to edit this user's icon."
.to_string(),
));
}
delete_old_images(
actual_user.avatar_url,
actual_user.raw_avatar_url,
FileHostPublicity::Public,
&**file_host,
)
.await?;
sqlx::query!(
"
UPDATE users
SET avatar_url = NULL, raw_avatar_url = NULL
WHERE (id = $1)
",
actual_user.id as crate::database::models::ids::DBUserId,
)
.execute(&**pool)
.await?;
DBUser::clear_caches(&[(actual_user.id, None)], &redis).await?;
Ok(HttpResponse::NoContent().body(""))
} else {
Err(ApiError::NotFound)
}
}
#[utoipa::path(tag = "users", responses((status = NO_CONTENT)))]
#[delete("/user/{id}")]
pub async fn user_delete_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<(), ApiError> {
user_delete(req, info, pool, redis, session_queue).await
}
pub async fn user_delete(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<(), ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::USER_DELETE,
)
.await?
.1;
let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis)
.await
.wrap_internal_err("failed to get user")?;
let id = id_option.map(|x| x.id).ok_or(ApiError::NotFound)?;
if !user.role.is_admin() && user.id != id.into() {
return Err(ApiError::CustomAuthentication(
"You do not have permission to delete this user!".to_string(),
));
}
let mut transaction = pool
.begin()
.await
.wrap_internal_err("failed to begin transaction")?;
let result = DBUser::remove(id, &mut transaction, &redis)
.await
.wrap_internal_err("failed to remove user")?;
transaction
.commit()
.await
.wrap_internal_err("failed to commit transaction")?;
if result.is_some() {
Ok(())
} else {
Err(ApiError::NotFound)
}
}
#[utoipa::path(tag = "users", responses((status = OK)))]
#[get("/user/{id}/follows")]
pub async fn user_follows_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
user_follows(req, info, pool, redis, session_queue).await
}
pub async fn user_follows(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::USER_READ,
)
.await?
.1;
let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(id) = id_option.map(|x| x.id) {
if !user.role.is_admin() && user.id != id.into() {
return Err(ApiError::CustomAuthentication(
"You do not have permission to see the projects this user follows!".to_string(),
));
}
let project_ids = DBUser::get_follows(id, &**pool).await?;
let projects: Vec<_> = crate::database::DBProject::get_many_ids(
&project_ids,
&**pool,
&redis,
)
.await?
.into_iter()
.map(Project::from)
.collect();
Ok(HttpResponse::Ok().json(projects))
} else {
Err(ApiError::NotFound)
}
}
#[utoipa::path(tag = "users", responses((status = OK)))]
#[get("/user/{id}/notifications")]
pub async fn user_notifications_route(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
user_notifications(req, info, pool, redis, session_queue).await
}
pub async fn user_notifications(
req: HttpRequest,
info: web::Path<(String,)>,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<HttpResponse, ApiError> {
let user = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::NOTIFICATION_READ,
)
.await?
.1;
let id_option = DBUser::get(&info.into_inner().0, &**pool, &redis).await?;
if let Some(id) = id_option.map(|x| x.id) {
if !user.role.is_admin() && user.id != id.into() {
return Err(ApiError::CustomAuthentication(
"You do not have permission to see the notifications of this user!".to_string(),
));
}
let mut notifications: Vec<Notification> =
crate::database::models::notification_item::DBNotification::get_many_user_exposed_on_site(
id, &**pool, &redis,
)
.await?
.into_iter()
.map(Into::into)
.collect();
notifications.sort_by_key(|b| Reverse(b.created));
Ok(HttpResponse::Ok().json(notifications))
} else {
Err(ApiError::NotFound)
}
}