From 8f146188d57b5c08a4c6204674c15379232280d8 Mon Sep 17 00:00:00 2001 From: Kyle Mendell Date: Tue, 18 Mar 2025 14:59:31 -0500 Subject: [PATCH] feat(profile-picture): allow reset of profile picture (#355) Co-authored-by: Elias Schneider --- .../internal/controller/user_controller.go | 40 +++++++++ backend/internal/service/user_service.go | 24 +++++ .../form/profile-picture-settings.svelte | 87 +++++++++++++------ frontend/src/lib/services/user-service.ts | 8 ++ .../src/routes/settings/account/+page.svelte | 16 +++- .../settings/admin/users/[id]/+page.svelte | 12 ++- 6 files changed, 156 insertions(+), 31 deletions(-) diff --git a/backend/internal/controller/user_controller.go b/backend/internal/controller/user_controller.go index ee207e4c..9dede1ed 100644 --- a/backend/internal/controller/user_controller.go +++ b/backend/internal/controller/user_controller.go @@ -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) +} diff --git a/backend/internal/service/user_service.go b/backend/internal/service/user_service.go index 663b27b1..42507bba 100644 --- a/backend/internal/service/user_service.go +++ b/backend/internal/service/user_service.go @@ -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 +} diff --git a/frontend/src/lib/components/form/profile-picture-settings.svelte b/frontend/src/lib/components/form/profile-picture-settings.svelte index 6631011c..c49e6f74 100644 --- a/frontend/src/lib/components/form/profile-picture-settings.svelte +++ b/frontend/src/lib/components/form/profile-picture-settings.svelte @@ -1,20 +1,23 @@
@@ -50,34 +69,48 @@

The image should be in PNG or JPEG format.

{/if} +
{#if isLdapUser} {:else} - -
- - - -
- {#if isLoading} - - {:else} - - {/if} +
+ +
+ + + +
+ {#if isLoading} + + {:else} + + {/if} +
-
- + +
{/if}
diff --git a/frontend/src/lib/services/user-service.ts b/frontend/src/lib/services/user-service.ts index 7e40aabb..9e87199c 100644 --- a/frontend/src/lib/services/user-service.ts +++ b/frontend/src/lib/services/user-service.ts @@ -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, diff --git a/frontend/src/routes/settings/account/+page.svelte b/frontend/src/routes/settings/account/+page.svelte index 03620abd..fbd2a02c 100644 --- a/frontend/src/routes/settings/account/+page.svelte +++ b/frontend/src/routes/settings/account/+page.svelte @@ -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 @@ diff --git a/frontend/src/routes/settings/admin/users/[id]/+page.svelte b/frontend/src/routes/settings/admin/users/[id]/+page.svelte index 0484069c..71a93659 100644 --- a/frontend/src/routes/settings/admin/users/[id]/+page.svelte +++ b/frontend/src/routes/settings/admin/users/[id]/+page.svelte @@ -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); } @@ -89,7 +96,8 @@