diff --git a/app/(protected)/account/page.tsx b/app/(protected)/account/page.tsx new file mode 100644 index 0000000..76a8c47 --- /dev/null +++ b/app/(protected)/account/page.tsx @@ -0,0 +1,13 @@ +import ChangePasswordForm from '@/components/account/change-password-form' + +export const metadata = { title: 'Account' } + +export default function AccountPage() { + return ( +
+

Account

+

Manage your password.

+ +
+ ) +} diff --git a/components/account/change-password-form.tsx b/components/account/change-password-form.tsx new file mode 100644 index 0000000..dfc8793 --- /dev/null +++ b/components/account/change-password-form.tsx @@ -0,0 +1,131 @@ +'use client' + +import { useState } from 'react' +import { createClient } from '@/lib/supabase/client' + +export default function ChangePasswordForm() { + const [current, setCurrent] = useState('') + const [newPass, setNewPass] = useState('') + const [confirm, setConfirm] = useState('') + const [loading, setLoading] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError(null) + setSuccess(false) + + // Client-side validation + if (!current || !newPass || !confirm) { + setError('All fields are required.') + return + } + if (newPass.length < 8) { + setError('Password must be at least 8 characters.') + return + } + if (newPass !== confirm) { + setError('Passwords do not match.') + return + } + if (newPass === current) { + setError('New password must differ from current password.') + return + } + + setLoading(true) + const supabase = createClient() + + // Get current user email for re-auth + const { data: { user } } = await supabase.auth.getUser() + if (!user?.email) { + setError('Session expired, please log in again.') + setLoading(false) + return + } + + // Re-authenticate with current password + const { error: reauthError } = await supabase.auth.signInWithPassword({ + email: user.email, + password: current, + }) + if (reauthError) { + setError('Current password is incorrect.') + setLoading(false) + return + } + + // Update to new password + const { error: updateError } = await supabase.auth.updateUser({ password: newPass }) + if (updateError) { + setError(updateError.message) + setLoading(false) + return + } + + setCurrent('') + setNewPass('') + setConfirm('') + setSuccess(true) + setLoading(false) + } + + return ( +
+

Change Password

+
+
+ + setCurrent(e.target.value)} + disabled={loading} + autoComplete="current-password" + className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50" + /> +
+
+ + setNewPass(e.target.value)} + disabled={loading} + autoComplete="new-password" + className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50" + /> +
+
+ + setConfirm(e.target.value)} + disabled={loading} + autoComplete="new-password" + className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50" + /> +
+ + {error &&

{error}

} + {success &&

Password changed successfully.

} + + +
+
+ ) +} diff --git a/components/layout/sidebar.tsx b/components/layout/sidebar.tsx index 4ccfef3..41d8510 100644 --- a/components/layout/sidebar.tsx +++ b/components/layout/sidebar.tsx @@ -58,6 +58,11 @@ const IconLogout = () => ( ) +const IconUser = () => ( + + + +) const NAV_ITEMS: Record = { reporter: [ @@ -104,7 +109,7 @@ function useNavItems(role: string): NavItem[] { function NavLink({ item, pathname }: { item: NavItem; pathname: string }) { // Exact match for root-level single-page roles, prefix match otherwise - const exactRoots = ['/reporter', '/supervisor', '/management', '/capa-owner', '/admin', '/report'] + const exactRoots = ['/reporter', '/supervisor', '/management', '/capa-owner', '/admin', '/report', '/account'] const isActive = exactRoots.includes(item.href) ? pathname === item.href : pathname.startsWith(item.href) @@ -170,6 +175,19 @@ export function Sidebar({ role, userName, userEmail }: SidebarProps) {

{roleLabel}

+ + + + + Account +