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 + + + + Current 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" + /> + + + + New Password + + 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" + /> + + + + Confirm New Password + + 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.} + + + {loading ? 'Saving…' : 'Update Password'} + + + + ) +} 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 + {mobileItems.map(item => { - 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) diff --git a/middleware.ts b/middleware.ts index 63bad33..e91fce0 100644 --- a/middleware.ts +++ b/middleware.ts @@ -25,7 +25,7 @@ export async function middleware(request: NextRequest) { const { data: { user } } = await supabase.auth.getUser() const { pathname } = request.nextUrl const isPublicRoute = pathname.startsWith('/login') || pathname.startsWith('/auth') || pathname.startsWith('/api/cron') - const isSharedRoute = pathname.startsWith('/report') + const isSharedRoute = pathname.startsWith('/report') || pathname.startsWith('/account') if (!user && !isPublicRoute) { const redirectUrl = request.nextUrl.clone()
Manage your password.
{error}
Password changed successfully.
{roleLabel}