feat(vpn,target-profiles,migrations): add startup data migrations, support scoped VPN route allow entries, and rename target profile hosts to ips

This commit is contained in:
2026-04-07 21:02:37 +00:00
parent f29ed9757e
commit 7fa6d82e58
24 changed files with 1503 additions and 1563 deletions

View File

@@ -30,6 +30,20 @@ export class OpsViewSecurity extends DeesElement {
@state()
accessor selectedTab: 'overview' | 'blocked' | 'authentication' | 'email-security' = 'overview';
private tabLabelMap: Record<string, string> = {
'overview': 'Overview',
'blocked': 'Blocked IPs',
'authentication': 'Authentication',
'email-security': 'Email Security',
};
private labelToTab: Record<string, 'overview' | 'blocked' | 'authentication' | 'email-security'> = {
'Overview': 'overview',
'Blocked IPs': 'blocked',
'Authentication': 'authentication',
'Email Security': 'email-security',
};
constructor() {
super();
const subscription = appstate.statsStatePart
@@ -40,35 +54,23 @@ export class OpsViewSecurity extends DeesElement {
this.rxSubscriptions.push(subscription);
}
async firstUpdated() {
const toggle = this.shadowRoot!.querySelector('dees-input-multitoggle') as any;
if (toggle) {
const sub = toggle.changeSubject.subscribe(() => {
const tab = this.labelToTab[toggle.selectedOption];
if (tab) this.selectedTab = tab;
});
this.rxSubscriptions.push(sub);
}
}
public static styles = [
cssManager.defaultStyles,
shared.viewHostCss,
css`
.tabs {
display: flex;
gap: 8px;
dees-input-multitoggle {
margin-bottom: 24px;
border-bottom: 2px solid ${cssManager.bdTheme('#e9ecef', '#333')};
}
.tab {
padding: 12px 24px;
background: none;
border: none;
border-bottom: 2px solid transparent;
cursor: pointer;
font-size: 16px;
color: ${cssManager.bdTheme('#666', '#999')};
transition: all 0.2s ease;
}
.tab:hover {
color: ${cssManager.bdTheme('#333', '#ccc')};
}
.tab.active {
color: ${cssManager.bdTheme('#2196F3', '#4a90e2')};
border-bottom-color: ${cssManager.bdTheme('#2196F3', '#4a90e2')};
}
h2 {
@@ -91,135 +93,22 @@ export class OpsViewSecurity extends DeesElement {
overflow: hidden;
}
.securityCard.alert {
border-color: ${cssManager.bdTheme('#f44336', '#ff6666')};
background: ${cssManager.bdTheme('#ffebee', '#4a1f1f')};
}
.securityCard.warning {
border-color: ${cssManager.bdTheme('#ff9800', '#ffaa33')};
background: ${cssManager.bdTheme('#fff3e0', '#4a3a1f')};
}
.securityCard.success {
border-color: ${cssManager.bdTheme('#4caf50', '#66cc66')};
background: ${cssManager.bdTheme('#e8f5e9', '#1f3f1f')};
}
.cardHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.cardTitle {
font-size: 18px;
font-weight: 600;
color: ${cssManager.bdTheme('#333', '#ccc')};
}
.cardStatus {
font-size: 14px;
padding: 4px 12px;
border-radius: 16px;
font-weight: 500;
}
.status-critical {
background: ${cssManager.bdTheme('#f44336', '#ff6666')};
color: ${cssManager.bdTheme('#fff', '#fff')};
}
.status-warning {
background: ${cssManager.bdTheme('#ff9800', '#ffaa33')};
color: ${cssManager.bdTheme('#fff', '#fff')};
}
.status-good {
background: ${cssManager.bdTheme('#4caf50', '#66cc66')};
color: ${cssManager.bdTheme('#fff', '#fff')};
}
.metricValue {
font-size: 32px;
font-weight: 700;
margin-bottom: 8px;
}
.metricLabel {
font-size: 14px;
color: ${cssManager.bdTheme('#666', '#999')};
}
.actionButton {
margin-top: 16px;
}
.blockedIpList {
max-height: 400px;
overflow-y: auto;
}
.blockedIpItem {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-bottom: 1px solid ${cssManager.bdTheme('#e9ecef', '#333')};
}
.blockedIpItem:last-child {
border-bottom: none;
}
.ipAddress {
font-family: 'Consolas', 'Monaco', monospace;
font-weight: 600;
}
.blockReason {
font-size: 14px;
color: ${cssManager.bdTheme('#666', '#999')};
}
.blockTime {
font-size: 12px;
color: ${cssManager.bdTheme('#999', '#666')};
}
`,
];
public render() {
return html`
<dees-heading level="2">Security</dees-heading>
<div class="tabs">
<button
class="tab ${this.selectedTab === 'overview' ? 'active' : ''}"
@click=${() => this.selectedTab = 'overview'}
>
Overview
</button>
<button
class="tab ${this.selectedTab === 'blocked' ? 'active' : ''}"
@click=${() => this.selectedTab = 'blocked'}
>
Blocked IPs
</button>
<button
class="tab ${this.selectedTab === 'authentication' ? 'active' : ''}"
@click=${() => this.selectedTab = 'authentication'}
>
Authentication
</button>
<button
class="tab ${this.selectedTab === 'email-security' ? 'active' : ''}"
@click=${() => this.selectedTab = 'email-security'}
>
Email Security
</button>
</div>
<dees-input-multitoggle
.type=${'single'}
.options=${['Overview', 'Blocked IPs', 'Authentication', 'Email Security']}
.selectedOption=${this.tabLabelMap[this.selectedTab]}
></dees-input-multitoggle>
${this.renderTabContent()}
`;
@@ -328,32 +217,53 @@ export class OpsViewSecurity extends DeesElement {
}
private renderBlockedIPs(metrics: any) {
const blockedIPs: string[] = metrics.blockedIPs || [];
const tiles: IStatsTile[] = [
{
id: 'totalBlocked',
title: 'Blocked IPs',
value: blockedIPs.length,
type: 'number',
icon: 'lucide:ShieldBan',
color: blockedIPs.length > 0 ? '#ef4444' : '#22c55e',
description: 'Currently blocked addresses',
},
];
return html`
<div class="securityCard">
<div class="cardHeader">
<h3 class="cardTitle">Blocked IP Addresses</h3>
<dees-button @click=${() => this.clearBlockedIPs()}>
Clear All
</dees-button>
</div>
<div class="blockedIpList">
${metrics.blockedIPs && metrics.blockedIPs.length > 0 ? metrics.blockedIPs.map((ipAddress, index) => html`
<div class="blockedIpItem">
<div>
<div class="ipAddress">${ipAddress}</div>
<div class="blockReason">Suspicious activity</div>
<div class="blockTime">Blocked</div>
</div>
<dees-button @click=${() => this.unblockIP(ipAddress)}>
Unblock
</dees-button>
</div>
`) : html`
<p>No blocked IPs</p>
`}
</div>
</div>
<dees-statsgrid
.tiles=${tiles}
.minTileWidth=${200}
></dees-statsgrid>
<dees-table
.heading1=${'Blocked IP Addresses'}
.heading2=${'IPs blocked due to suspicious activity'}
.data=${blockedIPs.map((ip) => ({ ip }))}
.displayFunction=${(item) => ({
'IP Address': item.ip,
'Reason': 'Suspicious activity',
})}
.dataActions=${[
{
name: 'Unblock',
iconName: 'lucide:shield-off',
type: ['contextmenu' as const],
actionFunc: async (item) => {
await this.unblockIP(item.ip);
},
},
{
name: 'Clear All',
iconName: 'lucide:trash-2',
type: ['header' as const],
actionFunc: async () => {
await this.clearBlockedIPs();
},
},
]}
></dees-table>
`;
}