admin can delete old data (from before migration)
authorPhiTux <redacted>
Tue, 9 Sep 2025 15:43:42 +0000 (17:43 +0200)
committerPhiTux <redacted>
Tue, 9 Sep 2025 15:43:42 +0000 (17:43 +0200)
backend/handlers/admin.go
backend/main.go
frontend/src/lib/settings/Admin.svelte

index 9628d6a5600241061c8a69065de53c8f7037592c..f6058e2455c1051ae77be5ab6522d57b032cd3c3 100644 (file)
@@ -63,8 +63,11 @@ func validateAdminPasswordInRequest(r *http.Request) bool {
        return req.AdminPassword == adminPassword
 }
 
-// GetAllUsers returns all users with their disk usage
-func GetAllUsers(w http.ResponseWriter, r *http.Request) {
+// GetAdminData returns:
+// - all users with their disk usage
+// - free disk space
+// - migration-info
+func GetAdminData(w http.ResponseWriter, r *http.Request) {
        if !validateAdminPasswordInRequest(r) {
                http.Error(w, "Invalid admin password", http.StatusUnauthorized)
                return
@@ -115,10 +118,14 @@ func GetAllUsers(w http.ResponseWriter, r *http.Request) {
                freeSpace = 0 // Default to 0 if we can't determine free space
        }
 
+       // Check for old directory and get old users info
+       oldDirInfo := getOldDirectoryInfo()
+
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]any{
                "users":      adminUsers,
                "free_space": freeSpace,
+               "old_data":   oldDirInfo,
        })
 }
 
@@ -158,6 +165,59 @@ func getFreeDiskSpace() (int64, error) {
        return freeSpace, nil
 }
 
