curl -X PUT "http://localhost:8080/user" \
-H "Authorization: Bearer your_access_token_here" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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 authenticated user profile information
curl -X PUT "http://localhost:8080/user" \
-H "Authorization: Bearer your_access_token_here" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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"
}
curl -X PUT "http://localhost:8080/user" \
-H "Authorization: Bearer your_access_token_here" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"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": "[email protected]",
"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": "[email protected]",
"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"
}
{
"code": 400,
"msg": "Invalid email format",
"details": "Please provide a valid email address"
}
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;
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;
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;
Update Request
Verification Email
Email Confirmation
Email Confirmed
Update Request
OTP Sent
OTP Verification
Phone Confirmed
Data Validation
User Experience
Security
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);
});
});