Fix avatar and display name update issues

- Remove avatar handling from profile update to prevent overwriting DB with display URL
- Skip ensureParent for .avatars to speed up upload
- Add change detection for display name save button
- Update API client to not send avatarUrl in profile update
This commit is contained in:
Leon Bösche
2026-01-29 22:03:36 +01:00
parent b30b8eb934
commit 36de8c2313
4 changed files with 18 additions and 16 deletions

View File

@@ -185,11 +185,9 @@ class ApiClient {
Future<Map<String, dynamic>> updateUserProfile({
required String displayName,
String? email,
String? avatarUrl,
}) async {
final data = <String, dynamic>{'displayName': displayName};
if (email != null) data['email'] = email;
if (avatarUrl != null) data['avatarUrl'] = avatarUrl;
return putRaw('/user/profile', data: data);
}

View File

@@ -24,6 +24,7 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
bool _isUploadingAvatar = false;
double _avatarUploadProgress = 0.0;
String? _error;
bool _hasChanges = false;
// Profile fields
late TextEditingController _displayNameController;
@@ -40,6 +41,11 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
void initState() {
super.initState();
_displayNameController = TextEditingController();
_displayNameController.addListener(() {
if (mounted && _currentUser != null) {
setState(() => _hasChanges = _displayNameController.text != (_currentUser!.displayName ?? ''));
}
});
_currentPasswordController = TextEditingController();
_newPasswordController = TextEditingController();
_confirmPasswordController = TextEditingController();
@@ -52,6 +58,7 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
_currentUser = authState.user;
_displayNameController.text = _currentUser?.displayName ?? '';
_avatarUrl = _currentUser?.avatarUrl;
_hasChanges = false;
}
}
});
@@ -145,6 +152,11 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
}
}
void _onSavePressed() {
if (!_hasChanges || _isLoading) return;
_updateProfile();
}
Future<void> _updateProfile() async {
if (_currentUser == null) {
return;
@@ -155,7 +167,6 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
final apiClient = GetIt.I<ApiClient>();
await apiClient.updateUserProfile(
displayName: _displayNameController.text,
avatarUrl: _avatarUrl,
);
final updatedUser = _currentUser!.copyWith(
@@ -641,7 +652,7 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
child: SizedBox(
width: 144,
child: ModernGlassButton(
onPressed: () => _updateProfile(),
onPressed: _onSavePressed,
isLoading: _isLoading,
child: _isLoading
? const SizedBox(

View File

@@ -3872,7 +3872,6 @@ func updateUserProfileHandler(w http.ResponseWriter, r *http.Request, db *databa
var req struct {
DisplayName *string `json:"displayName"`
Email *string `json:"email"`
AvatarURL *string `json:"avatarUrl"`
}
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
@@ -3895,11 +3894,6 @@ func updateUserProfileHandler(w http.ResponseWriter, r *http.Request, db *databa
args = append(args, *req.Email)
argIndex++
}
if req.AvatarURL != nil {
setParts = append(setParts, fmt.Sprintf("avatar_url = $%d", argIndex))
args = append(args, *req.AvatarURL)
argIndex++
}
if len(setParts) == 0 {
// No fields to update
@@ -3927,9 +3921,6 @@ func updateUserProfileHandler(w http.ResponseWriter, r *http.Request, db *databa
if req.Email != nil {
metadata["email"] = *req.Email
}
if req.AvatarURL != nil {
metadata["avatarUrl"] = *req.AvatarURL
}
auditLogger.Log(r.Context(), audit.Entry{
UserID: &userID,

View File

@@ -91,9 +91,11 @@ func (c *WebDAVClient) Upload(ctx context.Context, remotePath string, r io.Reade
if c == nil {
return fmt.Errorf("no webdav client configured")
}
// Ensure parent collections
if err := c.ensureParent(ctx, remotePath); err != nil {
return err
// Ensure parent collections, skip for .avatars as it should exist
if !strings.HasPrefix(remotePath, ".avatars/") {
if err := c.ensureParent(ctx, remotePath); err != nil {
return err
}
}
// Construct URL
// remotePath might be like /orgs/<id>/file.txt; ensure it joins to basePrefix