allauth/ (44 files) is a django-allauth React UI — a separate concern from the Mizan protocol. Moved to legacy/ pending extraction into a standalone mizan-django-allauth package. Also moved to legacy/: - client/AuthContext.tsx — generic auth state from /me endpoint - client/RouterContext.tsx — framework-agnostic router adapter - client/routing.tsx — UserRoute/StaffRoute/AnonymousRoute guards - client/nextjs.tsx — Next.js router adapter for auth These are auth UI infrastructure, not Mizan protocol. The Mizan core only needs JWT for auth header selection (jwt/ stays — MizanProvider depends on useJWT() to decide between Bearer and session auth). Cleaned up re-exports in client/react.ts and vitest aliases. 33 React tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
89 lines
3.0 KiB
TypeScript
89 lines
3.0 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useAllauthAPI } from '../../contexts/APIContext'
|
|
import { SettingsSection, SettingsItem, SettingsList, Badge, Button } from './SettingsComponents'
|
|
import type { Session } from '../../types'
|
|
|
|
function parseUserAgent(ua: string): string {
|
|
if (ua.includes('Chrome')) return 'Chrome'
|
|
if (ua.includes('Firefox')) return 'Firefox'
|
|
if (ua.includes('Safari')) return 'Safari'
|
|
if (ua.includes('Edge')) return 'Edge'
|
|
return 'Unknown Browser'
|
|
}
|
|
|
|
export function SessionsSection() {
|
|
const api = useAllauthAPI()
|
|
const [sessions, setSessions] = useState<Session[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [available, setAvailable] = useState(true)
|
|
|
|
const fetchSessions = async () => {
|
|
try {
|
|
const res = await api.session.list()
|
|
if (res.status === 200 && res.data) {
|
|
setSessions(res.data as Session[])
|
|
} else {
|
|
// Non-200 status means sessions feature not available
|
|
setAvailable(false)
|
|
}
|
|
} catch {
|
|
setAvailable(false)
|
|
}
|
|
setLoading(false)
|
|
}
|
|
|
|
useEffect(() => { fetchSessions() }, [])
|
|
|
|
const handleEnd = async (id: number) => {
|
|
if (!confirm('End this session?')) return
|
|
await api.session.remove([id])
|
|
fetchSessions()
|
|
}
|
|
|
|
const handleEndAllOthers = async () => {
|
|
const otherIds = sessions.filter(s => !s.is_current).map(s => s.id)
|
|
if (otherIds.length === 0) return
|
|
if (!confirm(`End ${otherIds.length} other session(s)?`)) return
|
|
await api.session.remove(otherIds)
|
|
fetchSessions()
|
|
}
|
|
|
|
if (loading || !available) return null
|
|
|
|
const otherSessions = sessions.filter(s => !s.is_current)
|
|
|
|
return (
|
|
<SettingsSection title="Active Sessions">
|
|
<SettingsList>
|
|
{sessions.map(session => (
|
|
<SettingsItem
|
|
key={session.id}
|
|
label={
|
|
<>
|
|
{parseUserAgent(session.user_agent)}
|
|
{session.is_current && <Badge variant="success">Current</Badge>}
|
|
</>
|
|
}
|
|
meta={`${session.ip} · ${session.last_seen_at ? new Date(session.last_seen_at * 1000).toLocaleString() : 'Unknown'}`}
|
|
actions={
|
|
!session.is_current && (
|
|
<Button variant="danger" onClick={() => handleEnd(session.id)}>
|
|
End
|
|
</Button>
|
|
)
|
|
}
|
|
/>
|
|
))}
|
|
</SettingsList>
|
|
|
|
{otherSessions.length > 0 && (
|
|
<Button variant="danger" onClick={handleEndAllOthers}>
|
|
End All Other Sessions
|
|
</Button>
|
|
)}
|
|
</SettingsSection>
|
|
)
|
|
}
|