diff --git a/maternal-web/app/settings/page.tsx b/maternal-web/app/settings/page.tsx
index ec553f0..7e157dc 100644
--- a/maternal-web/app/settings/page.tsx
+++ b/maternal-web/app/settings/page.tsx
@@ -9,6 +9,7 @@ import { ProtectedRoute } from '@/components/common/ProtectedRoute';
import { usersApi } from '@/lib/api/users';
import { MFASettings } from '@/components/settings/MFASettings';
import { SessionsManagement } from '@/components/settings/SessionsManagement';
+import { DeviceTrustManagement } from '@/components/settings/DeviceTrustManagement';
import { motion } from 'framer-motion';
export default function SettingsPage() {
@@ -241,11 +242,22 @@ export default function SettingsPage() {
+ {/* Device Trust Management */}
+
+
+
+
+
+
{/* Account Actions */}
diff --git a/maternal-web/components/settings/DeviceTrustManagement.tsx b/maternal-web/components/settings/DeviceTrustManagement.tsx
new file mode 100644
index 0000000..fe36679
--- /dev/null
+++ b/maternal-web/components/settings/DeviceTrustManagement.tsx
@@ -0,0 +1,340 @@
+'use client';
+
+import { useState, useEffect } from 'react';
+import {
+ Box,
+ Typography,
+ Card,
+ CardContent,
+ Button,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Alert,
+ CircularProgress,
+ Chip,
+ List,
+ ListItem,
+ ListItemText,
+ ListItemSecondaryAction,
+ IconButton,
+ Divider,
+ ToggleButton,
+ ToggleButtonGroup,
+} from '@mui/material';
+import {
+ Devices,
+ Computer,
+ PhoneAndroid,
+ Tablet,
+ Delete,
+ CheckCircle,
+ Shield,
+ ShieldOutlined,
+} from '@mui/icons-material';
+import { devicesApi, type DeviceInfo } from '@/lib/api/devices';
+import { motion } from 'framer-motion';
+import { formatDistanceToNow } from 'date-fns';
+
+export function DeviceTrustManagement() {
+ const [devices, setDevices] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [successMessage, setSuccessMessage] = useState(null);
+ const [filter, setFilter] = useState<'all' | 'trusted' | 'untrusted'>('all');
+
+ // Remove device dialog
+ const [removeDialogOpen, setRemoveDialogOpen] = useState(false);
+ const [deviceToRemove, setDeviceToRemove] = useState(null);
+ const [isRemoving, setIsRemoving] = useState(false);
+
+ // Remove all dialog
+ const [removeAllDialogOpen, setRemoveAllDialogOpen] = useState(false);
+ const [isRemovingAll, setIsRemovingAll] = useState(false);
+
+ useEffect(() => {
+ loadDevices();
+ }, []);
+
+ const loadDevices = async () => {
+ try {
+ setIsLoading(true);
+ const response = await devicesApi.getDevices();
+ setDevices(response.devices);
+ setError(null);
+ } catch (err: any) {
+ console.error('Failed to load devices:', err);
+ setError('Failed to load devices');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleTrustToggle = async (device: DeviceInfo) => {
+ try {
+ if (device.trusted) {
+ await devicesApi.revokeDeviceTrust(device.id);
+ setSuccessMessage('Device trust revoked');
+ } else {
+ await devicesApi.trustDevice(device.id);
+ setSuccessMessage('Device marked as trusted');
+ }
+ await loadDevices();
+ } catch (err: any) {
+ console.error('Failed to toggle device trust:', err);
+ setError(err.response?.data?.message || 'Failed to update device trust');
+ }
+ };
+
+ const handleRemoveDevice = async () => {
+ if (!deviceToRemove) return;
+
+ try {
+ setIsRemoving(true);
+ await devicesApi.removeDevice(deviceToRemove.id);
+ setSuccessMessage('Device removed successfully');
+ setRemoveDialogOpen(false);
+ setDeviceToRemove(null);
+ await loadDevices();
+ } catch (err: any) {
+ console.error('Failed to remove device:', err);
+ setError(err.response?.data?.message || 'Failed to remove device');
+ } finally {
+ setIsRemoving(false);
+ }
+ };
+
+ const handleRemoveAllDevices = async () => {
+ try {
+ setIsRemovingAll(true);
+ const response = await devicesApi.removeAllDevices();
+ setSuccessMessage(`${response.removedCount} device(s) removed successfully`);
+ setRemoveAllDialogOpen(false);
+ await loadDevices();
+ } catch (err: any) {
+ console.error('Failed to remove all devices:', err);
+ setError(err.response?.data?.message || 'Failed to remove devices');
+ } finally {
+ setIsRemovingAll(false);
+ }
+ };
+
+ const getPlatformIcon = (platform?: string) => {
+ if (!platform) return ;
+ const p = platform.toLowerCase();
+ if (p.includes('android') || p.includes('mobile')) return ;
+ if (p.includes('ios') || p.includes('iphone') || p.includes('ipad')) return ;
+ if (p.includes('tablet')) return ;
+ return ;
+ };
+
+ const filteredDevices = devices.filter((device) => {
+ if (filter === 'trusted') return device.trusted;
+ if (filter === 'untrusted') return !device.trusted;
+ return true;
+ });
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+ Device Trust Management
+
+
+
+
+
+ Manage which devices are trusted to access your account. Untrusted devices will require additional verification.
+
+
+ {error && (
+ setError(null)}>
+ {error}
+
+ )}
+
+ {successMessage && (
+ setSuccessMessage(null)}>
+ {successMessage}
+
+ )}
+
+ {/* Filter Toggle */}
+
+ newFilter && setFilter(newFilter)}
+ size="small"
+ >
+ All ({devices.length})
+
+ Trusted ({devices.filter((d) => d.trusted).length})
+
+
+ Untrusted ({devices.filter((d) => !d.trusted).length})
+
+
+
+
+ {filteredDevices.length === 0 ? (
+ No devices found for the selected filter.
+ ) : (
+ <>
+
+ {filteredDevices.map((device, index) => (
+
+ {index > 0 && }
+
+ {getPlatformIcon(device.platform)}
+
+
+ {device.platform || 'Unknown Platform'}
+
+ {device.isCurrent && (
+ } />
+ )}
+ {device.trusted ? (
+ } />
+ ) : (
+ }
+ />
+ )}
+
+ }
+ secondary={
+
+
+ Device: {device.deviceFingerprint}
+
+
+ Last seen: {formatDistanceToNow(new Date(device.lastSeen))} ago
+
+
+ }
+ />
+
+
+
+ {!device.isCurrent && (
+ {
+ setDeviceToRemove(device);
+ setRemoveDialogOpen(true);
+ }}
+ color="error"
+ >
+
+
+ )}
+
+
+
+
+ ))}
+
+
+ {devices.length > 1 && (
+
+
+
+ )}
+ >
+ )}
+
+
+
+ {/* Remove Device Dialog */}
+
+
+ {/* Remove All Devices Dialog */}
+
+ >
+ );
+}
diff --git a/maternal-web/lib/api/devices.ts b/maternal-web/lib/api/devices.ts
new file mode 100644
index 0000000..ab0ae00
--- /dev/null
+++ b/maternal-web/lib/api/devices.ts
@@ -0,0 +1,88 @@
+import axios from 'axios';
+
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020';
+
+export interface DeviceInfo {
+ id: string;
+ deviceFingerprint: string;
+ platform?: string;
+ trusted: boolean;
+ lastSeen: Date;
+ isCurrent?: boolean;
+}
+
+export const devicesApi = {
+ // Get all devices
+ async getDevices(): Promise<{ success: boolean; devices: DeviceInfo[]; totalCount: number }> {
+ const response = await axios.get(`${API_BASE_URL}/api/v1/auth/devices`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ });
+ return response.data;
+ },
+
+ // Get trusted devices only
+ async getTrustedDevices(): Promise<{ success: boolean; devices: DeviceInfo[]; totalCount: number }> {
+ const response = await axios.get(`${API_BASE_URL}/api/v1/auth/devices/trusted`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ });
+ return response.data;
+ },
+
+ // Get device counts
+ async getDeviceCount(): Promise<{ success: boolean; total: number; trusted: number; untrusted: number }> {
+ const response = await axios.get(`${API_BASE_URL}/api/v1/auth/devices/count`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ });
+ return response.data;
+ },
+
+ // Trust a device
+ async trustDevice(deviceId: string): Promise<{ success: boolean; message: string }> {
+ const response = await axios.post(
+ `${API_BASE_URL}/api/v1/auth/devices/${deviceId}/trust`,
+ {},
+ {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ }
+ );
+ return response.data;
+ },
+
+ // Revoke device trust
+ async revokeDeviceTrust(deviceId: string): Promise<{ success: boolean; message: string }> {
+ const response = await axios.delete(`${API_BASE_URL}/api/v1/auth/devices/${deviceId}/trust`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ });
+ return response.data;
+ },
+
+ // Remove device completely
+ async removeDevice(deviceId: string): Promise<{ success: boolean; message: string }> {
+ const response = await axios.delete(`${API_BASE_URL}/api/v1/auth/devices/${deviceId}`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ });
+ return response.data;
+ },
+
+ // Remove all devices except current
+ async removeAllDevices(): Promise<{ success: boolean; message: string; removedCount: number }> {
+ const response = await axios.delete(`${API_BASE_URL}/api/v1/auth/devices`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ });
+ return response.data;
+ },
+};