feat: [US-041] - Change password for logged-in user
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
ff52df2c28
commit
e8a6942cfe
|
|
@ -0,0 +1,161 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { createClient } from '@/lib/supabase/client'
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
const [currentPassword, setCurrentPassword] = useState('')
|
||||||
|
const [newPassword, setNewPassword] = useState('')
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [success, setSuccess] = useState('')
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const handleChangePassword = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setError('')
|
||||||
|
setSuccess('')
|
||||||
|
|
||||||
|
if (newPassword !== confirmPassword) {
|
||||||
|
setError('New passwords do not match.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.length < 6) {
|
||||||
|
setError('New password must be at least 6 characters.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const supabase = createClient()
|
||||||
|
|
||||||
|
// Re-authenticate with current password
|
||||||
|
const { data: { user } } = await supabase.auth.getUser()
|
||||||
|
if (!user?.email) {
|
||||||
|
setError('Unable to verify current user.')
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { error: signInError } = await supabase.auth.signInWithPassword({
|
||||||
|
email: user.email,
|
||||||
|
password: currentPassword,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (signInError) {
|
||||||
|
setError('Current password is incorrect.')
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update to new password
|
||||||
|
const { error: updateError } = await supabase.auth.updateUser({
|
||||||
|
password: newPassword,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (updateError) {
|
||||||
|
setError(updateError.message)
|
||||||
|
setIsLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuccess('Password updated successfully.')
|
||||||
|
setCurrentPassword('')
|
||||||
|
setNewPassword('')
|
||||||
|
setConfirmPassword('')
|
||||||
|
} catch {
|
||||||
|
setError('An unexpected error occurred.')
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50 mb-8">
|
||||||
|
Settings
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="max-w-md">
|
||||||
|
<h2 className="text-lg font-semibold text-zinc-900 dark:text-zinc-50 mb-4">
|
||||||
|
Change Password
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<form onSubmit={handleChangePassword} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md bg-red-50 p-3 text-sm text-red-700 dark:bg-red-900/30 dark:text-red-400">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<div className="rounded-md bg-green-50 p-3 text-sm text-green-700 dark:bg-green-900/30 dark:text-green-400">
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="currentPassword"
|
||||||
|
className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1"
|
||||||
|
>
|
||||||
|
Current Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="currentPassword"
|
||||||
|
type="password"
|
||||||
|
value={currentPassword}
|
||||||
|
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full rounded-md border border-zinc-300 px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="newPassword"
|
||||||
|
className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1"
|
||||||
|
>
|
||||||
|
New Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="newPassword"
|
||||||
|
type="password"
|
||||||
|
value={newPassword}
|
||||||
|
onChange={(e) => setNewPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full rounded-md border border-zinc-300 px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="confirmPassword"
|
||||||
|
className="block text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-1"
|
||||||
|
>
|
||||||
|
Confirm New Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full rounded-md border border-zinc-300 px-3 py-2 text-sm text-zinc-900 placeholder-zinc-400 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-100 dark:placeholder-zinc-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed dark:focus:ring-offset-zinc-900"
|
||||||
|
>
|
||||||
|
{isLoading ? 'Updating...' : 'Update Password'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,12 @@ export default function Navbar({ userEmail, isAdmin }: NavbarProps) {
|
||||||
Invite User
|
Invite User
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
<Link
|
||||||
|
href="/dashboard/settings"
|
||||||
|
className="text-sm font-medium text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-200"
|
||||||
|
>
|
||||||
|
Settings
|
||||||
|
</Link>
|
||||||
<span className="text-sm text-zinc-600 dark:text-zinc-400">
|
<span className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
{userEmail}
|
{userEmail}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue