Refactor updateUserProfile method to require displayName and simplify data construction in ApiClient

Add GET route for user avatar retrieval and update CORS settings in routes.go
Implement getUserAvatarHandler to serve user avatars from storage
This commit is contained in:
Leon Bösche
2026-01-29 10:12:20 +01:00
parent 5a62121591
commit 11daed18d7
4 changed files with 75 additions and 16 deletions

View File

@@ -183,12 +183,11 @@ class ApiClient {
}
Future<Map<String, dynamic>> updateUserProfile({
String? displayName,
required String displayName,
String? email,
String? avatarUrl,
}) async {
final data = <String, dynamic>{};
if (displayName != null) data['displayName'] = displayName;
final data = <String, dynamic>{'displayName': displayName};
if (email != null) data['email'] = email;
if (avatarUrl != null) data['avatarUrl'] = avatarUrl;

View File

@@ -139,16 +139,12 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
try {
final apiClient = GetIt.I<ApiClient>();
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,
);

Binary file not shown.

View File

@@ -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())