Blocks

Session Security Block

Account UI

A responsive account security workspace for reviewing signed-in devices, revoking risky sessions, checking recovery coverage, and tracking recent access activity.

Account settings

Device security center

Copy this into account settings, workspace security, consumer profile, fintech, healthcare, or admin-console products. Replace the local session, recovery, and event arrays with your authentication API data.

1200px

<script setup>
import { computed, ref } from 'vue';
import { DomButton, DomNativeSelect, DomToggle } from '@getdom/studio/vue';

const activeSessionId = ref('macbook');
const eventFilter = ref('all');
const revokeState = ref('idle');

const account = ref({
	name: 'Olivia Carter',
	email: 'olivia@northstar.example',
	securityScore: 86,
	mfaEnabled: true,
	passkeySignIn: true,
	loginAlerts: true,
});

const sessions = ref([
	{
		id: 'macbook',
		device: 'MacBook Pro',
		browser: 'Safari 18',
		platform: 'macOS',
		location: 'London, UK',
		ip: '81.2.69.142',
		lastActive: 'Active now',
		firstSeen: 'May 18',
		trusted: true,
		current: true,
		risk: 'Low',
		status: 'Active',
	},
	{
		id: 'iphone',
		device: 'iPhone 16',
		browser: 'DOM Studio iOS',
		platform: 'iOS',
		location: 'Manchester, UK',
		ip: '92.40.214.18',
		lastActive: '18 minutes ago',
		firstSeen: 'Jun 1',
		trusted: true,
		current: false,
		risk: 'Low',
		status: 'Active',
	},
	{
		id: 'windows',
		device: 'Windows workstation',
		browser: 'Chrome 137',
		platform: 'Windows',
		location: 'Berlin, DE',
		ip: '185.220.101.12',
		lastActive: 'Yesterday 22:48',
		firstSeen: 'Yesterday',
		trusted: false,
		current: false,
		risk: 'Review',
		status: 'Active',
	},
	{
		id: 'tablet',
		device: 'Shared tablet',
		browser: 'Firefox 139',
		platform: 'Android',
		location: 'Paris, FR',
		ip: '51.158.172.165',
		lastActive: '4 days ago',
		firstSeen: 'Apr 9',
		trusted: false,
		current: false,
		risk: 'Medium',
		status: 'Expired',
	},
]);

const recoveryItems = ref([
	{ key: 'passkeys', label: 'Passkeys registered', value: '2', status: 'Ready', detail: 'MacBook Pro and iPhone 16 can sign in without password.' },
	{ key: 'backup', label: 'Backup codes', value: '6 left', status: 'Refresh soon', detail: 'Generate a new set when fewer than 4 remain.' },
	{ key: 'email', label: 'Recovery email', value: 'Verified', status: 'Ready', detail: 'olivia.recovery@example.com receives recovery links.' },
	{ key: 'phone', label: 'Recovery phone', value: 'Not added', status: 'Missing', detail: 'Add a verified phone number for account recovery.' },
]);

const securityEvents = [
	{ id: 1, type: 'sign-in', label: 'Current device signed in', detail: 'Safari 18 on macOS from London, UK', time: 'Today 08:14', tone: 'success' },
	{ id: 2, type: 'setting', label: 'Login alerts enabled', detail: 'Security alerts will send for new devices and recovery changes.', time: 'Yesterday 16:02', tone: 'primary' },
	{ id: 3, type: 'review', label: 'Untrusted Windows session detected', detail: 'Chrome 137 from Berlin has not completed device trust.', time: 'Yesterday 22:48', tone: 'warning' },
	{ id: 4, type: 'recovery', label: 'Backup code used', detail: 'One backup code was used during account recovery.', time: 'Jun 6, 11:30', tone: 'muted' },
];

const eventOptions = [
	{ label: 'All events', value: 'all' },
	{ label: 'Sign-ins', value: 'sign-in' },
	{ label: 'Settings', value: 'setting' },
	{ label: 'Needs review', value: 'review' },
	{ label: 'Recovery', value: 'recovery' },
];

const activeSession = computed(() => sessions.value.find((session) => session.id === activeSessionId.value) || sessions.value[0]);
const activeSessions = computed(() => sessions.value.filter((session) => session.status === 'Active'));
const untrustedSessions = computed(() => activeSessions.value.filter((session) => !session.trusted));
const filteredEvents = computed(() => {
	if (eventFilter.value === 'all') return securityEvents;
	return securityEvents.filter((event) => event.type === eventFilter.value);
});
const readinessChecks = computed(() => [
	{ label: 'Multi-factor authentication', ready: account.value.mfaEnabled },
	{ label: 'Passkey sign-in', ready: account.value.passkeySignIn },
	{ label: 'Login alerts', ready: account.value.loginAlerts },
	{ label: 'No untrusted active sessions', ready: untrustedSessions.value.length === 0 },
]);
const readyCheckCount = computed(() => readinessChecks.value.filter((check) => check.ready).length);

