Implement profile management (task 14)
- Add updateProfile and updatePassword server actions - Add getUserProfile function with sensitive data protection - Create ProfileForm component for name and email updates - Create PasswordChangeForm component with current password verification - Create profile page at /profile with all profile management features - Add profile and invitations navigation links to main page - Implement email uniqueness validation - Implement password hashing on password change Requirements: 7.1, 7.2, 7.3, 7.4, 7.5
This commit is contained in:
@@ -173,3 +173,157 @@ export async function isAuthenticated(): Promise<boolean> {
|
||||
const session = await getCurrentSession()
|
||||
return !!session?.user
|
||||
}
|
||||
|
||||
// Profile management schemas
|
||||
const updateProfileSchema = z.object({
|
||||
name: z.string().min(1, { message: 'Name is required' }).max(100, { message: 'Name must be less than 100 characters' }).optional(),
|
||||
email: z.string().email({ message: 'Invalid email address' }).optional(),
|
||||
})
|
||||
|
||||
const updatePasswordSchema = z.object({
|
||||
currentPassword: z.string().min(1, { message: 'Current password is required' }),
|
||||
newPassword: z.string().min(8, { message: 'New password must be at least 8 characters' }),
|
||||
})
|
||||
|
||||
export type UpdateProfileResult =
|
||||
| { success: true }
|
||||
| { success: false; error: string }
|
||||
|
||||
export type UpdatePasswordResult =
|
||||
| { success: true }
|
||||
| { success: false; error: string }
|
||||
|
||||
/**
|
||||
* Update user profile information
|
||||
* Requirements: 7.1, 7.4, 7.5
|
||||
*/
|
||||
export async function updateProfile(
|
||||
userId: string,
|
||||
data: { name?: string; email?: string }
|
||||
): Promise<UpdateProfileResult> {
|
||||
try {
|
||||
// Validate input
|
||||
const validation = updateProfileSchema.safeParse(data)
|
||||
|
||||
if (!validation.success) {
|
||||
const errors = validation.error.issues.map((issue) => issue.message).join(', ')
|
||||
return { success: false, error: errors }
|
||||
}
|
||||
|
||||
const { name, email } = validation.data
|
||||
|
||||
// Check if there's anything to update
|
||||
if (!name && !email) {
|
||||
return { success: false, error: 'No data to update' }
|
||||
}
|
||||
|
||||
// If email is being updated, check uniqueness (Requirement 7.4)
|
||||
if (email) {
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
})
|
||||
|
||||
// Check if email is already in use by another user
|
||||
if (existingUser && existingUser.id !== userId) {
|
||||
return { success: false, error: 'Email already in use' }
|
||||
}
|
||||
}
|
||||
|
||||
// Update user profile (Requirement 7.1)
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
...(name !== undefined && { name }),
|
||||
...(email !== undefined && { email }),
|
||||
},
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Profile update error:', error)
|
||||
|
||||
// Handle unique constraint violation
|
||||
if (error instanceof Error && error.message.includes('Unique constraint')) {
|
||||
return { success: false, error: 'Email already in use' }
|
||||
}
|
||||
|
||||
return { success: false, error: 'Profile update failed. Please try again.' }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user password
|
||||
* Requirements: 7.2, 7.3
|
||||
*/
|
||||
export async function updatePassword(
|
||||
userId: string,
|
||||
currentPassword: string,
|
||||
newPassword: string
|
||||
): Promise<UpdatePasswordResult> {
|
||||
try {
|
||||
// Validate input
|
||||
const validation = updatePasswordSchema.safeParse({ currentPassword, newPassword })
|
||||
|
||||
if (!validation.success) {
|
||||
const errors = validation.error.issues.map((issue) => issue.message).join(', ')
|
||||
return { success: false, error: errors }
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
})
|
||||
|
||||
if (!user || !user.passwordHash) {
|
||||
return { success: false, error: 'User not found' }
|
||||
}
|
||||
|
||||
// Verify current password (Requirement 7.2)
|
||||
const isCurrentPasswordValid = await bcrypt.compare(currentPassword, user.passwordHash)
|
||||
|
||||
if (!isCurrentPasswordValid) {
|
||||
return { success: false, error: 'Current password is incorrect' }
|
||||
}
|
||||
|
||||
// Hash new password (Requirement 7.3)
|
||||
const newPasswordHash = await bcrypt.hash(newPassword, 10)
|
||||
|
||||
// Update password
|
||||
await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: {
|
||||
passwordHash: newPasswordHash,
|
||||
},
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error('Password update error:', error)
|
||||
return { success: false, error: 'Password update failed. Please try again.' }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile (without sensitive data)
|
||||
* Requirement: 7.5 - Profile data exposure protection
|
||||
*/
|
||||
export async function getUserProfile(userId: string) {
|
||||
try {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
// Explicitly exclude passwordHash (Requirement 7.5)
|
||||
},
|
||||
})
|
||||
|
||||
return user
|
||||
} catch (error) {
|
||||
console.error('Get user profile error:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user