Skip to main content
PUT
/
user
curl -X PUT "http://localhost:8080/user" \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "newemail@example.com",
    "phone": "+1234567890",
    "password": "newpassword123",
    "data": {
      "first_name": "John",
      "last_name": "Doe",
      "avatar_url": "https://example.com/avatar.jpg"
    }
  }'
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "aud": "authenticated",
  "role": "authenticated",
  "email": "newemail@example.com",
  "email_confirmed_at": null,
  "phone": "+1234567890",
  "phone_confirmed_at": null,
  "confirmed_at": "2024-01-15T10:30:00Z",
  "last_sign_in_at": "2024-01-20T14:22:00Z",
  "app_metadata": {
    "provider": "email",
    "providers": ["email", "phone"]
  },
  "user_metadata": {
    "first_name": "John",
    "last_name": "Doe",
    "avatar_url": "https://example.com/avatar.jpg",
    "preferences": {
      "theme": "dark",
      "notifications": true
    }
  },
  "identities": [
    {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "user_id": "123e4567-e89b-12d3-a456-426614174000",
      "identity_data": {
        "email": "newemail@example.com",
        "sub": "123e4567-e89b-12d3-a456-426614174000"
      },
      "provider": "email",
      "last_sign_in_at": "2024-01-20T14:22:00Z",
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-20T16:45:00Z"
    }
  ],
  "created_at": "2024-01-15T10:30:00Z",
  "updated_at": "2024-01-20T16:45:00Z"
}
Update the profile information for the currently authenticated user. This endpoint allows users to modify their personal information, preferences, and user metadata.
This endpoint requires authentication. Include the Bearer token in the Authorization header.
curl -X PUT "http://localhost:8080/user" \
  -H "Authorization: Bearer your_access_token_here" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "newemail@example.com",
    "phone": "+1234567890",
    "password": "newpassword123",
    "data": {
      "first_name": "John",
      "last_name": "Doe",
      "avatar_url": "https://example.com/avatar.jpg"
    }
  }'

Request Body

email
string
New email address. If changed, email verification will be required.
phone
string
New phone number in E.164 format. If changed, phone verification will be required.
password
string
New password. Must meet security requirements (minimum 8 characters).
data
object
User metadata object containing profile information and preferences.
nonce
string
Optional nonce for additional security (prevents replay attacks).

Response

Returns the updated user object with the same structure as the GET /user endpoint.
id
string
Unique user identifier (UUID)
email
string
Updated email address
phone
string
Updated phone number
user_metadata
object
Updated user metadata
updated_at
string
ISO timestamp when user account was last updated
{
  "id": "123e4567-e89b-12d3-a456-426614174000",
  "aud": "authenticated",
  "role": "authenticated",
  "email": "newemail@example.com",
  "email_confirmed_at": null,
  "phone": "+1234567890",
  "phone_confirmed_at": null,
  "confirmed_at": "2024-01-15T10:30:00Z",
  "last_sign_in_at": "2024-01-20T14:22:00Z",
  "app_metadata": {
    "provider": "email",
    "providers": ["email", "phone"]
  },
  "user_metadata": {
    "first_name": "John",
    "last_name": "Doe",
    "avatar_url": "https://example.com/avatar.jpg",
    "preferences": {
      "theme": "dark",
      "notifications": true
    }
  },
  "identities": [
    {
      "id": "123e4567-e89b-12d3-a456-426614174000",
      "user_id": "123e4567-e89b-12d3-a456-426614174000",
      "identity_data": {
        "email": "newemail@example.com",
        "sub": "123e4567-e89b-12d3-a456-426614174000"
      },
      "provider": "email",
      "last_sign_in_at": "2024-01-20T14:22:00Z",
      "created_at": "2024-01-15T10:30:00Z",
      "updated_at": "2024-01-20T16:45:00Z"
    }
  ],
  "created_at": "2024-01-15T10:30:00Z",
  "updated_at": "2024-01-20T16:45:00Z"
}

Error Responses

{
  "code": 400,
  "msg": "Invalid email format",
  "details": "Please provide a valid email address"
}