function selectSession(id) {
	activeSessionId.value = id;
	revokeState.value = 'idle';
}

function revokeSelectedSession() {
	if (activeSession.value.current) return;
	activeSession.value.status = 'Revoked';
	activeSession.value.trusted = false;
	revokeState.value = 'revoked';
}
</script>

<template>
	<div class="w-full max-w-7xl overflow-hidden rounded-lg border border-border bg-background text-fg shadow-2xl shadow-black/10">
		<header class="border-b border-border skin-raised px-5 py-5 sm:px-7">
			<div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
				<div>
					<p class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-fg">Account security</p>
					<h3 class="mt-2 text-2xl font-semibold tracking-tight">Device security center</h3>
					<p class="mt-2 max-w-3xl text-sm leading-6 text-muted-fg">
						Give users a direct place to review signed-in devices, revoke access, strengthen recovery, and audit recent account activity.
					</p>
				</div>
				<div class="flex flex-wrap items-center gap-2">
					<span class="rounded-full bg-success/15 px-3 py-1 text-xs font-semibold text-success">
						{{ account.securityScore }}% protected
					</span>
					<DomButton variant="secondary" size="sm">Download log</DomButton>
					<DomButton size="sm">Run security check</DomButton>
				</div>
			</div>

			<div class="mt-5 grid gap-3 md:grid-cols-4">
				<div class="border-l-4 border-primary bg-background px-4 py-3">
					<p class="text-xs font-medium text-muted-fg">Active sessions</p>
					<p class="mt-1 text-2xl font-semibold tracking-tight">{{ activeSessions.length }}</p>
				</div>
				<div class="border-l-4 border-warning bg-background px-4 py-3">
					<p class="text-xs font-medium text-muted-fg">Needs review</p>
					<p class="mt-1 text-2xl font-semibold tracking-tight">{{ untrustedSessions.length }}</p>
				</div>
				<div class="border-l-4 border-success bg-background px-4 py-3">
					<p class="text-xs font-medium text-muted-fg">Readiness checks</p>
					<p class="mt-1 text-2xl font-semibold tracking-tight">{{ readyCheckCount }}/{{ readinessChecks.length }}</p>
				</div>
				<div class="border-l-4 border-border bg-background px-4 py-3">
					<p class="text-xs font-medium text-muted-fg">Account owner</p>
					<p class="mt-1 truncate text-base font-semibold">{{ account.email }}</p>
				</div>
			</div>
		</header>

		<div class="grid lg:grid-cols-[18rem_minmax(0,1fr)_22rem]">
			<aside class="border-b border-border bg-secondary/35 lg:border-b-0 lg:border-r">
				<div class="flex items-center justify-between gap-3 px-5 py-4">
					<h4 class="text-sm font-semibold">Signed-in devices</h4>
					<span class="rounded-full bg-background px-2 py-1 text-xs font-semibold text-muted-fg">{{ sessions.length }}</span>
				</div>
				<div class="grid">
					<button
						v-for="session in sessions"
						:key="session.id"
						type="button"
						class="border-t border-border px-5 py-4 text-left transition hover:bg-background/70"
						:class="activeSessionId === session.id ? 'bg-background' : ''"
						@click="selectSession(session.id)"
					>
						<div class="flex items-start justify-between gap-3">
							<div class="min-w-0">
								<p class="truncate text-sm font-semibold">{{ session.device }}</p>
								<p class="mt-1 text-xs leading-5 text-muted-fg">{{ session.browser }} on {{ session.platform }}</p>
							</div>
							<span
								class="rounded-full px-2 py-0.5 text-[11px] font-semibold"
								:class="session.status === 'Revoked' ? 'bg-muted text-muted-fg' : session.risk === 'Low' ? 'bg-success/15 text-success' : 'bg-warning/15 text-warning'"
							>
								{{ session.status === 'Revoked' ? 'Revoked' : session.risk }}
							</span>
						</div>
						<div class="mt-4 grid grid-cols-2 gap-2 text-xs">
							<div>
								<p class="text-muted-fg">Location</p>
								<p class="mt-1 truncate font-semibold">{{ session.location }}</p>
							</div>
							<div>
								<p class="text-muted-fg">Last active</p>
								<p class="mt-1 truncate font-semibold">{{ session.lastActive }}</p>
							</div>
						</div>
					</button>
				</div>
			</aside>

			<main class="min-w-0">
				<section class="border-b border-border px-5 py-5 sm:px-7">
					<div class="flex flex-col gap-3 md:flex-row md:items-start md:justify-between">
						<div>
							<p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-fg">Selected session</p>
							<h4 class="mt-2 text-xl font-semibold tracking-tight">{{ activeSession.device }}</h4>
							<p class="mt-2 text-sm leading-6 text-muted-fg">
								{{ activeSession.browser }} on {{ activeSession.platform }} from {{ activeSession.location }}.
							</p>
						</div>
						<div class="flex flex-wrap gap-2">
							<span v-if="activeSession.current" class="rounded-full bg-primary/15 px-3 py-1 text-xs font-semibold text-primary">Current device</span>
							<span v-if="activeSession.trusted" class="rounded-full bg-success/15 px-3 py-1 text-xs font-semibold text-success">Trusted</span>
							<span v-else class="rounded-full bg-warning/15 px-3 py-1 text-xs font-semibold text-warning">Untrusted</span>
						</div>
					</div>
				</section>

				<section class="grid gap-6 border-b border-border px-5 py-6 sm:px-7 xl:grid-cols-2">
					<div>
						<h5 class="text-sm font-semibold">Session details</h5>
						<dl class="mt-4 grid gap-3 text-sm">
							<div class="flex justify-between gap-4 border-b border-border pb-3">
								<dt class="text-muted-fg">IP address</dt>
								<dd class="font-semibold">{{ activeSession.ip }}</dd>
							</div>
							<div class="flex justify-between gap-4 border-b border-border pb-3">
								<dt class="text-muted-fg">First seen</dt>
								<dd class="font-semibold">{{ activeSession.firstSeen }}</dd>
							</div>
							<div class="flex justify-between gap-4 border-b border-border pb-3">
								<dt class="text-muted-fg">Status</dt>
								<dd class="font-semibold">{{ activeSession.status }}</dd>
							</div>
							<div class="flex justify-between gap-4">
								<dt class="text-muted-fg">Risk</dt>
								<dd class="font-semibold">{{ activeSession.risk }}</dd>
							</div>
						</dl>
					</div>

					<div>
						<h5 class="text-sm font-semibold">Available actions</h5>
						<div class="mt-4 grid gap-3">
							<DomButton variant="secondary">Rename device</DomButton>
							<DomButton variant="secondary" :disabled="activeSession.status === 'Revoked'">Mark as trusted</DomButton>
							<DomButton
								:variant="activeSession.current || activeSession.status === 'Revoked' ? 'secondary' : 'primary'"
								:disabled="activeSession.current || activeSession.status === 'Revoked'"
								@click="revokeSelectedSession"
							>
								{{ activeSession.current ? 'Cannot revoke current session' : activeSession.status === 'Revoked' ? 'Session revoked' : 'Revoke this session' }}
							</DomButton>
							<p v-if="revokeState === 'revoked'" class="rounded-lg bg-success/15 px-3 py-2 text-sm font-medium text-success">
								Access for this device has been revoked in the UI state.
							</p>
						</div>
					</div>
				</section>

				<section class="px-5 py-6 sm:px-7">
					<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
						<div>
							<h5 class="text-sm font-semibold">Recent security activity</h5>
							<p class="mt-1 text-sm leading-6 text-muted-fg">Show immutable events from sign-in, recovery, and security-setting changes.</p>
						</div>
						<DomNativeSelect v-model="eventFilter" :options="eventOptions" class="md:w-44" />
					</div>
					<div class="mt-4 divide-y divide-border border-y border-border">
						<div v-for="event in filteredEvents" :key="event.id" class="grid gap-2 py-4 sm:grid-cols-[1fr_auto] sm:items-start">
							<div>
								<div class="flex flex-wrap items-center gap-2">
									<p class="text-sm font-semibold">{{ event.label }}</p>
									<span
										class="rounded-full px-2 py-0.5 text-[11px] font-semibold"
										:class="event.tone === 'warning' ? 'bg-warning/15 text-warning' : event.tone === 'success' ? 'bg-success/15 text-success' : 'bg-secondary text-muted-fg'"
									>
										{{ event.type }}
									</span>
								</div>
								<p class="mt-1 text-sm leading-6 text-muted-fg">{{ event.detail }}</p>
							</div>
							<p class="text-xs font-semibold text-muted-fg">{{ event.time }}</p>
						</div>
					</div>
				</section>
			</main>

			<aside class="grid content-start gap-6 border-t border-border bg-secondary/20 p-5 lg:border-l lg:border-t-0 sm:p-6">
				<section>
					<h4 class="font-semibold tracking-tight">Protection controls</h4>
					<div class="mt-4 grid gap-4">
						<div class="border-b border-border pb-4">
							<DomToggle v-model="account.mfaEnabled" label="Require MFA at sign-in" />
							<p class="mt-2 text-sm leading-6 text-muted-fg">Ask for a second factor on new browsers and risky sign-ins.</p>
						</div>
						<div class="border-b border-border pb-4">
							<DomToggle v-model="account.passkeySignIn" label="Allow passkey sign-in" />
							<p class="mt-2 text-sm leading-6 text-muted-fg">Let verified devices use hardware-backed passkeys.</p>
						</div>
						<div>
							<DomToggle v-model="account.loginAlerts" label="New sign-in alerts" />
							<p class="mt-2 text-sm leading-6 text-muted-fg">Send an alert whenever a new device accesses the account.</p>
						</div>
					</div>
				</section>

				<section class="border-t border-border pt-5">
					<h4 class="font-semibold tracking-tight">Recovery coverage</h4>
					<div class="mt-4 divide-y divide-border border-y border-border">
						<div v-for="item in recoveryItems" :key="item.key" class="py-3">
							<div class="flex items-start justify-between gap-3">
								<div>
									<p class="text-sm font-semibold">{{ item.label }}</p>
									<p class="mt-1 text-xs leading-5 text-muted-fg">{{ item.detail }}</p>
								</div>
								<span
									class="shrink-0 rounded-full px-2 py-0.5 text-[11px] font-semibold"
									:class="item.status === 'Ready' ? 'bg-success/15 text-success' : item.status === 'Missing' ? 'bg-warning/15 text-warning' : 'bg-secondary text-muted-fg'"
								>
									{{ item.value }}
								</span>
							</div>
						</div>
					</div>
				</section>

				<section class="border-t border-border pt-5">
					<h4 class="font-semibold tracking-tight">Readiness checks</h4>
					<div class="mt-3 grid gap-2">
						<div v-for="check in readinessChecks" :key="check.label" class="flex items-center justify-between gap-3 text-sm">
							<span class="text-muted-fg">{{ check.label }}</span>
							<span :class="check.ready ? 'text-success' : 'text-warning'" class="font-semibold">
								{{ check.ready ? 'Ready' : 'Review' }}
							</span>
						</div>
					</div>
				</section>
			</aside>
		</div>
	</div>