+// getOldDirectoryInfo checks for old directory and returns info about old users and directory size
+func getOldDirectoryInfo() map[string]any {
+       oldDirPath := filepath.Join(utils.Settings.DataPath, "old")
+
+       // Check if old directory exists
+       if _, err := os.Stat(oldDirPath); os.IsNotExist(err) {
+               return map[string]any{
+                       "exists": false,
+               }
+       }
+
+       // Read users.json from old directory
+       usersJsonPath := filepath.Join(oldDirPath, "users.json")
+       var oldUsernames []string
+
+       if _, err := os.Stat(usersJsonPath); err == nil {
+               // Read and parse users.json
+               data, err := os.ReadFile(usersJsonPath)
+               if err == nil {
+                       var usersData map[string]any
+                       if json.Unmarshal(data, &usersData) == nil {
+                               if usersList, ok := usersData["users"].([]any); ok {
+                                       for _, u := range usersList {
+                                               if user, ok := u.(map[string]any); ok {
+                                                       if username, ok := user["username"].(string); ok {
+                                                               oldUsernames = append(oldUsernames, username)
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               }
+       }
+
+       // Calculate total size of old directory
+       var totalSize int64
+       filepath.Walk(oldDirPath, func(path string, info os.FileInfo, err error) error {
+               if err != nil {
+                       return nil // Continue on errors
+               }
+               if !info.IsDir() {
+                       totalSize += info.Size()
+               }
+               return nil
+       })
+
+       return map[string]any{
+               "exists":     true,
+               "usernames":  oldUsernames,
+               "total_size": totalSize,
+       }
+}
+
 // DeleteUser deletes a user and all their data
 func DeleteUser(w http.ResponseWriter, r *http.Request) {
        var req struct {
@@ -194,3 +254,33 @@ func DeleteUser(w http.ResponseWriter, r *http.Request) {
                "success": true,
        })
 }
+
+// DeleteOldData deletes the entire old directory
+func DeleteOldData(w http.ResponseWriter, r *http.Request) {
+       if !validateAdminPasswordInRequest(r) {
+               http.Error(w, "Invalid admin password", http.StatusUnauthorized)
+               return
+       }
+
+       oldDirPath := filepath.Join(utils.Settings.DataPath, "old")
+
+       // Check if old directory exists
+       if _, err := os.Stat(oldDirPath); os.IsNotExist(err) {
+               http.Error(w, "Old directory does not exist", http.StatusNotFound)
+               return
+       }
+
+       // Remove the entire old directory
+       if err := os.RemoveAll(oldDirPath); err != nil {
+               log.Printf("Error deleting old directory: %v", err)
+               http.Error(w, "Error deleting old directory", http.StatusInternalServerError)
+               return
+       }
+
+       log.Printf("Old directory successfully deleted by admin (id: %d, username: %s)", r.Context().Value(utils.UserIDKey), r.Context().Value(utils.UsernameKey))
+
+       w.Header().Set("Content-Type", "application/json")
+       json.NewEncoder(w).Encode(map[string]bool{
+               "success": true,
+       })
+}
index c040b863b610100df3ee84e1de7b604cd85dff96..ed9bbc633cccead8de8d9eda65ab1fc22dac70ec 100644 (file)
@@ -91,9 +91,10 @@ func main() {
        mux.HandleFunc("GET /logs/exportData", middleware.RequireAuth(handlers.ExportData))
 
        // Admin routes
-       mux.HandleFunc("POST /admin/validate-password", handlers.ValidateAdminPassword)
-       mux.HandleFunc("POST /admin/users", handlers.GetAllUsers)
-       mux.HandleFunc("POST /admin/delete-user", handlers.DeleteUser)
+       mux.HandleFunc("POST /admin/validate-password", middleware.RequireAuth(handlers.ValidateAdminPassword))
+       mux.HandleFunc("POST /admin/get-data", middleware.RequireAuth(handlers.GetAdminData))
+       mux.HandleFunc("POST /admin/delete-user", middleware.RequireAuth(handlers.DeleteUser))
+       mux.HandleFunc("POST /admin/delete-old-data", middleware.RequireAuth(handlers.DeleteOldData))
 
        // Create a handler chain with Timeout, Logger and CORS middleware
        // Timeout middleware will be executed first, then Logger, then CORS
index 481667c8406436fc91356c26902e7de88534297b..f69090ebd4d08ad05a93491a9ec07fff523ec1f3 100644 (file)
 
        // Admin data
        let freeSpace = $state(0);
+       let oldData = $state({});
        let users = $state([]);
        let isLoadingUsers = $state(false);
        let deleteUserId = $state(null);
        let isDeletingUser = $state(false);
 
+       let confirmDeleteOldData = $state(false);
+       let isDeletingOldData = $state(false);
+
        onMount(() => {
                currentUser = localStorage.getItem('user');
                resetAdminState();
                isLoadingUsers = true;
 
                try {
-                       const response = await makeAdminApiCall('/admin/users');
+                       const response = await makeAdminApiCall('/admin/get-data');
                        users = response.data.users || [];
                        freeSpace = response.data.free_space;
+                       oldData = response.data.old_data;
                } catch (error) {
                        console.error('Error loading users:', error);
                        if (error.response?.status === 401) {
        function confirmDeleteUser(userId) {
                deleteUserId = deleteUserId === userId ? null : userId;
        }
+
+       // Delete old data directory
+       async function deleteOldData() {
+               if (isDeletingOldData) return;
+               isDeletingOldData = true;
+
+               try {
+                       const response = await makeAdminApiCall('/admin/delete-old-data');
+                       if (response.data.success) {
+                               // Reset old data state
+                               oldData = { exists: false };
+                               confirmDeleteOldData = false;
+
+                               // Show success toast
+                               const toast = new bootstrap.Toast(document.getElementById('toastSuccessOldDataDelete'));
+                               toast.show();
+                       } else {
+                               // Show error toast
+                               const toast = new bootstrap.Toast(document.getElementById('toastErrorOldDataDelete'));
+                               toast.show();
+                       }
+               } catch (error) {
+                       console.error('Error deleting old data:', error);
+                       if (error.response?.status === 401) {
+                               resetAdminState();
+                       } else {
+                               // Show error toast
+                               const toast = new bootstrap.Toast(document.getElementById('toastErrorOldDataDelete'));
+                               toast.show();
+                       }
+               } finally {
+                       isDeletingOldData = false;
+               }
+       }
+
+       function toggleDeleteOldDataConfirmation() {
+               confirmDeleteOldData = !confirmDeleteOldData;
+       }
 </script>
 
 <div class="settings-admin">
                                                        </div>
                                                </div>
                                        {/if}
+                               </div>
+                       </div>
 
-                                       <div class="mt-3">
-                                               <button class="btn btn-outline-primary" onclick={loadUsers} disabled={isLoadingUsers}>
-                                                       {#if isLoadingUsers}
-                                                               <span class="spinner-border spinner-border-sm me-2"></span>
+                       <!-- Old Data Card -->
+                       {#if oldData.exists}
+                               <div class="card mt-4">
+                                       <div class="card-header">
+                                               <h4 class="card-title mb-0">📦 {$t('settings.admin.old_data')}</h4>
+                                       </div>
+                                       <div class="card-body">
+                                               <p class="text-muted mb-3">
+                                                       {@html $t('settings.admin.old_data_description')}
+                                               </p>
+
+                                               {#if oldData.usernames && oldData.usernames.length > 0}
+                                                       <h6>{$t('settings.admin.old_users')}:</h6>
+                                                       <div class="mb-3">
+                                                               {#each oldData.usernames as username, index}
+                                                                       <span class="badge bg-secondary me-1">{username}</span>
+                                                               {/each}
+                                                       </div>
+                                               {:else}
+                                                       <p class="text-warning">
+                                                               {$t('settings.admin.no_old_users_found')}
+                                                       </p>
+                                               {/if}
+
+                                               <div class="row">
+                                                       <div class="col-md-6">
+                                                               <strong>{@html $t('settings.admin.old_data_size')}: </strong>
+                                                               {formatBytes(oldData.total_size)}
+                                                       </div>
+                                               </div>
+
+                                               <!-- Delete old data button -->
+                                               <div class="mt-3">
+                                                       <button
+                                                               class="btn btn-danger"
+                                                               onclick={toggleDeleteOldDataConfirmation}
+                                                               disabled={isDeletingOldData}
+                                                       >
+                                                               🗑️ {$t('settings.admin.delete_old_data')}
+                                                       </button>
+
+                                                       {#if confirmDeleteOldData}
+                                                               <div class="mt-3" transition:slide>
+                                                                       <div class="alert alert-danger">
+                                                                               <p class="mb-2">
+                                                                                       <strong>{$t('settings.admin.confirm_delete_old_data')}</strong><br />
+                                                                                       {@html $t('settings.admin.delete_old_data_warning')}
+                                                                               </p>
+                                                                               <div class="d-flex gap-2">
+                                                                                       <button
+                                                                                               class="btn btn-secondary btn-sm"
+                                                                                               onclick={toggleDeleteOldDataConfirmation}
+                                                                                       >
+                                                                                               {$t('settings.admin.cancel')}
+                                                                                       </button>
+                                                                                       <button
+                                                                                               class="btn btn-danger btn-sm"
+                                                                                               onclick={deleteOldData}
+                                                                                               disabled={isDeletingOldData}
+                                                                                       >
+                                                                                               {#if isDeletingOldData}
+                                                                                                       <span class="spinner-border spinner-border-sm me-1"></span>
+                                                                                               {/if}
+                                                                                               {$t('settings.admin.delete')}
+                                                                                       </button>
+                                                                               </div>
+                                                                       </div>
+                                                               </div>
                                                        {/if}
-                                                       {$t('settings.admin.refresh_users')}
-                                               </button>
+                                               </div>
                                        </div>
                                </div>
+                       {/if}
+
+                       <!-- Reload Button moved to bottom -->
+                       <div class="mt-4 d-flex justify-content-center">
+                               <button class="btn btn-outline-primary" onclick={loadUsers} disabled={isLoadingUsers}>
+                                       {#if isLoadingUsers}
+                                               <span class="spinner-border spinner-border-sm me-2"></span>
+                                       {/if}
+                                       {$t('settings.admin.refresh_users')}
+                               </button>
                        </div>
                </div>
        {/if}
                                <div class="toast-body">
                                        {$t('settings.statistics.toast_error_user_delete')}
                                </div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
+                       </div>
+               </div>
+
+               <div
+                       id="toastSuccessOldDataDelete"
+                       class="toast align-items-center text-bg-success"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">
+                                       {$t('settings.admin.toast_success_old_data_delete')}
+                               </div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
+                       </div>
+               </div>
+
+               <div
+                       id="toastErrorOldDataDelete"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">
+                                       {$t('settings.admin.toast_error_old_data_delete')}
+                               </div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
                        </div>
                </div>
        </div>
git clone https://git.99rst.org/PROJECT