auth-yes/ui/components/SessionsPage.tsx

234 lines
8.4 KiB
TypeScript

import { AuthenticatedLayout } from "./AuthenticatedLayout.tsx";
export const SessionsPage = ({
sessions,
currentSessionId,
isAdmin = false,
}: {
sessions: any[];
currentSessionId: string;
isAdmin?: boolean;
}) => {
return (
<AuthenticatedLayout
title="Sessions"
currentPath="/dashboard/sessions"
isAdmin={isAdmin}
>
<div style="margin-bottom: 2rem;">
<h1 style="font-size: 1.75rem; font-weight: 700; margin: 0 0 0.5rem 0; color: var(--text-primary);">
Active Sessions
</h1>
<p style="color: var(--text-secondary); margin: 0; font-size: 0.95rem;">
Manage and revoke authenticated device sessions connected to your
account.
</p>
</div>
{/* Desktop Table View (≥ 768px) */}
<div class="card desktop-only" style="display: none;">
<div class="table-container">
<table>
<thead>
<tr>
<th>Status</th>
<th>Created At</th>
<th>Expires At</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{sessions.length === 0
? (
<tr>
<td
colSpan={4}
style="text-align: center; padding: 2rem; color: var(--text-muted);"
>
No active sessions found.
</td>
</tr>
)
: (
sessions.map((session) => {
const isCurrent = session.id === currentSessionId;
return (
<tr key={session.id}>
<td>
{isCurrent
? (
<span class="badge badge-success">
Current Session
</span>
)
: (
<span class="badge badge-secondary">
Remote Session
</span>
)}
</td>
<td style="color: var(--text-secondary);">
{new Date(session.created_at).toLocaleString()}
</td>
<td style="color: var(--text-secondary);">
{new Date(session.expires_at).toLocaleString()}
</td>
<td>
{!isCurrent && (
<button
type="button"
class="btn-danger revoke-btn"
data-session-id={session.id}
style="padding: 0.35rem 0.75rem; font-size: 0.85rem; min-height: 32px;"
>
Revoke
</button>
)}
</td>
</tr>
);
})
)}
</tbody>
</table>
</div>
</div>
{/* Mobile Adaptive Cards View (< 768px) */}
<div
class="mobile-only"
style="display: flex; flex-direction: column; gap: 1rem;"
>
{sessions.length === 0
? (
<div class="card" style="text-align: center; padding: 2rem;">
<p style="color: var(--text-muted); margin: 0;">
No active sessions found.
</p>
</div>
)
: (
sessions.map((session) => {
const isCurrent = session.id === currentSessionId;
return (
<div class="card" key={session.id} style="margin-bottom: 0;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.75rem;">
<div style="display: flex; align-items: center; gap: 0.5rem;">
<div style="display: flex; align-items: center; justify-content: center; width: 36px; height: 36px; background: var(--surface-muted); border-radius: var(--radius-md); color: var(--text-secondary);">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect
width="14"
height="20"
x="5"
y="2"
rx="2"
ry="2"
>
</rect>
<line x1="12" x2="12.01" y1="18" y2="18"></line>
</svg>
</div>
<div>
<div style="font-weight: 600; font-size: 0.95rem; color: var(--text-primary);">
{isCurrent ? "This Device" : "Remote Device"}
</div>
<div style="font-size: 0.75rem; color: var(--text-muted); font-family: monospace;">
{session.id.substring(0, 13)}...
</div>
</div>
</div>
{isCurrent
? <span class="badge badge-success">Active Now</span>
: <span class="badge badge-secondary">Active</span>}
</div>
<div style="font-size: 0.8rem; color: var(--text-secondary); margin-bottom: 1rem; line-height: 1.6;">
<div>
<strong>Signed in:</strong>{" "}
{new Date(session.created_at).toLocaleString()}
</div>
<div>
<strong>Expires:</strong>{" "}
{new Date(session.expires_at).toLocaleString()}
</div>
</div>
{!isCurrent && (
<button
type="button"
class="btn-danger revoke-btn"
data-session-id={session.id}
style="width: 100%; min-height: 44px;"
>
Revoke Device Session
</button>
)}
</div>
);
})
)}
</div>
<style>
{`
@media (min-width: 768px) {
.desktop-only { display: block !important; }
.mobile-only { display: none !important; }
}
@media (max-width: 767px) {
.desktop-only { display: none !important; }
.mobile-only { display: flex !important; }
}
`}
</style>
<script
dangerouslySetInnerHTML={{
__html: `
document.querySelectorAll('.revoke-btn').forEach(btn => {
btn.addEventListener('click', async (e) => {
if (!confirm('Are you sure you want to revoke this session?')) return;
const sessionId = e.currentTarget.getAttribute('data-session-id');
const originalText = e.currentTarget.textContent;
e.currentTarget.textContent = 'Revoking...';
e.currentTarget.disabled = true;
try {
const res = await fetch(\`/api/sessions/\${sessionId}\`, {
method: 'DELETE'
});
if (res.ok) {
window.location.reload();
} else {
const data = await res.json();
alert(data.error || 'Failed to revoke session');
e.currentTarget.textContent = originalText;
e.currentTarget.disabled = false;
}
} catch (err) {
alert('An error occurred');
e.currentTarget.textContent = originalText;
e.currentTarget.disabled = false;
}
});
});
`,
}}
>
</script>
</AuthenticatedLayout>
);
};