diff --git a/maternal-web/app/settings/page.tsx b/maternal-web/app/settings/page.tsx
index fa035f2..ec553f0 100644
--- a/maternal-web/app/settings/page.tsx
+++ b/maternal-web/app/settings/page.tsx
@@ -8,6 +8,7 @@ import { AppShell } from '@/components/layouts/AppShell/AppShell';
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 { motion } from 'framer-motion';
export default function SettingsPage() {
@@ -229,11 +230,22 @@ export default function SettingsPage() {
- {/* Account Actions */}
+ {/* Sessions Management */}
+
+
+
+
+
+ {/* Account Actions */}
+
diff --git a/maternal-web/components/settings/SessionsManagement.tsx b/maternal-web/components/settings/SessionsManagement.tsx
new file mode 100644
index 0000000..7183ca6
--- /dev/null
+++ b/maternal-web/components/settings/SessionsManagement.tsx
@@ -0,0 +1,278 @@
+'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,
+} from '@mui/material';
+import {
+ Devices,
+ Computer,
+ PhoneAndroid,
+ Tablet,
+ Delete,
+ CheckCircle,
+} from '@mui/icons-material';
+import { sessionsApi, type SessionInfo } from '@/lib/api/sessions';
+import { motion } from 'framer-motion';
+import { formatDistanceToNow } from 'date-fns';
+
+export function SessionsManagement() {
+ const [sessions, setSessions] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [successMessage, setSuccessMessage] = useState(null);
+
+ // Revoke session dialog
+ const [revokeDialogOpen, setRevokeDialogOpen] = useState(false);
+ const [sessionToRevoke, setSessionToRevoke] = useState(null);
+ const [isRevoking, setIsRevoking] = useState(false);
+
+ // Revoke all dialog
+ const [revokeAllDialogOpen, setRevokeAllDialogOpen] = useState(false);
+ const [isRevokingAll, setIsRevokingAll] = useState(false);
+
+ useEffect(() => {
+ loadSessions();
+ }, []);
+
+ const loadSessions = async () => {
+ try {
+ setIsLoading(true);
+ const response = await sessionsApi.getSessions();
+ setSessions(response.sessions);
+ setError(null);
+ } catch (err: any) {
+ console.error('Failed to load sessions:', err);
+ setError('Failed to load sessions');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleRevokeSession = async () => {
+ if (!sessionToRevoke) return;
+
+ try {
+ setIsRevoking(true);
+ await sessionsApi.revokeSession(sessionToRevoke.id);
+ setSuccessMessage('Session revoked successfully');
+ setRevokeDialogOpen(false);
+ setSessionToRevoke(null);
+ await loadSessions();
+ } catch (err: any) {
+ console.error('Failed to revoke session:', err);
+ setError(err.response?.data?.message || 'Failed to revoke session');
+ } finally {
+ setIsRevoking(false);
+ }
+ };
+
+ const handleRevokeAllSessions = async () => {
+ try {
+ setIsRevokingAll(true);
+ const response = await sessionsApi.revokeAllSessions();
+ setSuccessMessage(`${response.revokedCount} session(s) revoked successfully`);
+ setRevokeAllDialogOpen(false);
+ await loadSessions();
+ } catch (err: any) {
+ console.error('Failed to revoke all sessions:', err);
+ setError(err.response?.data?.message || 'Failed to revoke sessions');
+ } finally {
+ setIsRevokingAll(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 ;
+ };
+
+ if (isLoading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+ <>
+
+
+
+
+
+ Active Sessions
+
+
+
+
+
+ Manage your active sessions across different devices. You can revoke access from any device.
+
+
+ {error && (
+ setError(null)}>
+ {error}
+
+ )}
+
+ {successMessage && (
+ setSuccessMessage(null)}>
+ {successMessage}
+
+ )}
+
+ {sessions.length === 0 ? (
+ No active sessions found.
+ ) : (
+ <>
+
+ {sessions.map((session, index) => (
+
+ {index > 0 && }
+
+ {getPlatformIcon(session.platform)}
+
+
+ {session.platform || 'Unknown Platform'}
+
+ {session.isCurrent && (
+ } />
+ )}
+
+ }
+ secondary={
+
+
+ Device: {session.deviceFingerprint}
+
+
+ Last active: {formatDistanceToNow(new Date(session.lastUsed))} ago
+
+
+ Created: {formatDistanceToNow(new Date(session.createdAt))} ago
+
+
+ }
+ />
+
+ {!session.isCurrent && (
+ {
+ setSessionToRevoke(session);
+ setRevokeDialogOpen(true);
+ }}
+ color="error"
+ >
+
+
+ )}
+
+
+
+ ))}
+
+
+ {sessions.length > 1 && (
+
+
+
+ )}
+ >
+ )}
+
+
+
+ {/* Revoke Session Dialog */}
+
+
+ {/* Revoke All Sessions Dialog */}
+
+ >
+ );
+}
diff --git a/maternal-web/lib/api/sessions.ts b/maternal-web/lib/api/sessions.ts
new file mode 100644
index 0000000..323a30f
--- /dev/null
+++ b/maternal-web/lib/api/sessions.ts
@@ -0,0 +1,55 @@
+import axios from 'axios';
+
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3020';
+
+export interface SessionInfo {
+ id: string;
+ deviceFingerprint: string;
+ platform?: string;
+ lastUsed: Date;
+ createdAt: Date;
+ ipAddress?: string;
+ isCurrent: boolean;
+}
+
+export const sessionsApi = {
+ // Get all sessions
+ async getSessions(): Promise<{ success: boolean; sessions: SessionInfo[]; totalCount: number }> {
+ const response = await axios.get(`${API_BASE_URL}/api/v1/auth/sessions`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ });
+ return response.data;
+ },
+
+ // Get session count
+ async getSessionCount(): Promise<{ success: boolean; activeSessionCount: number }> {
+ const response = await axios.get(`${API_BASE_URL}/api/v1/auth/sessions/count`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ });
+ return response.data;
+ },
+
+ // Revoke specific session
+ async revokeSession(sessionId: string): Promise<{ success: boolean; message: string }> {
+ const response = await axios.delete(`${API_BASE_URL}/api/v1/auth/sessions/${sessionId}`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ });
+ return response.data;
+ },
+
+ // Revoke all sessions except current
+ async revokeAllSessions(): Promise<{ success: boolean; message: string; revokedCount: number }> {
+ const response = await axios.delete(`${API_BASE_URL}/api/v1/auth/sessions`, {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem('accessToken')}`,
+ },
+ });
+ return response.data;
+ },
+};