Session Device Manager
blocksecurityActive multi-device login sessions manager and security dashboard featuring current session hero card with IP geolocation, suspicious new login alert banner, 2FA verification badges, and instant individual or bulk session revocation for desktop, mobile, and CLI tokens.
Installation
$pnpm dlx uipkge-ng@latest add session-device-manager$npx uipkge-ng@latest add session-device-manager$yarn dlx uipkge-ng@latest add session-device-manager$bunx uipkge-ng@latest add session-device-managerManifest:
https://uipkge.dev/r/angular/session-device-manager.jsonInstalls to:src/app/components/blocks/session-device-manager/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
SessionDeviceinterface SessionDevice {
id: string
name: string
deviceType: 'laptop' | 'desktop' | 'mobile' | 'tablet' | 'terminal'
os: string
browser: string
location: string
countryFlag: string
ip: string
signedInAt: string
lastActive: string
isMfa: boolean
isSuspicious?: boolean
customBadge?: {
label: string
variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'info'
}
}npm dependencies
Files installed (2)
src/app/components/blocks/session-device-manager/session-device-manager.component.ts18 kB
// Active multi-device login sessions manager: current session hero card with IP // geolocation, suspicious new login alert banner, 2FA verification badges, and instant // individual (two-click confirm) or bulk session revocation. Port of the React block 1:1. import { ChangeDetectionStrategy, Component, DestroyRef, Input, inject, signal } from '@angular/core' import { CheckCircle2, Clock, Globe, Laptop, LogOut, LucideAngularModule, MapPin, Monitor, Radio, ShieldCheck, Smartphone, Tablet, Terminal, TriangleAlert, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent, type BadgeVariant } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, } from '@/ui/card/card.component' import { UiSeparatorComponent } from '@/ui/separator/separator.component' export interface SessionDevice { id: string name: string deviceType: 'laptop' | 'desktop' | 'mobile' | 'tablet' | 'terminal' os: string browser: string location: string countryFlag: string ip: string signedInAt: string lastActive: string isMfa: boolean isSuspicious?: boolean customBadge?: { label: string variant: 'default' | 'secondary' | 'destructive' | 'outline' | 'success' | 'warning' | 'info' } } const currentSession = { id: 'sess-current-01', name: 'MacBook Pro 16"', deviceType: 'laptop' as const, os: 'macOS Sequoia', browser: 'Chrome 128.0', location: 'San Francisco, CA, United States', countryFlag: '🇺🇸', ip: '76.76.21.21', signedInAt: 'Aug 18, 2026 · 09:42 AM', lastActive: 'Active right now', isMfa: true, } const stubOtherSessions: SessionDevice[] = [ { id: 'sess-02', name: 'iPhone 16 Pro', deviceType: 'mobile', os: 'iOS 18.0', browser: 'Safari Mobile', location: 'Los Angeles, CA, United States', countryFlag: '🇺🇸', ip: '172.56.21.89', signedInAt: 'Aug 19, 2026', lastActive: '12m ago', isMfa: true, }, { id: 'sess-03', name: 'Workstation', deviceType: 'desktop', os: 'Ubuntu 24.04 LTS', browser: 'Firefox 130', location: 'London, Greater London, United Kingdom', countryFlag: '🇬🇧', ip: '185.220.101.5', signedInAt: 'Aug 21, 2026', lastActive: '2h ago', isMfa: true, isSuspicious: true, customBadge: { label: 'New Location', variant: 'warning', }, }, { id: 'sess-04', name: 'CLI Token', deviceType: 'terminal', os: 'Darwin x64', browser: 'Node.js SDK', location: 'AWS us-east-1 (N. Virginia), United States', countryFlag: '🇺🇸', ip: '54.234.19.102', signedInAt: 'Aug 10, 2026', lastActive: '34m ago', isMfa: true, customBadge: { label: 'CLI Token', variant: 'secondary', }, }, { id: 'sess-05', name: 'iPad Air', deviceType: 'tablet', os: 'iPadOS 17.6', browser: 'Safari', location: 'New York, NY, United States', countryFlag: '🇺🇸', ip: '68.195.44.110', signedInAt: 'Aug 14, 2026', lastActive: '3d ago', isMfa: true, }, ] @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-session-device-manager, [ui-session-device-manager]', standalone: true, // React renders the root <div> itself: the host stays out of layout and `class` goes to the root. host: { '[attr.class]': '"contents"' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiCardComponent, UiCardContentComponent, UiCardDescriptionComponent, UiCardHeaderComponent, UiCardTitleComponent, UiSeparatorComponent, ], template: ` <div data-slot="session-device-manager" [class]="rootClass"> <!-- Header --> <div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between"> <div class="space-y-1"> <h2 class="text-foreground text-xl font-semibold tracking-tight sm:text-2xl"> Active Sessions & Connected Devices </h2> <p class="text-muted-foreground text-xs sm:text-sm"> Manage and revoke active login sessions across your desktop, mobile, and CLI tokens. </p> </div> <button ui-button variant="destructive" size="sm" [disabled]="otherSessions().length === 0 || revokingAll()" class="shrink-0" (click)="revokeAllOtherSessions()" > <lucide-icon [img]="LogOut" class="size-4" /> Revoke All Other Sessions </button> </div> <!-- Suspicious Login Alert Banner --> @if (showSuspiciousAlert()) { <div class="border-warning/30 bg-warning/10 text-foreground relative flex flex-col gap-3 rounded-lg border p-4 text-sm sm:flex-row sm:items-start sm:justify-between" role="alert" > <div class="flex items-start gap-3"> <div class="bg-warning/20 text-warning mt-0.5 grid size-8 shrink-0 place-items-center rounded-md"> <lucide-icon [img]="TriangleAlert" class="size-4" /> </div> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <p class="text-foreground font-semibold">Suspicious Login Detected</p> <span ui-badge variant="warning" class="text-xs">Is this you?</span> </div> <p class="text-muted-foreground text-xs leading-relaxed"> Recent new login on <span class="text-foreground font-medium">Workstation · Firefox 130</span> from <span class="text-foreground font-medium">London, UK (IP 185.220.101.5)</span> on Aug 21, 2026. If you do not recognize this activity, revoke the session immediately. </p> </div> </div> <div class="flex shrink-0 items-center gap-2 sm:self-center"> <button ui-button variant="outline" size="sm" class="text-muted-foreground hover:text-foreground h-8 text-xs" (click)="dismissAlert()" > This was me </button> <button ui-button variant="destructive" size="sm" class="h-8 text-xs" (click)="revokeSuspicious()"> Revoke Session </button> </div> </div> } <!-- Current Session Hero Card --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header class="pb-3"> <div class="flex flex-wrap items-center justify-between gap-2"> <div class="flex items-center gap-2.5"> <span class="text-muted-foreground text-xs font-semibold tracking-wider uppercase"> This Device </span> <span ui-badge variant="success" class="gap-1.5 py-0.5 text-xs"> <span class="relative flex size-2"> <span class="bg-success absolute inline-flex size-full rounded-full opacity-75"></span> <span class="bg-success relative inline-flex size-2 rounded-full"></span> </span> Current Active Session </span> </div> <span ui-badge variant="outline" class="text-muted-foreground gap-1 text-xs"> <lucide-icon [img]="ShieldCheck" class="text-success size-3.5" /> 2FA Verified </span> </div> </div> <div ui-card-content class="space-y-4"> <div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"> <div class="flex items-start gap-3.5"> <div class="border-border bg-muted/80 text-foreground flex size-11 shrink-0 items-center justify-center rounded-lg border shadow-xs" > <lucide-icon [img]="Laptop" class="size-5" /> </div> <div class="space-y-1"> <div class="flex flex-wrap items-center gap-2"> <h3 class="text-foreground text-base font-semibold sm:text-lg"> {{ currentSession.name }} · {{ currentSession.os }} </h3> </div> <p class="text-muted-foreground text-xs font-medium sm:text-sm">{{ currentSession.browser }}</p> </div> </div> <div class="border-border bg-muted/40 text-muted-foreground flex items-center gap-1.5 self-start rounded-md border px-2.5 py-1 text-xs sm:self-auto" > <lucide-icon [img]="Radio" class="text-success size-3.5" /> <span class="text-foreground font-medium">{{ currentSession.lastActive }}</span> </div> </div> <ui-separator /> <div class="grid grid-cols-1 gap-3 text-xs sm:grid-cols-2 lg:grid-cols-3"> <div class="text-muted-foreground flex items-center gap-2"> <lucide-icon [img]="MapPin" class="text-foreground size-3.5 shrink-0" /> <span class="truncate"> {{ currentSession.countryFlag }} {{ currentSession.location }} </span> </div> <div class="text-muted-foreground flex items-center gap-2 font-mono"> <lucide-icon [img]="Globe" class="text-foreground size-3.5 shrink-0" /> <span>IP: {{ currentSession.ip }}</span> </div> <div class="text-muted-foreground flex items-center gap-2 sm:col-span-2 lg:col-span-1"> <lucide-icon [img]="Clock" class="text-foreground size-3.5 shrink-0" /> <span>Signed in: {{ currentSession.signedInAt }}</span> </div> </div> </div> </div> <!-- Other Active Sessions List --> <div ui-card class="border-border bg-card shadow-xs"> <div ui-card-header> <div class="flex flex-wrap items-center justify-between gap-2"> <div> <h3 ui-card-title class="text-foreground text-base font-semibold sm:text-lg">Other Active Sessions</h3> <p ui-card-description class="mt-1 text-xs sm:text-sm"> {{ otherSessions().length }} active session{{ otherSessions().length === 1 ? '' : 's' }} authenticated across your secondary devices and automation workflows. </p> </div> <span ui-badge variant="secondary" class="font-mono text-xs"> {{ otherSessions().length }} active </span> </div> </div> <div ui-card-content> @if (otherSessions().length === 0) { <div class="border-border flex flex-col items-center justify-center rounded-lg border border-dashed py-12 text-center" > <div class="bg-muted text-muted-foreground grid size-11 place-items-center rounded-full"> <lucide-icon [img]="CheckCircle2" class="text-success size-5" /> </div> <p class="text-foreground mt-3 text-sm font-medium">No other active sessions</p> <p class="text-muted-foreground mt-1 max-w-sm text-xs"> Your account is only signed in on this device. All previous mobile, workstation, and CLI sessions have been revoked. </p> </div> } @else { <ul class="divide-border -my-2 divide-y"> @for (session of otherSessions(); track session.id) { <li class="flex flex-col gap-3 py-4 sm:flex-row sm:items-center sm:justify-between"> <div class="flex items-start gap-3"> <div class="border-border bg-muted/60 text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-lg border shadow-xs" > @switch (session.deviceType) { @case ('mobile') { <lucide-icon [img]="Smartphone" class="size-4" /> } @case ('desktop') { <lucide-icon [img]="Monitor" class="size-4" /> } @case ('terminal') { <lucide-icon [img]="Terminal" class="size-4" /> } @case ('tablet') { <lucide-icon [img]="Tablet" class="size-4" /> } @default { <lucide-icon [img]="Laptop" class="size-4" /> } } </div> <div class="min-w-0 space-y-1"> <div class="flex flex-wrap items-center gap-2"> <p class="text-foreground truncate text-sm font-medium"> {{ session.name }} · {{ session.browser }} </p> <span ui-badge variant="outline" class="gap-1 py-0 text-xs"> <lucide-icon [img]="ShieldCheck" class="text-success size-3" /> 2FA Verified </span> @if (session.customBadge) { <span ui-badge [variant]="customBadgeVariant(session)" class="text-xs"> {{ session.customBadge!.label }} </span> } </div> <div class="text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1 text-xs"> <span class="flex items-center gap-1"> <lucide-icon [img]="MapPin" class="size-3 shrink-0" /> {{ session.countryFlag }} {{ session.location }} </span> <span>·</span> <span class="font-mono">IP: {{ session.ip }}</span> </div> <p class="text-muted-foreground text-xs"> Signed in {{ session.signedInAt }} · Last active <span class="text-foreground font-medium">{{ session.lastActive }}</span> </p> </div> </div> <div class="flex shrink-0 items-center gap-2 self-end sm:self-center"> <button ui-button variant="outline" size="sm" [class]="revokeButtonClass(session.id)" (click)="requestRevoke(session.id)" (blur)="cancelRevoke(session.id)" > {{ confirmingRevokeId() === session.id ? 'Confirm Revoke?' : 'Revoke Session' }} </button> </div> </li> } </ul> } </div> </div> </div> `, }) export class UiSessionDeviceManagerComponent { protected readonly CheckCircle2 = CheckCircle2 protected readonly Clock = Clock protected readonly Globe = Globe protected readonly Laptop = Laptop protected readonly LogOut = LogOut protected readonly MapPin = MapPin protected readonly Monitor = Monitor protected readonly Radio = Radio protected readonly ShieldCheck = ShieldCheck protected readonly Smartphone = Smartphone protected readonly Tablet = Tablet protected readonly Terminal = Terminal protected readonly TriangleAlert = TriangleAlert protected readonly currentSession = currentSession @Input('initialOtherSessions') set initialOtherSessionsInput(v: SessionDevice[] | undefined) { if (v) this.otherSessions.set(v.map((s) => ({ ...s }))) } @Input('initialShowSuspiciousAlert') set initialShowSuspiciousAlertInput(v: boolean | undefined) { if (v !== undefined) this.showSuspiciousAlert.set(!!v) } @Input('class') className?: string readonly otherSessions = signal<SessionDevice[]>(stubOtherSessions.map((s) => ({ ...s }))) readonly showSuspiciousAlert = signal(true) readonly revokingAll = signal(false) readonly confirmingRevokeId = signal<string | null>(null) private confirmTimer: ReturnType<typeof setTimeout> | undefined private revokeAllTimer: ReturnType<typeof setTimeout> | undefined constructor() { inject(DestroyRef).onDestroy(() => { clearTimeout(this.confirmTimer) clearTimeout(this.revokeAllTimer) }) } get rootClass(): string { return cn('mx-auto w-full max-w-4xl space-y-6', this.className) } customBadgeVariant(session: SessionDevice): BadgeVariant { return (session.customBadge?.variant ?? 'secondary') as BadgeVariant } revokeButtonClass(id: string): string { return this.confirmingRevokeId() === id ? 'bg-destructive hover:bg-destructive/90 border-transparent text-white' : 'text-destructive border-destructive/30 hover:bg-destructive/10 hover:text-destructive' } dismissAlert(): void { this.showSuspiciousAlert.set(false) } revokeSuspicious(): void { this.otherSessions.update((prev) => prev.filter((s) => !s.isSuspicious)) this.showSuspiciousAlert.set(false) } requestRevoke(id: string): void { if (this.confirmingRevokeId() === id) { this.revokeSession(id) return } this.confirmingRevokeId.set(id) clearTimeout(this.confirmTimer) this.confirmTimer = setTimeout(() => { this.confirmingRevokeId.set(null) }, 3000) } cancelRevoke(id: string): void { if (this.confirmingRevokeId() === id) { clearTimeout(this.confirmTimer) this.confirmingRevokeId.set(null) } } revokeSession(id: string): void { clearTimeout(this.confirmTimer) this.confirmingRevokeId.set(null) const target = this.otherSessions().find((s) => s.id === id) if (target?.isSuspicious) { this.showSuspiciousAlert.set(false) } this.otherSessions.update((prev) => prev.filter((s) => s.id !== id)) } revokeAllOtherSessions(): void { this.revokingAll.set(true) this.otherSessions.set([]) this.showSuspiciousAlert.set(false) clearTimeout(this.revokeAllTimer) this.revokeAllTimer = setTimeout(() => { this.revokingAll.set(false) }, 600) } }src/app/components/blocks/session-device-manager/index.ts0.1 kB
export { UiSessionDeviceManagerComponent, type SessionDevice } from './session-device-manager.component'
Raw manifest:https://uipkge.dev/r/angular/session-device-manager.json