diff --git a/b0esche_cloud/lib/services/api_client.dart b/b0esche_cloud/lib/services/api_client.dart index 026f086..22a73ca 100644 --- a/b0esche_cloud/lib/services/api_client.dart +++ b/b0esche_cloud/lib/services/api_client.dart @@ -183,12 +183,11 @@ class ApiClient { } Future> updateUserProfile({ - String? displayName, + required String displayName, String? email, String? avatarUrl, }) async { - final data = {}; - if (displayName != null) data['displayName'] = displayName; + final data = {'displayName': displayName}; if (email != null) data['email'] = email; if (avatarUrl != null) data['avatarUrl'] = avatarUrl; diff --git a/b0esche_cloud/lib/widgets/account_settings_dialog.dart b/b0esche_cloud/lib/widgets/account_settings_dialog.dart index 3ca05a2..a67a4c4 100644 --- a/b0esche_cloud/lib/widgets/account_settings_dialog.dart +++ b/b0esche_cloud/lib/widgets/account_settings_dialog.dart @@ -139,16 +139,12 @@ class _AccountSettingsDialogState extends State { try { final apiClient = GetIt.I(); await apiClient.updateUserProfile( - displayName: _displayNameController.text.isEmpty - ? null - : _displayNameController.text, + displayName: _displayNameController.text, avatarUrl: _avatarUrl, ); final updatedUser = _currentUser!.copyWith( - displayName: _displayNameController.text.isEmpty - ? null - : _displayNameController.text, + displayName: _displayNameController.text, avatarUrl: _avatarUrl, ); diff --git a/go_cloud/api b/go_cloud/api index 73569bd..752bb19 100755 Binary files a/go_cloud/api and b/go_cloud/api differ diff --git a/go_cloud/internal/http/routes.go b/go_cloud/internal/http/routes.go index 19af043..06da2ee 100644 --- a/go_cloud/internal/http/routes.go +++ b/go_cloud/internal/http/routes.go @@ -259,9 +259,12 @@ func NewRouter(cfg *config.Config, db *database.DB, jwtManager *jwt.Manager, aut r.Post("/user/avatar", func(w http.ResponseWriter, req *http.Request) { uploadUserAvatarHandler(w, req, db, auditLogger, cfg) }) + r.Get("/user/avatar", func(w http.ResponseWriter, req *http.Request) { + getUserAvatarHandler(w, req, db, cfg) + }) r.Options("/user/avatar", func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") w.WriteHeader(http.StatusOK) }) @@ -3843,6 +3846,11 @@ func getUserProfileHandler(w http.ResponseWriter, r *http.Request, db *database. return } + // If avatar exists, return the backend URL instead of the internal WebDAV URL + if user.AvatarURL != nil && *user.AvatarURL != "" { + user.AvatarURL = &[]string{"/user/avatar"}[0] + } + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(user) } @@ -4030,7 +4038,7 @@ func uploadUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *databas // Upload to Nextcloud client := storage.NewWebDAVClient(cfg) - avatarPath := fmt.Sprintf("avatars/%s", filename) + avatarPath := fmt.Sprintf("remote.php/dav/files/b0esche/avatars/%s", filename) err = client.Upload(r.Context(), avatarPath, bytes.NewReader(fileBytes), header.Size) if err != nil { errors.LogError(r, err, "Failed to upload avatar") @@ -4040,11 +4048,7 @@ func uploadUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *databas // Get public URL - for now, construct it manually since Nextcloud doesn't provide direct public URLs // In a real setup, you'd configure Nextcloud to serve public URLs or use a CDN - baseURL := cfg.NextcloudURL - if !strings.HasSuffix(baseURL, "/") { - baseURL += "/" - } - publicURL := fmt.Sprintf("%sindex.php/apps/files_sharing/public.php/webdav/avatars/%s", baseURL, filename) + publicURL := "/user/avatar" // Update user profile with avatar URL _, err = db.ExecContext(r.Context(), @@ -4074,6 +4078,66 @@ func uploadUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *databas }) } +// getUserAvatarHandler serves the user's avatar image +func getUserAvatarHandler(w http.ResponseWriter, r *http.Request, db *database.DB, cfg *config.Config) { + userIDStr, ok := middleware.GetUserID(r.Context()) + if !ok { + errors.WriteError(w, errors.CodeUnauthenticated, "Unauthorized", http.StatusUnauthorized) + return + } + + userID, err := uuid.Parse(userIDStr) + if err != nil { + errors.WriteError(w, errors.CodeInvalidArgument, "Invalid user ID", http.StatusBadRequest) + return + } + + var avatarURL *string + err = db.QueryRowContext(r.Context(), + `SELECT avatar_url FROM users WHERE id = $1`, userID). + Scan(&avatarURL) + if err != nil { + if err == sql.ErrNoRows { + errors.WriteError(w, errors.CodeNotFound, "User not found", http.StatusNotFound) + return + } + errors.LogError(r, err, "Failed to get user avatar") + errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError) + return + } + + if avatarURL == nil || *avatarURL == "" { + // No avatar, return 404 + w.WriteHeader(http.StatusNotFound) + return + } + + // Parse the avatar URL to get the remote path + // Assuming avatarURL is like https://storage.b0esche.cloud/remote.php/dav/files/b0esche/avatars/filename + baseURL := cfg.NextcloudURL + if !strings.HasSuffix(baseURL, "/") { + baseURL += "/" + } + remotePath := strings.TrimPrefix(*avatarURL, baseURL) + + // Download from WebDAV + client := storage.NewWebDAVClient(cfg) + resp, err := client.Download(r.Context(), remotePath, "") + if err != nil { + errors.LogError(r, err, "Failed to download avatar") + errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + // Copy headers + for k, v := range resp.Header { + w.Header()[k] = v + } + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) +} + // deleteUserAccountHandler handles user account deletion func deleteUserAccountHandler(w http.ResponseWriter, r *http.Request, db *database.DB, auditLogger *audit.Logger, cfg *config.Config) { userIDStr, ok := middleware.GetUserID(r.Context())