This endpoint requires authentication. Include the Bearer token in the Authorization header.
Copy
Ask AI
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
New email address. If changed, email verification will be required.
New phone number in E.164 format. If changed, phone verification will be required.
New password. Must meet security requirements (minimum 8 characters).
User metadata object containing profile information and preferences.
Optional nonce for additional security (prevents replay attacks).
Response
Returns the updated user object with the same structure as the GET /user endpoint.Unique user identifier (UUID)
Updated email address
Updated phone number
Updated user metadata
ISO timestamp when user account was last updated
Copy
Ask AI
{
"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
Copy
Ask AI
{
"code": 400,
"msg": "Invalid email format",
"details": "Please provide a valid email address"
}
Implementation Examples
React Profile Edit Form
Copy
Ask AI
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
Copy
Ask AI
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
Copy
Ask AI
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
Data Validation
Data Validation
- 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
User Experience
User Experience
- 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
Security
Security
- 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
Copy
Ask AI
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);
});
});