Implementation Examples

React Profile Edit Form

import { useState, useEffect } from 'react';

function ProfileEditForm({ user, onUpdate }) {
  const [formData, setFormData] = useState({
    email: '',
    phone: '',
    password: '',
    confirmPassword: '',
    first_name: '',
    last_name: '',
    avatar_url: '',
    bio: '',
    website: '',
    location: '',
    preferences: {
      theme: 'light',
      notifications: true,
      language: 'en'
    }
  });
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');
  const [success, setSuccess] = useState('');

  useEffect(() => {
    if (user) {
      setFormData({
        email: user.email || '',
        phone: user.phone || '',
        password: '',
        confirmPassword: '',
        first_name: user.user_metadata?.first_name || '',
        last_name: user.user_metadata?.last_name || '',
        avatar_url: user.user_metadata?.avatar_url || '',
        bio: user.user_metadata?.bio || '',
        website: user.user_metadata?.website || '',
        location: user.user_metadata?.location || '',
        preferences: {
          theme: user.user_metadata?.preferences?.theme || 'light',
          notifications: user.user_metadata?.preferences?.notifications ?? true,
          language: user.user_metadata?.preferences?.language || 'en'
        }
      });
    }
  }, [user]);

  const handleInputChange = (field, value) => {
    if (field.includes('.')) {
      const [parent, child] = field.split('.');
      setFormData(prev => ({
        ...prev,
        [parent]: {
          ...prev[parent],
          [child]: value
        }
      }));
    } else {
      setFormData(prev => ({
        ...prev,
        [field]: value
      }));
    }
  };

  const validateForm = () => {
    if (formData.password && formData.password !== formData.confirmPassword) {
      setError('Passwords do not match');
      return false;
    }

    if (formData.password && formData.password.length < 8) {
      setError('Password must be at least 8 characters');
      return false;
    }

    if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
      setError('Please enter a valid email address');
      return false;
    }

    if (formData.phone && !/^\+[1-9]\d{1,14}$/.test(formData.phone)) {
      setError('Please enter a valid phone number in E.164 format');
      return false;
    }

    return true;
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    if (!validateForm()) {
      return;
    }

    setLoading(true);
    setError('');
    setSuccess('');

    try {
      const token = localStorage.getItem('access_token');
      
      const updateData = {
        email: formData.email,
        phone: formData.phone,
        data: {
          first_name: formData.first_name,
          last_name: formData.last_name,
          avatar_url: formData.avatar_url,
          bio: formData.bio,
          website: formData.website,
          location: formData.location,
          preferences: formData.preferences
        }
      };

      // Only include password if it's being changed
      if (formData.password) {
        updateData.password = formData.password;
      }

      const response = await fetch('/api/auth/user', {
        method: 'PUT',
        headers: {
          'Authorization': `Bearer ${token}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(updateData),
      });

      if (!response.ok) {
        const errorData = await response.json();
        throw new Error(errorData.msg || 'Failed to update profile');
      }

      const updatedUser = await response.json();
      
      setSuccess('Profile updated successfully!');
      onUpdate?.(updatedUser);
      
      // Clear password fields
      setFormData(prev => ({
        ...prev,
        password: '',
        confirmPassword: ''
      }));
      
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="profile-edit-form">
      <h2>Edit Profile</h2>
      
      {error && (
        <div className="error-message">
          {error}
        </div>
      )}
      
      {success && (
        <div className="success-message">
          {success}
        </div>
      )}

      <div className="form-section">
        <h3>Basic Information</h3>
        
        <div className="form-row">
          <div className="form-group">
            <label htmlFor="first_name">First Name</label>
            <input
              type="text"
              id="first_name"
              value={formData.first_name}
              onChange={(e) => handleInputChange('first_name', e.target.value)}
            />
          </div>
          
          <div className="form-group">
            <label htmlFor="last_name">Last Name</label>
            <input
              type="text"
              id="last_name"
              value={formData.last_name}
              onChange={(e) => handleInputChange('last_name', e.target.value)}
            />
          </div>
        </div>

        <div className="form-group">
          <label htmlFor="email">Email Address</label>
          <input
            type="email"
            id="email"
            value={formData.email}
            onChange={(e) => handleInputChange('email', e.target.value)}
          />
          <small>Changing your email will require verification</small>
        </div>

        <div className="form-group">
          <label htmlFor="phone">Phone Number</label>
          <input
            type="tel"
            id="phone"
            value={formData.phone}
            onChange={(e) => handleInputChange('phone', e.target.value)}
            placeholder="+1234567890"
          />
          <small>Use E.164 format (e.g., +1234567890)</small>
        </div>

        <div className="form-group">
          <label htmlFor="avatar_url">Avatar URL</label>
          <input
            type="url"
            id="avatar_url"
            value={formData.avatar_url}
            onChange={(e) => handleInputChange('avatar_url', e.target.value)}
            placeholder="https://example.com/avatar.jpg"
          />
        </div>
      </div>

      <div className="form-section">
        <h3>Additional Information</h3>
        
        <div className="form-group">
          <label htmlFor="bio">Bio</label>
          <textarea
            id="bio"
            value={formData.bio}
            onChange={(e) => handleInputChange('bio', e.target.value)}
            rows={3}
            placeholder="Tell us about yourself..."
          />
        </div>

        <div className="form-group">
          <label htmlFor="website">Website</label>
          <input
            type="url"
            id="website"
            value={formData.website}
            onChange={(e) => handleInputChange('website', e.target.value)}
            placeholder="https://yourwebsite.com"
          />
        </div>

        <div className="form-group">
          <label htmlFor="location">Location</label>
          <input
            type="text"
            id="location"
            value={formData.location}
            onChange={(e) => handleInputChange('location', e.target.value)}
            placeholder="City, Country"
          />
        </div>
      </div>

      <div className="form-section">
        <h3>Preferences</h3>
        
        <div className="form-group">
          <label htmlFor="theme">Theme</label>
          <select
            id="theme"
            value={formData.preferences.theme}
            onChange={(e) => handleInputChange('preferences.theme', e.target.value)}
          >
            <option value="light">Light</option>
            <option value="dark">Dark</option>
            <option value="auto">Auto</option>
          </select>
        </div>

        <div className="form-group">
          <label htmlFor="language">Language</label>
          <select
            id="language"
            value={formData.preferences.language}
            onChange={(e) => handleInputChange('preferences.language', e.target.value)}
          >
            <option value="en">English</option>
            <option value="es">Spanish</option>
            <option value="fr">French</option>
            <option value="de">German</option>
          </select>
        </div>

        <div className="form-group">
          <label className="checkbox-label">
            <input
              type="checkbox"
              checked={formData.preferences.notifications}
              onChange={(e) => handleInputChange('preferences.notifications', e.target.checked)}
            />
            Enable email notifications
          </label>
        </div>
      </div>

      <div className="form-section">
        <h3>Change Password</h3>
        <p className="section-description">
          Leave blank to keep your current password
        </p>
        
        <div className="form-group">
          <label htmlFor="password">New Password</label>
          <input
            type="password"
            id="password"
            value={formData.password}
            onChange={(e) => handleInputChange('password', e.target.value)}
            minLength={8}
          />
          <small>
            Password must be at least 8 characters with uppercase, lowercase, number, and special character
          </small>
        </div>

        <div className="form-group">
          <label htmlFor="confirmPassword">Confirm New Password</label>
          <input
            type="password"
            id="confirmPassword"
            value={formData.confirmPassword}
            onChange={(e) => handleInputChange('confirmPassword', e.target.value)}
          />
        </div>
      </div>

      <div className="form-actions">
        <button type="submit" disabled={loading} className="save-button">
          {loading ? 'Saving...' : 'Save Changes'}
        </button>
        
        <button 
          type="button" 
          onClick={() => window.history.back()}
          className="cancel-button"
        >
          Cancel
        </button>
      </div>
    </form>
  );
}

export default ProfileEditForm;

React Avatar Upload Component

import { useState } from 'react';

function AvatarUpload({ currentAvatar, onAvatarUpdate }) {
  const [uploading, setUploading] = useState(false);
  const [preview, setPreview] = useState(currentAvatar);

  const handleFileSelect = async (event) => {
    const file = event.target.files[0];
    if (!file) return;

    // Validate file type
    if (!file.type.startsWith('image/')) {
      alert('Please select an image file');
      return;
    }

    // Validate file size (max 5MB)
    if (file.size > 5 * 1024 * 1024) {
      alert('File size must be less than 5MB');
      return;
    }

    setUploading(true);

    try {
      // Create preview
      const previewUrl = URL.createObjectURL(file);
      setPreview(previewUrl);

      // Upload to your storage service (e.g., AWS S3, Cloudinary)
      const formData = new FormData();
      formData.append('avatar', file);

      const uploadResponse = await fetch('/api/upload/avatar', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
        },
        body: formData,
      });

      if (!uploadResponse.ok) {
        throw new Error('Failed to upload avatar');
      }

      const { url } = await uploadResponse.json();

      // Update user profile with new avatar URL
      const updateResponse = await fetch('/api/auth/user', {
        method: 'PUT',
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('access_token')}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          data: {
            avatar_url: url
          }
        }),
      });

      if (!updateResponse.ok) {
        throw new Error('Failed to update profile');
      }

      const updatedUser = await updateResponse.json();
      onAvatarUpdate?.(url, updatedUser);

      // Clean up preview URL
      URL.revokeObjectURL(previewUrl);
      
    } catch (error) {
      console.error('Avatar upload error:', error);
      alert('Failed to upload avatar. Please try again.');
      setPreview(currentAvatar); // Reset preview
    } finally {
      setUploading(false);
    }
  };

  return (
    <div className="avatar-upload">
      <div className="avatar-preview">
        {preview ? (
          <img src={preview} alt="Avatar" className="avatar-image" />
        ) : (
          <div className="avatar-placeholder">
            <span>No Image</span>
          </div>
        )}
        
        {uploading && (
          <div className="upload-overlay">
            <div className="spinner"></div>
            <span>Uploading...</span>
          </div>
        )}
      </div>

      <div className="avatar-controls">
        <label htmlFor="avatar-input" className="upload-button">
          {uploading ? 'Uploading...' : 'Change Avatar'}
        </label>
        
        <input
          id="avatar-input"
          type="file"
          accept="image/*"
          onChange={handleFileSelect}
          disabled={uploading}
          style={{ display: 'none' }}
        />

        {preview && preview !== currentAvatar && (
          <button
            onClick={() => setPreview(currentAvatar)}
            className="cancel-button"
            disabled={uploading}
          >
            Cancel
          </button>
        )}
      </div>

      <p className="upload-hint">
        Supported formats: JPG, PNG, GIF. Max size: 5MB.
      </p>
    </div>
  );
}

export default AvatarUpload;

Node.js Backend Handler

const express = require('express');
const { body, validationResult } = require('express-validator');
const { authenticateToken } = require('../middleware/auth');

const router = express.Router();

router.put('/user', [
  authenticateToken,
  body('email').optional().isEmail().normalizeEmail(),
  body('phone').optional().matches(/^\+[1-9]\d{1,14}$/),
  body('password').optional().isLength({ min: 8 }),
  body('data').optional().isObject()
], async (req, res) => {
  try {
    // Check validation errors
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({
        code: 400,
        msg: 'Invalid request data',
        details: errors.array()
      });
    }

    const { email, phone, password, data, nonce } = req.body;
    const userId = req.user.sub;

    // Validate password strength if provided
    if (password && !isStrongPassword(password)) {
      return res.status(422).json({
        code: 422,
        msg: 'Password too weak',
        details: 'Password must be at least 8 characters with uppercase, lowercase, number, and special character'
      });
    }

    // Call Strike Auth Service
    const response = await fetch(`${process.env.AUTH_SERVICE_URL}/user`, {
      method: 'PUT',
      headers: {
        'Authorization': req.headers.authorization,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        email,
        phone,
        password,
        data,
        nonce
      }),
    });

    const result = await response.json();

    if (!response.ok) {
      return res.status(response.status).json(result);
    }

    // Log profile update
    console.log(`User profile updated: ${userId} (${result.email})`);

    // Send verification emails if email/phone changed
    if (email && email !== req.user.email) {
      console.log(`Email change requested for user ${userId}: ${req.user.email} -> ${email}`);
    }

    if (phone && phone !== req.user.phone) {
      console.log(`Phone change requested for user ${userId}: ${req.user.phone} -> ${phone}`);
    }

    res.json(result);

  } catch (error) {
    console.error('Update user profile error:', error);
    res.status(500).json({
      code: 500,
      msg: 'Internal server error',
      details: 'Failed to update user profile'
    });
  }
});

function isStrongPassword(password) {
  // At least 8 characters, 1 uppercase, 1 lowercase, 1 number, 1 special char
  const strongPasswordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
  return strongPasswordRegex.test(password);
}

module.exports = router;

Verification Requirements

When updating email or phone number, verification is required:

Email Change Flow

1

Update Request

User submits new email address via PUT /user
2

Verification Email

System sends verification email to new address
3

Email Confirmation

User clicks verification link in email
4

Email Confirmed

New email is confirmed and becomes active

Phone Change Flow

1

Update Request

User submits new phone number via PUT /user
2

OTP Sent

System sends OTP to new phone number
3

OTP Verification

User enters OTP code for verification
4

Phone Confirmed

New phone number is confirmed and becomes active

Security Features

  • Authentication Required: All updates require valid JWT token
  • Email Verification: New email addresses must be verified
  • Phone Verification: New phone numbers must be verified
  • Password Strength: Enforced password complexity requirements
  • Rate Limiting: Prevents abuse of profile updates
  • Audit Logging: All profile changes are logged

Best Practices

  • Validate all input data on both client and server
  • Sanitize user inputs to prevent XSS attacks
  • Implement proper email and phone format validation
  • Enforce strong password requirements
  • Limit metadata size to prevent abuse
  • Provide real-time validation feedback
  • Show clear success and error messages
  • Implement auto-save for non-critical fields
  • Allow partial updates without requiring all fields
  • Provide preview functionality for changes
  • Always verify email and phone changes
  • Log all profile modifications for audit trails
  • Implement rate limiting for update requests
  • Use HTTPS for all profile update requests
  • Consider implementing change confirmation for sensitive updates

Testing

Unit Tests

describe('PUT /user', () => {
  test('should update user profile successfully', async () => {
    const token = await getValidAccessToken();
    
    const updateData = {
      data: {
        first_name: 'Updated',
        last_name: 'Name',
        preferences: {
          theme: 'dark'
        }
      }
    };

    const response = await request(app)
      .put('/user')
      .set('Authorization', `Bearer ${token}`)
      .send(updateData)
      .expect(200);

    expect(response.body.user_metadata.first_name).toBe('Updated');
    expect(response.body.user_metadata.last_name).toBe('Name');
    expect(response.body.user_metadata.preferences.theme).toBe('dark');
  });

  test('should require authentication', async () => {
    await request(app)
      .put('/user')
      .send({ data: { first_name: 'Test' } })
      .expect(401);
  });

  test('should validate email format', async () => {
    const token = await getValidAccessToken();
    
    await request(app)
      .put('/user')
      .set('Authorization', `Bearer ${token}`)
      .send({ email: 'invalid-email' })
      .expect(400);
  });

  test('should validate phone format', async () => {
    const token = await getValidAccessToken();
    
    await request(app)
      .put('/user')
      .set('Authorization', `Bearer ${token}`)
      .send({ phone: '1234567890' }) // Missing +
      .expect(400);
  });

  test('should enforce password strength', async () => {
    const token = await getValidAccessToken();
    
    await request(app)
      .put('/user')
      .set('Authorization', `Bearer ${token}`)
      .send({ password: 'weak' })
      .expect(422);
  });
});
I