121 lines
3.2 KiB
TypeScript
121 lines
3.2 KiB
TypeScript
|
|
import * as appstate from '../../appstate.js';
|
||
|
|
|
||
|
|
import {
|
||
|
|
DeesElement,
|
||
|
|
customElement,
|
||
|
|
html,
|
||
|
|
state,
|
||
|
|
css,
|
||
|
|
cssManager,
|
||
|
|
type TemplateResult,
|
||
|
|
} from '@design.estate/dees-element';
|
||
|
|
import { type IStatsTile } from '@design.estate/dees-catalog';
|
||
|
|
|
||
|
|
declare global {
|
||
|
|
interface HTMLElementTagNameMap {
|
||
|
|
'ops-view-security-authentication': OpsViewSecurityAuthentication;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
@customElement('ops-view-security-authentication')
|
||
|
|
export class OpsViewSecurityAuthentication extends DeesElement {
|
||
|
|
@state()
|
||
|
|
accessor statsState: appstate.IStatsState = appstate.statsStatePart.getState()!;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
super();
|
||
|
|
const sub = appstate.statsStatePart
|
||
|
|
.select((s) => s)
|
||
|
|
.subscribe((s) => {
|
||
|
|
this.statsState = s;
|
||
|
|
});
|
||
|
|
this.rxSubscriptions.push(sub);
|
||
|
|
}
|
||
|
|
|
||
|
|
public static styles = [
|
||
|
|
cssManager.defaultStyles,
|
||
|
|
css`
|
||
|
|
:host { display: block; }
|
||
|
|
h2 {
|
||
|
|
margin: 32px 0 16px 0;
|
||
|
|
font-size: 24px;
|
||
|
|
font-weight: 600;
|
||
|
|
color: ${cssManager.bdTheme('#333', '#ccc')};
|
||
|
|
}
|
||
|
|
dees-statsgrid {
|
||
|
|
margin-bottom: 32px;
|
||
|
|
}
|
||
|
|
`,
|
||
|
|
];
|
||
|
|
|
||
|
|
public render(): TemplateResult {
|
||
|
|
const metrics = this.statsState.securityMetrics;
|
||
|
|
|
||
|
|
if (!metrics) {
|
||
|
|
return html`
|
||
|
|
<div class="loadingMessage">
|
||
|
|
<p>Loading security metrics...</p>
|
||
|
|
</div>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Derive auth events from recentEvents
|
||
|
|
const allEvents: any[] = metrics.recentEvents || [];
|
||
|
|
const authEvents = allEvents.filter((evt: any) => evt.type === 'authentication');
|
||
|
|
const successfulLogins = authEvents.filter((evt: any) => evt.success === true).length;
|
||
|
|
|
||
|
|
const tiles: IStatsTile[] = [
|
||
|
|
{
|
||
|
|
id: 'authFailures',
|
||
|
|
title: 'Authentication Failures',
|
||
|
|
value: metrics.authenticationFailures,
|
||
|
|
type: 'number',
|
||
|
|
icon: 'lucide:LockOpen',
|
||
|
|
color: metrics.authenticationFailures > 10 ? '#ef4444' : '#f59e0b',
|
||
|
|
description: 'Failed authentication attempts today',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
id: 'successfulLogins',
|
||
|
|
title: 'Successful Logins',
|
||
|
|
value: successfulLogins,
|
||
|
|
type: 'number',
|
||
|
|
icon: 'lucide:Lock',
|
||
|
|
color: '#22c55e',
|
||
|
|
description: 'Successful logins today',
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
// Map auth events to login history table data
|
||
|
|
const loginHistory = authEvents.map((evt: any) => ({
|
||
|
|
timestamp: evt.timestamp,
|
||
|
|
username: evt.details?.username || 'unknown',
|
||
|
|
ipAddress: evt.ipAddress || 'unknown',
|
||
|
|
success: evt.success ?? false,
|
||
|
|
reason: evt.success ? '' : evt.message || 'Authentication failed',
|
||
|
|
}));
|
||
|
|
|
||
|
|
return html`
|
||
|
|
<dees-heading level="hr">Authentication</dees-heading>
|
||
|
|
|
||
|
|
<dees-statsgrid
|
||
|
|
.tiles=${tiles}
|
||
|
|
.minTileWidth=${200}
|
||
|
|
></dees-statsgrid>
|
||
|
|
|
||
|
|
<h2>Recent Login Attempts</h2>
|
||
|
|
<dees-table
|
||
|
|
.heading1=${'Login History'}
|
||
|
|
.heading2=${'Recent authentication attempts'}
|
||
|
|
.data=${loginHistory}
|
||
|
|
.displayFunction=${(item) => ({
|
||
|
|
'Time': new Date(item.timestamp).toLocaleString(),
|
||
|
|
'Username': item.username,
|
||
|
|
'IP Address': item.ipAddress,
|
||
|
|
'Status': item.success ? 'Success' : 'Failed',
|
||
|
|
'Reason': item.reason || '-',
|
||
|
|
})}
|
||
|
|
></dees-table>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
}
|