; locked: boolean }) {
- const label = tier === 'admiral' ? 'ADMIRAL' : 'SKIPPER';
- if (locked) {
- return (
-
-
- {label}
-
- );
- }
- return (
-
- {label}
-
- );
-}
diff --git a/frontend/src/components/UserProfileDropdown.tsx b/frontend/src/components/UserProfileDropdown.tsx
index 6f9e2780..7581d35a 100644
--- a/frontend/src/components/UserProfileDropdown.tsx
+++ b/frontend/src/components/UserProfileDropdown.tsx
@@ -1,4 +1,5 @@
import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
import {
Settings,
LogOut,
@@ -28,7 +29,6 @@ type Theme = 'light' | 'dark' | 'auto';
interface UserProfileDropdownProps {
theme: Theme;
setTheme: (theme: Theme) => void;
- onOpenSettings: () => void;
}
const THEME_OPTIONS = [
@@ -48,7 +48,8 @@ function getInitials(username: string | undefined): string {
return trimmed.slice(0, 2).toUpperCase();
}
-export function UserProfileDropdown({ theme, setTheme, onOpenSettings }: UserProfileDropdownProps) {
+export function UserProfileDropdown({ theme, setTheme }: UserProfileDropdownProps) {
+ const navigate = useNavigate();
const { logout, user, isAdmin } = useAuth();
const { license } = useLicense();
const [billingLoading, setBillingLoading] = useState(false);
@@ -131,7 +132,7 @@ export function UserProfileDropdown({ theme, setTheme, onOpenSettings }: UserPro
{/* Navigation strip */}
-
+ navigate('/settings')} />
{showBilling ? (
void;
-}
-
function StatusBadge({ value, locked, requiredTier }: {
value: string;
locked?: boolean;
@@ -94,10 +91,11 @@ function SkeletonRow() {
);
}
-export function ConfigurationStatus({ onOpenSettings }: ConfigurationStatusProps) {
+export function ConfigurationStatus() {
+ const navigate = useNavigate();
const { status, loading } = useConfigurationStatus();
- const open = (section: SectionId) => () => onOpenSettings?.(section);
+ const open = (section: SectionId) => () => navigate(`/settings/${section}`);
if (loading) {
return (
diff --git a/frontend/src/components/settings/AccountSection.tsx b/frontend/src/components/settings/AccountSection.tsx
index af4571a1..0cdcee17 100644
--- a/frontend/src/components/settings/AccountSection.tsx
+++ b/frontend/src/components/settings/AccountSection.tsx
@@ -12,13 +12,6 @@ import { MfaBackupCodesDialog } from '@/components/mfa/MfaBackupCodesDialog';
import { toast } from '@/components/ui/toast-store';
import { apiFetch } from '@/lib/api';
-interface AccountSectionProps {
- authData: { oldPassword: string; newPassword: string; confirmPassword: string };
- onAuthDataChange: (data: { oldPassword: string; newPassword: string; confirmPassword: string }) => void;
- onPasswordChange: () => Promise;
- isSaving: boolean;
-}
-
interface MfaStatus {
enabled: boolean;
backupCodesRemaining: number;
@@ -30,7 +23,10 @@ interface SSOProvider {
type: 'ldap' | 'oidc';
}
-export function AccountSection({ authData, onAuthDataChange, onPasswordChange, isSaving }: AccountSectionProps) {
+export function AccountSection() {
+ const [authData, setAuthData] = useState({ oldPassword: '', newPassword: '', confirmPassword: '' });
+ const [isSaving, setIsSaving] = useState(false);
+
const [mfa, setMfa] = useState(null);
const [mfaLoading, setMfaLoading] = useState(true);
const [hasSso, setHasSso] = useState(false);
@@ -59,6 +55,39 @@ export function AccountSection({ authData, onAuthDataChange, onPasswordChange, i
.catch(() => setHasSso(false));
}, [refreshMfa]);
+ const handlePasswordChange = async () => {
+ if (!authData.oldPassword || !authData.newPassword || !authData.confirmPassword) {
+ toast.error('All fields are required');
+ return;
+ }
+ if (authData.newPassword !== authData.confirmPassword) {
+ toast.error('New passwords do not match');
+ return;
+ }
+ if (authData.newPassword.length < 8) {
+ toast.error('New password must be at least 8 characters');
+ return;
+ }
+ setIsSaving(true);
+ try {
+ const res = await apiFetch('/auth/password', {
+ method: 'PUT',
+ body: JSON.stringify({ oldPassword: authData.oldPassword, newPassword: authData.newPassword }),
+ });
+ if (res.ok) {
+ toast.success('Password updated successfully');
+ setAuthData({ oldPassword: '', newPassword: '', confirmPassword: '' });
+ } else {
+ const data = await res.json().catch(() => ({}));
+ toast.error(data?.error || 'Failed to update password');
+ }
+ } catch (e: unknown) {
+ toast.error((e as Error)?.message || 'Network error during password change');
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
const handleBypassToggle = async (enforce: boolean) => {
setTogglingBypass(true);
try {
@@ -89,7 +118,7 @@ export function AccountSection({ authData, onAuthDataChange, onPasswordChange, i
onAuthDataChange({ ...authData, oldPassword: e.target.value })}
+ onChange={(e) => setAuthData({ ...authData, oldPassword: e.target.value })}
/>
@@ -97,7 +126,7 @@ export function AccountSection({ authData, onAuthDataChange, onPasswordChange, i
onAuthDataChange({ ...authData, newPassword: e.target.value })}
+ onChange={(e) => setAuthData({ ...authData, newPassword: e.target.value })}
/>
@@ -105,10 +134,10 @@ export function AccountSection({ authData, onAuthDataChange, onPasswordChange, i
onAuthDataChange({ ...authData, confirmPassword: e.target.value })}
+ onChange={(e) => setAuthData({ ...authData, confirmPassword: e.target.value })}
/>
-