"message": "Password changed successfully",
})
}
+
+type DeleteAccountRequest struct {
+ Password string `json:"password"`
+}
+
+func DeleteAccount(w http.ResponseWriter, r *http.Request) {
+ // Get user ID from context
+ userID, ok := r.Context().Value(utils.UserIDKey).(int)
+ if !ok {
+ utils.JSONResponse(w, http.StatusOK, map[string]any{
+ "success": false,
+ "message": "User not authenticated",
+ })
+ return
+ }
+
+ // Parse request body
+ var req DeleteAccountRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
+ "success": false,
+ "message": "Invalid request body",
+ })
+ return
+ }
+
+ // Check if password is correct
+ users, err := utils.GetUsers()
+ if err != nil {
+ utils.JSONResponse(w, http.StatusOK, map[string]any{
+ "success": false,
+ "message": fmt.Sprintf("Error retrieving users: %v", err),
+ })
+ return
+ }
+ usersList, ok := users["users"].([]any)
+ if !ok {
+ utils.JSONResponse(w, http.StatusOK, map[string]any{
+ "success": false,
+ "message": "Users data is not in the correct format",
+ })
+ return
+ }
+
+ var user map[string]any
+ for _, u := range usersList {
+ uMap, ok := u.(map[string]any)
+ if !ok {
+ continue
+ }
+ if id, ok := uMap["user_id"].(float64); ok && int(id) == userID {
+ user = uMap
+ break
+ }
+ }
+
+ if user == nil {
+ utils.JSONResponse(w, http.StatusOK, map[string]any{
+ "success": false,
+ "message": "User not found",
+ })
+ return
+ }
+
+ password, ok := user["password"].(string)
+ if !ok {
+ utils.JSONResponse(w, http.StatusOK, map[string]any{
+ "success": false,
+ "message": "Current hashed password not found for user",
+ })
+ return
+ }
+
+ if !utils.VerifyPassword(req.Password, password) {
+ utils.JSONResponse(w, http.StatusOK, map[string]any{
+ "success": false,
+ "message": "Password is incorrect",
+ "password_incorrect": true,
+ })
+ return
+ }
+
+ // Remove user from users list
+ var newUsersList []any
+ for _, u := range usersList {
+ uMap, ok := u.(map[string]any)
+ if !ok {
+ continue
+ }
+ // Keep all users, except the one with the same user_id
+ if id, ok := uMap["user_id"].(float64); !ok || int(id) != userID {
+ utils.Logger.Printf("Keeping user with ID %f (%d)", id, userID)
+ newUsersList = append(newUsersList, u)
+ }
+ }
+ users["users"] = newUsersList
+
+ if err := utils.WriteUsers(users); err != nil {
+ utils.JSONResponse(w, http.StatusOK, map[string]any{
+ "success": false,
+ "message": fmt.Sprintf("Error writing users data: %v", err),
+ })
+ return
+ }
+
+ // Delete directory of the user with all his data
+ if err := utils.DeleteUserData(userID); err != nil {
+ utils.JSONResponse(w, http.StatusOK, map[string]any{
+ "success": false,
+ "message": fmt.Sprintf("Error deleting user data of ID %d (account already deleted): %v", userID, err),
+ })
+ utils.Logger.Printf("Error deleting user data of ID %d (You can savely delete the directory with the same id): %v", userID, err)
+ return
+ }
+
+ utils.JSONResponse(w, http.StatusOK, map[string]any{
+ "success": true,
+ })
+}
mux.HandleFunc("GET /users/getUserSettings", middleware.RequireAuth(handlers.GetUserSettings))
mux.HandleFunc("POST /users/saveUserSettings", middleware.RequireAuth(handlers.SaveUserSettings))
mux.HandleFunc("POST /users/changePassword", middleware.RequireAuth(handlers.ChangePassword))
+ mux.HandleFunc("POST /users/deleteAccount", middleware.RequireAuth(handlers.DeleteAccount))
mux.HandleFunc("POST /logs/saveLog", middleware.RequireAuth(handlers.SaveLog))
mux.HandleFunc("GET /logs/getLog", middleware.RequireAuth(handlers.GetLog))
return months, nil
}
+
+func DeleteUserData(userID int) error {
+ // Try to remove the user directory
+ dirPath := filepath.Join(Settings.DataPath, strconv.Itoa(userID))
+ if err := os.RemoveAll(dirPath); err != nil {
+ Logger.Printf("Error removing directory %s: %v", dirPath, err)
+ return fmt.Errorf("internal server error when trying to remove user data for ID %d", userID)
+ }
+
+ return nil
+}
});
document.getElementById('settingsModal').addEventListener('shown.bs.modal', function () {
- console.log("triggered 'shown.bs.modal' event");
const height = document.getElementById('modal-body').clientHeight;
document.getElementById('settings-content').style.height = 'calc(' + height + 'px - 2rem)';
document.getElementById('settings-nav').style.height = 'calc(' + height + 'px - 2rem)';
});
});
- function logout() {
+ function logout(errorCode) {
axios
.get(API_URL + '/users/logout')
.then((response) => {
localStorage.removeItem('user');
- goto('/login');
+ if (errorCode) {
+ goto(`/login?error=${errorCode}`);
+ } else {
+ goto('/login');
+ }
})
.catch((error) => {
console.error(error);
isChangingPassword = false;
});
}
+
+ let showConfirmDeleteAccount = $state(false);
+ let deleteAccountPassword = $state('');
+ let isDeletingAccount = $state(false);
+ let deleteAccountPasswordIncorrect = $state(false);
+ let showDeleteAccountSuccess = $state(false);
+
+ function deleteAccount() {
+ if (isDeletingAccount) return;
+ isDeletingAccount = true;
+
+ axios
+ .post(API_URL + '/users/deleteAccount', {
+ password: deleteAccountPassword
+ })
+ .then((response) => {
+ if (response.data.success) {
+ showDeleteAccountSuccess = true;
+
+ // close modal
+ settingsModal.hide();
+
+ logout(410); // HTTP 410 Gone => Account deleted
+ } else if (response.data.password_incorrect) {
+ deleteAccountPasswordIncorrect = true;
+ } else {
+ console.error('Error deleting account');
+ console.error(response.data);
+ }
+ })
+ .catch((error) => {
+ console.error(error);
+ deleteAccountPasswordIncorrect = true;
+ })
+ .finally(() => {
+ isDeletingAccount = false;
+ showConfirmDeleteAccount = false;
+ deleteAccountPassword = '';
+ });
+ }
</script>
<div class="d-flex flex-column h-100">
<button class="btn btn-outline-secondary me-2" onclick={openSettingsModal}
><Fa icon={faSliders} /></button
>
- <button class="btn btn-outline-secondary" onclick={logout}
+ <button class="btn btn-outline-secondary" onclick={logout(null)}
><Fa icon={faRightFromBracket} /></button
>
</div>
deleteTag={askDeleteTag}
/>
{#if deleteTagId === tag.id}
- <div class="alert alert-danger align-items-center" role="alert">
+ <div
+ class="alert alert-danger align-items-center"
+ role="alert"
+ transition:slide
+ >
<div>
<Fa icon={faTriangleExclamation} fw /> <b>Tag dauerhaft löschen?</b>
Dies kann einen Moment dauern, da jeder Eintrag nach potenziellen Verlinkungen
</div>
<div id="backupkeys"><h5>Backup-Keys</h5></div>
<div id="username"><h5>Username ändern</h5></div>
- <div id="deleteaccount"><h5>Konto löschen</h5></div>
+ <div id="deleteaccount">
+ <h5>Konto löschen</h5>
+ <p>
+ Dies löscht dein Konto und alle damit verbundenen Daten. Dies kann nicht
+ rückgängig gemacht werden!
+ </p>
+ <form
+ onsubmit={() => {
+ showConfirmDeleteAccount = true;
+ }}
+ >
+ <div class="form-floating mb-3">
+ <input
+ type="password"
+ class="form-control"
+ id="currentPassword"
+ placeholder="Aktuelles Passwort"
+ bind:value={deleteAccountPassword}
+ />
+ <label for="currentPassword">Passwort bestätigen</label>
+ </div>
+ <button
+ class="btn btn-danger"
+ onclick={() => {
+ showConfirmDeleteAccount = true;
+ }}
+ data-sveltekit-noscroll
+ >
+ Konto löschen
+ {#if isDeletingAccount}
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
+ <div class="spinner-border" role="status">
+ <span class="visually-hidden">Loading...</span>
+ </div>
+ {/if}
+ </button>
+ </form>
+ {#if showDeleteAccountSuccess}
+ <div class="alert alert-success mt-2" role="alert" transition:slide>
+ Dein Konto wurde erfolgreich gelöscht!<br />
+ Du solltest jetzt eigentlich automatisch ausgeloggt werden. Falls nicht, dann logge
+ dich bitte sebst aus.
+ </div>
+ {/if}
+ {#if deleteAccountPasswordIncorrect}
+ <div class="alert alert-danger mt-2" role="alert" transition:slide>
+ Das eingegebene Passwort ist falsch!
+ </div>
+ {/if}
+ {#if showConfirmDeleteAccount}
+ <div class="alert alert-danger mt-2" role="alert" transition:slide>
+ Bist du dir sicher, dass du dein Konto löschen möchtest? Dies kann nicht
+ rückgängig gemacht werden!
+ <div class="d-flex flex-row mt-2">
+ <button
+ class="btn btn-secondary"
+ onclick={() => {
+ showConfirmDeleteAccount = false;
+ deleteAccountPassword = '';
+ }}>Abbrechen</button
+ >
+ <button
+ class="btn btn-danger ms-3"
+ onclick={deleteAccount}
+ disabled={isDeletingAccount}
+ >Löschen bestätigen
+ {#if isDeletingAccount}
+ <span
+ class="spinner-border spinner-border-sm ms-2"
+ role="status"
+ aria-hidden="true"
+ ></span>
+ {/if}
+ </button>
+ </div>
+ </div>
+ {/if}
+ </div>
</div>
<div id="about">
} else if (window.location.search.includes('error=401')) {
const toast = new bootstrap.Toast(document.getElementById('toastLoginInvalid'));
toast.show();
+ } else if (window.location.search.includes('error=410')) {
+ const toast = new bootstrap.Toast(document.getElementById('toastAccountDeleted'));
+ toast.show();
}
// check if registration is allowed
</div>
{/if}
- Fortschritt:
+ <u>Fortschritt:</u>
<div class="progress-item {active_phase >= 0 ? 'active' : ''}">
<div class="d-flex">
<div class="emoji">
<div class="toast-body">Authentifizierung fehlgeschlagen. Bitte neu anmelden.</div>
</div>
</div>
+
+ <div
+ id="toastAccountDeleted"
+ class="toast align-items-center text-bg-success"
+ role="alert"
+ aria-live="assertive"
+ aria-atomic="true"
+ >
+ <div class="d-flex">
+ <div class="toast-body">Account erfolgreich gelöscht.</div>
+ </div>
+ </div>
</div>
</div>