</template>

Integration

How to use this block

Use this block when users need a concrete way to understand account access and act on suspicious activity. It joins device inventory, trust status, session actions, recovery readiness, MFA/passkey controls, and immutable security events in one copyable surface.

  • Replace sessions with server-issued session records including device, browser, region, IP, last activity, trust state, and revocation eligibility.
  • Route revoke actions through a session API that invalidates refresh tokens and writes a security event with actor, target session, IP, and reason.
  • Keep passkey, MFA, recovery email, phone, and backup-code state server-owned; the UI should render readiness and start setup flows, not decide policy locally.
  • Send high-risk events to email, push, or in-app alerts so account owners can confirm or deny new sign-ins quickly.
  • For regulated products, require fresh authentication before revoking all other sessions, exporting events, changing recovery methods, or disabling MFA.

Data

Recommended security payload

{
	accountId: 'acct_olivia',
	securityScore: 86,
	sessions: [
		{
			id: 'sess_macbook_london',
			device: 'MacBook Pro',
			browser: 'Safari 18',
			platform: 'macOS',
			region: 'London, UK',
			ip: '81.2.69.142',
			lastActiveAt: '2026-06-10T17:42:00Z',
			trusted: true,
			current: true,
			risk: 'Low'
		}
	],
	recovery: {
		mfaEnabled: true,
		passkeys: 2,
		backupCodesRemaining: 6,
		recoveryEmailVerified: true,
		phoneVerified: false
	},
	events: [
		{ type: 'new_device', label: 'New sign-in approved', time: 'Today 08:14', region: 'London, UK' }
	]
}

Customization

Implementation notes

Session safety

Never trust client-selected session state alone. Revoke by server session ID and handle current-session revocation with a clear redirect path.

Risk signals

Calculate risk from device fingerprint, impossible travel, IP reputation, MFA result, and recent failed attempts on the backend.

Future updates

Useful follow-ups include reusable re-auth dialogs, passkey setup flows, trusted-device naming, security event filters, and exportable audit logs.