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'
|
openapi: '3.0.0'
|
||||||
|
|
||||||
info:
|
info:
|
||||||
version: v2.7.0/15cf3fc
|
version: v2.7.0/366f528
|
||||||
title: Labrinth
|
title: Labrinth
|
||||||
termsOfService: https://modrinth.com/legal/terms
|
termsOfService: https://modrinth.com/legal/terms
|
||||||
contact:
|
contact:
|
||||||
@@ -3018,6 +3018,24 @@ paths:
|
|||||||
$ref: '#/components/schemas/InvalidInputError'
|
$ref: '#/components/schemas/InvalidInputError'
|
||||||
'404':
|
'404':
|
||||||
description: The requested item(s) were not found or no authorization to access the requested item(s)
|
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:
|
/user/{id|username}/projects:
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/UserIdentifier'
|
- $ref: '#/components/parameters/UserIdentifier'
|
||||||
|
|||||||
@@ -32,6 +32,10 @@
|
|||||||
>
|
>
|
||||||
<UploadIcon />
|
<UploadIcon />
|
||||||
</FileInput>
|
</FileInput>
|
||||||
|
<Button v-if="avatarUrl !== null" :action="removePreviewImage">
|
||||||
|
<TrashIcon />
|
||||||
|
{{ formatMessage(commonMessages.removeImageButton) }}
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-if="previewImage"
|
v-if="previewImage"
|
||||||
:action="
|
:action="
|
||||||
@@ -86,7 +90,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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";
|
import { Avatar, FileInput, Button, commonMessages } from "@modrinth/ui";
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
@@ -142,6 +146,7 @@ const bio = ref(auth.value.user.bio);
|
|||||||
const avatarUrl = ref(auth.value.user.avatar_url);
|
const avatarUrl = ref(auth.value.user.avatar_url);
|
||||||
const icon = shallowRef(null);
|
const icon = shallowRef(null);
|
||||||
const previewImage = shallowRef(null);
|
const previewImage = shallowRef(null);
|
||||||
|
const pendingAvatarDeletion = ref(false);
|
||||||
const saved = ref(false);
|
const saved = ref(false);
|
||||||
|
|
||||||
const hasUnsavedChanges = computed(
|
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() {
|
function cancel() {
|
||||||
icon.value = null;
|
icon.value = null;
|
||||||
previewImage.value = null;
|
previewImage.value = null;
|
||||||
|
pendingAvatarDeletion.value = false;
|
||||||
username.value = auth.value.user.username;
|
username.value = auth.value.user.username;
|
||||||
bio.value = auth.value.user.bio;
|
bio.value = auth.value.user.bio;
|
||||||
}
|
}
|
||||||
@@ -170,6 +181,14 @@ function cancel() {
|
|||||||
async function saveChanges() {
|
async function saveChanges() {
|
||||||
startLoading();
|
startLoading();
|
||||||
try {
|
try {
|
||||||
|
if (pendingAvatarDeletion.value) {
|
||||||
|
await useBaseFetch(`user/${auth.value.user.id}/icon`, {
|
||||||
|
method: "DELETE",
|
||||||
|
});
|
||||||
|
pendingAvatarDeletion.value = false;
|
||||||
|
previewImage.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (icon.value) {
|
if (icon.value) {
|
||||||
await useBaseFetch(
|
await useBaseFetch(
|
||||||
`user/${auth.value.user.id}/icon?ext=${
|
`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_delete)
|
||||||
.service(user_edit)
|
.service(user_edit)
|
||||||
.service(user_icon_edit)
|
.service(user_icon_edit)
|
||||||
|
.service(user_icon_delete)
|
||||||
.service(user_notifications)
|
.service(user_notifications)
|
||||||
.service(user_follows),
|
.service(user_follows),
|
||||||
);
|
);
|
||||||
@@ -223,6 +224,28 @@ pub async fn user_icon_edit(
|
|||||||
.or_else(v2_reroute::flatten_404_error)
|
.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}")]
|
#[delete("{id}")]
|
||||||
pub async fn user_delete(
|
pub async fn user_delete(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||||||
.route("{user_id}/organizations", web::get().to(orgs_list))
|
.route("{user_id}/organizations", web::get().to(orgs_list))
|
||||||
.route("{id}", web::patch().to(user_edit))
|
.route("{id}", web::patch().to(user_edit))
|
||||||
.route("{id}/icon", web::patch().to(user_icon_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}", web::delete().to(user_delete))
|
||||||
.route("{id}/follows", web::get().to(user_follows))
|
.route("{id}/follows", web::get().to(user_follows))
|
||||||
.route("{id}/notifications", web::get().to(user_notifications))
|
.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(
|
pub async fn user_delete(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
info: web::Path<(String,)>,
|
info: web::Path<(String,)>,
|
||||||
|
|||||||
@@ -153,6 +153,10 @@ export const commonMessages = defineMessages({
|
|||||||
id: 'button.upload-image',
|
id: 'button.upload-image',
|
||||||
defaultMessage: 'Upload image',
|
defaultMessage: 'Upload image',
|
||||||
},
|
},
|
||||||
|
removeImageButton: {
|
||||||
|
id: 'button.remove-image',
|
||||||
|
defaultMessage: 'Remove image',
|
||||||
|
},
|
||||||
visibilityLabel: {
|
visibilityLabel: {
|
||||||
id: 'label.visibility',
|
id: 'label.visibility',
|
||||||
defaultMessage: 'Visibility',
|
defaultMessage: 'Visibility',
|
||||||
|
|||||||
Reference in New Issue
Block a user