mirror of
https://github.com/pocket-id/pocket-id.git
synced 2025-12-09 23:02:56 +03:00
feat(profile-picture): allow reset of profile picture (#355)
Co-authored-by: Elias Schneider <login@eliasschneider.com>
This commit is contained in:
@@ -47,6 +47,9 @@ func NewUserController(group *gin.RouterGroup, authMiddleware *middleware.AuthMi
|
||||
group.POST("/one-time-access-token/:token", rateLimitMiddleware.Add(rate.Every(10*time.Second), 5), uc.exchangeOneTimeAccessTokenHandler)
|
||||
group.POST("/one-time-access-token/setup", uc.getSetupAccessTokenHandler)
|
||||
group.POST("/one-time-access-email", rateLimitMiddleware.Add(rate.Every(10*time.Minute), 3), uc.requestOneTimeAccessEmailHandler)
|
||||
|
||||
group.DELETE("/users/:id/profile-picture", authMiddleware.Add(), uc.resetUserProfilePictureHandler)
|
||||
group.DELETE("/users/me/profile-picture", authMiddleware.WithAdminNotRequired().Add(), uc.resetCurrentUserProfilePictureHandler)
|
||||
}
|
||||
|
||||
type UserController struct {
|
||||
@@ -480,3 +483,40 @@ func (uc *UserController) updateUser(c *gin.Context, updateOwnUser bool) {
|
||||
|
||||
c.JSON(http.StatusOK, userDto)
|
||||
}
|
||||
|
||||
// resetUserProfilePictureHandler godoc
|
||||
// @Summary Reset user profile picture
|
||||
// @Description Reset a specific user's profile picture to the default
|
||||
// @Tags Users
|
||||
// @Produce json
|
||||
// @Param id path string true "User ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Router /users/{id}/profile-picture [delete]
|
||||
func (uc *UserController) resetUserProfilePictureHandler(c *gin.Context) {
|
||||
userID := c.Param("id")
|
||||
|
||||
if err := uc.userService.ResetProfilePicture(userID); err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// resetCurrentUserProfilePictureHandler godoc
|
||||
// @Summary Reset current user's profile picture
|
||||
// @Description Reset the currently authenticated user's profile picture to the default
|
||||
// @Tags Users
|
||||
// @Produce json
|
||||
// @Success 204 "No Content"
|
||||
// @Router /users/me/profile-picture [delete]
|
||||
func (uc *UserController) resetCurrentUserProfilePictureHandler(c *gin.Context) {
|
||||
userID := c.GetString("userID")
|
||||
|
||||
if err := uc.userService.ResetProfilePicture(userID); err != nil {
|
||||
c.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
@@ -365,3 +365,27 @@ func (s *UserService) checkDuplicatedFields(user model.User) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetProfilePicture deletes a user's custom profile picture
|
||||
func (s *UserService) ResetProfilePicture(userID string) error {
|
||||
// Validate the user ID to prevent directory traversal
|
||||
if err := uuid.Validate(userID); err != nil {
|
||||
return &common.InvalidUUIDError{}
|
||||
}
|
||||
|
||||
// Build path to profile picture
|
||||
profilePicturePath := fmt.Sprintf("%s/profile-pictures/%s.png", common.EnvConfig.UploadPath, userID)
|
||||
|
||||
// Check if file exists and delete it
|
||||
if _, err := os.Stat(profilePicturePath); err == nil {
|
||||
if err := os.Remove(profilePicturePath); err != nil {
|
||||
return fmt.Errorf("failed to delete profile picture: %w", err)
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
// If any error other than "file not exists"
|
||||
return fmt.Errorf("failed to check if profile picture exists: %w", err)
|
||||
}
|
||||
// It's okay if the file doesn't exist - just means there's no custom picture to delete
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
<script lang="ts">
|
||||
import FileInput from '$lib/components/form/file-input.svelte';
|
||||
import * as Avatar from '$lib/components/ui/avatar';
|
||||
import { LucideLoader, LucideUpload } from 'lucide-svelte';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import { LucideLoader, LucideRefreshCw, LucideUpload } from 'lucide-svelte';
|
||||
import { openConfirmDialog } from '../confirm-dialog';
|
||||
|
||||
let {
|
||||
userId,
|
||||
isLdapUser = false,
|
||||
callback
|
||||
resetCallback,
|
||||
updateCallback
|
||||
}: {
|
||||
userId: string;
|
||||
isLdapUser?: boolean;
|
||||
callback: (image: File) => Promise<void>;
|
||||
resetCallback: () => Promise<void>;
|
||||
updateCallback: (image: File) => Promise<void>;
|
||||
} = $props();
|
||||
|
||||
let isLoading = $state(false);
|
||||
|
||||
let imageDataURL = $state(`/api/users/${userId}/profile-picture.png`);
|
||||
|
||||
async function onImageChange(e: Event) {
|
||||
@@ -29,11 +32,27 @@
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
|
||||
await callback(file).catch(() => {
|
||||
imageDataURL = `/api/users/${userId}/profile-picture.png`;
|
||||
await updateCallback(file).catch(() => {
|
||||
imageDataURL = `/api/users/${userId}/profile-picture.png}`;
|
||||
});
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
function onReset() {
|
||||
openConfirmDialog({
|
||||
title: 'Reset profile picture?',
|
||||
message:
|
||||
'This will remove the uploaded image, and reset the profile picture to default. Do you want to continue?',
|
||||
confirm: {
|
||||
label: 'Reset',
|
||||
action: async () => {
|
||||
isLoading = true;
|
||||
await resetCallback().catch();
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex gap-5">
|
||||
@@ -50,34 +69,48 @@
|
||||
</p>
|
||||
<p class="text-muted-foreground mt-1 text-sm">The image should be in PNG or JPEG format.</p>
|
||||
{/if}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="mt-5"
|
||||
on:click={onReset}
|
||||
disabled={isLoading || isLdapUser}
|
||||
>
|
||||
<LucideRefreshCw class="mr-2 h-4 w-4" />
|
||||
Reset to default
|
||||
</Button>
|
||||
</div>
|
||||
{#if isLdapUser}
|
||||
<Avatar.Root class="h-24 w-24">
|
||||
<Avatar.Image class="object-cover" src={imageDataURL} />
|
||||
</Avatar.Root>
|
||||
{:else}
|
||||
<FileInput
|
||||
id="profile-picture-input"
|
||||
variant="secondary"
|
||||
accept="image/png, image/jpeg"
|
||||
onchange={onImageChange}
|
||||
>
|
||||
<div class="group relative h-28 w-28 rounded-full">
|
||||
<Avatar.Root class="h-full w-full transition-opacity duration-200">
|
||||
<Avatar.Image
|
||||
class="object-cover group-hover:opacity-10 {isLoading ? 'opacity-10' : ''}"
|
||||
src={imageDataURL}
|
||||
/>
|
||||
</Avatar.Root>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
{#if isLoading}
|
||||
<LucideLoader class="h-5 w-5 animate-spin" />
|
||||
{:else}
|
||||
<LucideUpload class="h-5 w-5 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
{/if}
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<FileInput
|
||||
id="profile-picture-input"
|
||||
variant="secondary"
|
||||
accept="image/png, image/jpeg"
|
||||
onchange={onImageChange}
|
||||
>
|
||||
<div class="group relative h-28 w-28 rounded-full">
|
||||
<Avatar.Root class="h-full w-full transition-opacity duration-200">
|
||||
<Avatar.Image
|
||||
class="object-cover group-hover:opacity-10 {isLoading ? 'opacity-10' : ''}"
|
||||
src={imageDataURL}
|
||||
/>
|
||||
</Avatar.Root>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
{#if isLoading}
|
||||
<LucideLoader class="h-5 w-5 animate-spin" />
|
||||
{:else}
|
||||
<LucideUpload
|
||||
class="h-5 w-5 opacity-0 transition-opacity group-hover:opacity-100"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</FileInput>
|
||||
</FileInput>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,6 +59,14 @@ export default class UserService extends APIService {
|
||||
await this.api.put('/users/me/profile-picture', formData);
|
||||
}
|
||||
|
||||
async resetCurrentUserProfilePicture() {
|
||||
await this.api.delete(`/users/me/profile-picture`);
|
||||
}
|
||||
|
||||
async resetProfilePicture(userId: string) {
|
||||
await this.api.delete(`/users/${userId}/profile-picture`);
|
||||
}
|
||||
|
||||
async createOneTimeAccessToken(expiresAt: Date, userId: string) {
|
||||
const res = await this.api.post(`/users/${userId}/one-time-access-token`, {
|
||||
userId,
|
||||
|
||||
@@ -26,6 +26,15 @@
|
||||
const userService = new UserService();
|
||||
const webauthnService = new WebAuthnService();
|
||||
|
||||
async function resetProfilePicture() {
|
||||
await userService
|
||||
.resetCurrentUserProfilePicture()
|
||||
.then(() =>
|
||||
toast.success('Profile picture has been reset. It may take a few minutes to update.')
|
||||
)
|
||||
.catch(axiosErrorToast);
|
||||
}
|
||||
|
||||
async function updateAccount(user: UserCreate) {
|
||||
let success = true;
|
||||
await userService
|
||||
@@ -42,7 +51,9 @@
|
||||
async function updateProfilePicture(image: File) {
|
||||
await userService
|
||||
.updateCurrentUsersProfilePicture(image)
|
||||
.then(() => toast.success('Profile picture updated successfully'))
|
||||
.then(() =>
|
||||
toast.success('Profile picture updated successfully. It may take a few minutes to update.')
|
||||
)
|
||||
.catch(axiosErrorToast);
|
||||
}
|
||||
|
||||
@@ -101,7 +112,8 @@
|
||||
<ProfilePictureSettings
|
||||
userId={account.id}
|
||||
isLdapUser={!!account.ldapId}
|
||||
callback={updateProfilePicture}
|
||||
updateCallback={updateProfilePicture}
|
||||
resetCallback={resetProfilePicture}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
@@ -58,7 +58,14 @@
|
||||
async function updateProfilePicture(image: File) {
|
||||
await userService
|
||||
.updateProfilePicture(user.id, image)
|
||||
.then(() => toast.success('Profile picture updated successfully'))
|
||||
.then(() => toast.success('Profile picture updated successfully. It may take a few minutes to update.'))
|
||||
.catch(axiosErrorToast);
|
||||
}
|
||||
|
||||
async function resetProfilePicture() {
|
||||
await userService
|
||||
.resetProfilePicture(user.id)
|
||||
.then(() => toast.success('Profile picture has been reset. It may take a few minutes to update.'))
|
||||
.catch(axiosErrorToast);
|
||||
}
|
||||
</script>
|
||||
@@ -89,7 +96,8 @@
|
||||
<ProfilePictureSettings
|
||||
userId={user.id}
|
||||
isLdapUser={!!user.ldapId}
|
||||
callback={updateProfilePicture}
|
||||
updateCallback={updateProfilePicture}
|
||||
resetCallback={resetProfilePicture}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
Reference in New Issue
Block a user