You've already forked AstralRinth
forked from didirus/AstralRinth
Add ability to delete user icon (#3383)
* Add user icon delete route By request of moderation, but also just generally nice to have * Add relevant docs and frontend * Add v2 version
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
openapi: '3.0.0'
|
||||
|
||||
info:
|
||||
version: v2.7.0/15cf3fc
|
||||
version: v2.7.0/366f528
|
||||
title: Labrinth
|
||||
termsOfService: https://modrinth.com/legal/terms
|
||||
contact:
|
||||
@@ -3018,6 +3018,24 @@ paths:
|
||||
$ref: '#/components/schemas/InvalidInputError'
|
||||
'404':
|
||||
description: The requested item(s) were not found or no authorization to access the requested item(s)
|
||||
delete:
|
||||
summary: Remove user's avatar
|
||||
operationId: deleteUserIcon
|
||||
tags:
|
||||
- users
|
||||
security:
|
||||
- TokenAuth: ['USER_WRITE']
|
||||
responses:
|
||||
'204':
|
||||
description: Expected response to a valid request
|
||||
'400':
|
||||
description: Request was invalid, see given error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/InvalidInputError'
|
||||
'404':
|
||||
description: The requested item(s) were not found or no authorization to access the requested item(s)
|
||||
/user/{id|username}/projects:
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/UserIdentifier'
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
>
|
||||
<UploadIcon />
|
||||
</FileInput>
|
||||
<Button v-if="avatarUrl !== null" :action="removePreviewImage">
|
||||
<TrashIcon />
|
||||
{{ formatMessage(commonMessages.removeImageButton) }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="previewImage"
|
||||
:action="
|
||||
@@ -86,7 +90,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { UserIcon, SaveIcon, UploadIcon, UndoIcon, XIcon } from "@modrinth/assets";
|
||||
import { UserIcon, SaveIcon, UploadIcon, UndoIcon, XIcon, TrashIcon } from "@modrinth/assets";
|
||||
import { Avatar, FileInput, Button, commonMessages } from "@modrinth/ui";
|
||||
|
||||
useHead({
|
||||
@@ -142,6 +146,7 @@ const bio = ref(auth.value.user.bio);
|
||||
const avatarUrl = ref(auth.value.user.avatar_url);
|
||||
const icon = shallowRef(null);
|
||||
const previewImage = shallowRef(null);
|
||||
const pendingAvatarDeletion = ref(false);
|
||||
const saved = ref(false);
|
||||
|
||||
const hasUnsavedChanges = computed(
|
||||
@@ -160,9 +165,15 @@ function showPreviewImage(files) {
|
||||
};
|
||||
}
|
||||
|
||||
function removePreviewImage() {
|
||||
pendingAvatarDeletion.value = true;
|
||||
previewImage.value = "https://cdn.modrinth.com/placeholder.png";
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
icon.value = null;
|
||||
previewImage.value = null;
|
||||
pendingAvatarDeletion.value = false;
|
||||
username.value = auth.value.user.username;
|
||||
bio.value = auth.value.user.bio;
|
||||
}
|
||||
@@ -170,6 +181,14 @@ function cancel() {
|
||||
async function saveChanges() {
|
||||
startLoading();
|
||||
try {
|
||||
if (pendingAvatarDeletion.value) {
|
||||
await useBaseFetch(`user/${auth.value.user.id}/icon`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
pendingAvatarDeletion.value = false;
|
||||
previewImage.value = null;
|
||||
}
|
||||
|
||||
if (icon.value) {
|
||||
await useBaseFetch(
|
||||
`user/${auth.value.user.id}/icon?ext=${
|
||||
|
||||
14
apps/labrinth/.sqlx/query-483cb875ba81c7563a2f7220158cfcb9e6a117a4efc070438606e4c94103a9a4.json
generated
Normal file
14
apps/labrinth/.sqlx/query-483cb875ba81c7563a2f7220158cfcb9e6a117a4efc070438606e4c94103a9a4.json
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE users\n SET avatar_url = NULL, raw_avatar_url = NULL\n WHERE (id = $1)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "483cb875ba81c7563a2f7220158cfcb9e6a117a4efc070438606e4c94103a9a4"
|
||||
}
|
||||
@@ -27,6 +27,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
.service(user_delete)
|
||||
.service(user_edit)
|
||||
.service(user_icon_edit)
|
||||
.service(user_icon_delete)
|
||||
.service(user_notifications)
|
||||
.service(user_follows),
|
||||
);
|
||||
@@ -223,6 +224,28 @@ pub async fn user_icon_edit(
|
||||
.or_else(v2_reroute::flatten_404_error)
|
||||
}
|
||||
|
||||
#[delete("{id}/icon")]
|
||||
pub async fn user_icon_delete(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
// Returns NoContent, so we don't need to convert to V2
|
||||
v3::users::user_icon_delete(
|
||||
req,
|
||||
info,
|
||||
pool,
|
||||
redis,
|
||||
file_host,
|
||||
session_queue,
|
||||
)
|
||||
.await
|
||||
.or_else(v2_reroute::flatten_404_error)
|
||||
}
|
||||
|
||||
#[delete("{id}")]
|
||||
pub async fn user_delete(
|
||||
req: HttpRequest,
|
||||
|
||||
@@ -38,6 +38,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
.route("{user_id}/organizations", web::get().to(orgs_list))
|
||||
.route("{id}", web::patch().to(user_edit))
|
||||
.route("{id}/icon", web::patch().to(user_icon_edit))
|
||||
.route("{id}/icon", web::delete().to(user_icon_delete))
|
||||
.route("{id}", web::delete().to(user_delete))
|
||||
.route("{id}/follows", web::get().to(user_follows))
|
||||
.route("{id}/notifications", web::get().to(user_notifications))
|
||||
@@ -623,6 +624,59 @@ pub async fn user_icon_edit(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn user_icon_delete(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
pool: web::Data<PgPool>,
|
||||
redis: web::Data<RedisPool>,
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let id_option = User::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,
|
||||
&***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::UserId,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await?;
|
||||
|
||||
User::clear_caches(&[(actual_user.id, None)], &redis).await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn user_delete(
|
||||
req: HttpRequest,
|
||||
info: web::Path<(String,)>,
|
||||
|
||||
@@ -153,6 +153,10 @@ export const commonMessages = defineMessages({
|
||||
id: 'button.upload-image',
|
||||
defaultMessage: 'Upload image',
|
||||
},
|
||||
removeImageButton: {
|
||||
id: 'button.remove-image',
|
||||
defaultMessage: 'Remove image',
|
||||
},
|
||||
visibilityLabel: {
|
||||
id: 'label.visibility',
|
||||
defaultMessage: 'Visibility',
|
||||
|
||||
Reference in New Issue
Block a user