Refactor user profile update handler to support optional email field and dynamic query construction

This commit is contained in:
Leon Bösche
2026-01-27 08:47:21 +01:00
parent 8400e97d17
commit 425bfcb495
3 changed files with 78 additions and 60 deletions

View File

@@ -28,7 +28,6 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
// Profile fields
late TextEditingController _displayNameController;
late TextEditingController _emailController;
String? _avatarUrl;
String? _blurHash;
@@ -43,7 +42,6 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
void initState() {
super.initState();
_displayNameController = TextEditingController();
_emailController = TextEditingController();
_currentPasswordController = TextEditingController();
_newPasswordController = TextEditingController();
_confirmPasswordController = TextEditingController();
@@ -53,7 +51,6 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
@override
void dispose() {
_displayNameController.dispose();
_emailController.dispose();
_currentPasswordController.dispose();
_newPasswordController.dispose();
_confirmPasswordController.dispose();
@@ -65,7 +62,6 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
if (authState is AuthAuthenticated) {
_currentUser = authState.user;
_displayNameController.text = _currentUser?.displayName ?? '';
_emailController.text = _currentUser?.email ?? '';
_avatarUrl = _currentUser?.avatarUrl;
_blurHash = _currentUser?.blurHash;
}
@@ -135,7 +131,6 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
displayName: _displayNameController.text.isEmpty
? null
: _displayNameController.text,
email: _emailController.text,
avatarUrl: _avatarUrl,
blurHash: _blurHash,
);
@@ -144,7 +139,6 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
displayName: _displayNameController.text.isEmpty
? null
: _displayNameController.text,
email: _emailController.text,
avatarUrl: _avatarUrl,
blurHash: _blurHash,
);
@@ -340,6 +334,7 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
Center(
child: Column(
children: [
SizedBox(height: 16),
GestureDetector(
onTap: _pickAndUploadAvatar,
child: CircleAvatar(
@@ -397,41 +392,27 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
),
),
),
const SizedBox(height: 16),
const SizedBox(height: 24),
// Email
Text(
'Email',
style: TextStyle(
color: AppTheme.primaryText,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
style: TextStyle(color: AppTheme.primaryText),
decoration: InputDecoration(
hintText: 'Enter email',
hintStyle: TextStyle(color: AppTheme.secondaryText),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: AppTheme.secondaryText),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: AppTheme.secondaryText),
// Save Button
Center(
child: SizedBox(
width: 88,
child: ModernGlassButton(
onPressed: _updateProfile,
child: const Text('Save Changes'),
),
),
),
const SizedBox(height: 24),
// Save Button
// Logout Button
Center(
child: ModernGlassButton(
onPressed: _updateProfile,
child: const Text('Save Changes'),
child: IconButton(
onPressed: _logout,
icon: Icon(Icons.logout, color: AppTheme.errorColor),
tooltip: 'Logout',
iconSize: 28,
),
),
],
@@ -530,19 +511,12 @@ class _AccountSettingsDialogState extends State<AccountSettingsDialog> {
// Change Password Button
Center(
child: ModernGlassButton(
onPressed: _changePassword,
child: const Text('Change Password'),
),
),
const SizedBox(height: 24),
// Logout Button
Center(
child: TextButton(
onPressed: _logout,
style: TextButton.styleFrom(foregroundColor: AppTheme.errorColor),
child: const Text('Logout'),
child: SizedBox(
width: 88,
child: ModernGlassButton(
onPressed: _changePassword,
child: const Text('Change Password'),
),
),
),
],

Binary file not shown.

View File

@@ -3735,7 +3735,7 @@ func updateUserProfileHandler(w http.ResponseWriter, r *http.Request, db *databa
var req struct {
DisplayName *string `json:"displayName"`
Email string `json:"email"`
Email *string `json:"email"`
AvatarURL *string `json:"avatarUrl"`
BlurHash *string `json:"blurHash"`
}
@@ -3745,11 +3745,44 @@ func updateUserProfileHandler(w http.ResponseWriter, r *http.Request, db *databa
return
}
// Update user
_, err = db.ExecContext(r.Context(),
`UPDATE users SET display_name = $1, email = $2, avatar_url = $3, blur_hash = $4, updated_at = NOW()
WHERE id = $5`,
req.DisplayName, req.Email, req.AvatarURL, req.BlurHash, userID)
// Build dynamic update query
var setParts []string
var args []interface{}
argIndex := 1
if req.DisplayName != nil {
setParts = append(setParts, fmt.Sprintf("display_name = $%d", argIndex))
args = append(args, *req.DisplayName)
argIndex++
}
if req.Email != nil {
setParts = append(setParts, fmt.Sprintf("email = $%d", argIndex))
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 req.BlurHash != nil {
setParts = append(setParts, fmt.Sprintf("blur_hash = $%d", argIndex))
args = append(args, *req.BlurHash)
argIndex++
}
if len(setParts) == 0 {
// No fields to update
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"message": "No changes to update"})
return
}
setParts = append(setParts, "updated_at = NOW()")
query := fmt.Sprintf("UPDATE users SET %s WHERE id = $%d", strings.Join(setParts, ", "), argIndex)
args = append(args, userID)
_, err = db.ExecContext(r.Context(), query, args...)
if err != nil {
errors.LogError(r, err, "Failed to update user profile")
errors.WriteError(w, errors.CodeInternal, "Server error", http.StatusInternalServerError)
@@ -3757,14 +3790,25 @@ func updateUserProfileHandler(w http.ResponseWriter, r *http.Request, db *databa
}
// Audit log
metadata := make(map[string]interface{})
if req.DisplayName != nil {
metadata["displayName"] = *req.DisplayName
}
if req.Email != nil {
metadata["email"] = *req.Email
}
if req.AvatarURL != nil {
metadata["avatarUrl"] = *req.AvatarURL
}
if req.BlurHash != nil {
metadata["blurHash"] = *req.BlurHash
}
auditLogger.Log(r.Context(), audit.Entry{
UserID: &userID,
Action: "profile_update",
Success: true,
Metadata: map[string]interface{}{
"displayName": req.DisplayName,
"email": req.Email,
},
UserID: &userID,
Action: "profile_update",
Success: true,
Metadata: metadata,
})
w.WriteHeader(http.StatusOK)