mirror of
https://github.com/MikroWizard/mikrofront.git
synced 2026-05-14 15:51:29 +00:00
621 lines
No EOL
32 KiB
HTML
621 lines
No EOL
32 KiB
HTML
<div class="fade-in">
|
|
<!-- Top Cards -->
|
|
<c-row class="mb-4 d-flex justify-content-between">
|
|
<!-- Server Status Card -->
|
|
<c-col sm="4">
|
|
<c-card class="shadow-sm border-0 h-100"
|
|
[ngClass]="{'bg-success text-white': status?.status === 'running', 'bg-danger text-white': status?.status === 'error' || !status, 'bg-warning text-dark': status?.status === 'setup_required'}">
|
|
<c-card-body class="p-3 d-flex align-items-center">
|
|
<div class="me-3 fs-2 opacity-75">
|
|
<i class="fas fa-server"></i>
|
|
</div>
|
|
<div>
|
|
<div class="text-uppercase fw-semibold" style="font-size: 0.75rem; letter-spacing: 0.5px; opacity: 0.8;">
|
|
Server Status</div>
|
|
<div class="fs-5 fw-bold mt-1">
|
|
{{status?.status === 'running' ? 'Running' : (status?.status === 'setup_required' ? 'Setup Required' :
|
|
'Error/Offline')}}
|
|
</div>
|
|
</div>
|
|
</c-card-body>
|
|
</c-card>
|
|
</c-col>
|
|
|
|
<!-- Total Peers Card -->
|
|
<c-col sm="4">
|
|
<c-card class="shadow-sm border-0 h-100 bg-primary text-white">
|
|
<c-card-body class="p-3 d-flex align-items-center">
|
|
<div class="me-3 fs-2 opacity-75">
|
|
<i class="fas fa-network-wired"></i>
|
|
</div>
|
|
<div>
|
|
<div class="text-uppercase fw-semibold" style="font-size: 0.75rem; letter-spacing: 0.5px; opacity: 0.8;">
|
|
Connected Peers</div>
|
|
<div class="fs-5 fw-bold mt-1">
|
|
{{ source.length }} Active
|
|
</div>
|
|
</div>
|
|
</c-card-body>
|
|
</c-card>
|
|
</c-col>
|
|
|
|
<!-- Total Traffic Card -->
|
|
<c-col sm="4">
|
|
<c-card class="shadow-sm border-0 h-100 bg-info text-white position-relative">
|
|
<button class="btn btn-sm btn-light position-absolute top-0 end-0 m-2" style="opacity: 0.8; z-index: 2;"
|
|
(click)="promptResetServer()" cTooltip="Reset Server Counters">
|
|
<i class="fa-solid fa-redo"></i>
|
|
</button>
|
|
<c-card-body class="p-3 d-flex align-items-center">
|
|
<div class="me-3 fs-3 opacity-75">
|
|
<i class="fas fa-satellite-dish"></i>
|
|
</div>
|
|
<div class="w-100">
|
|
<div class="d-flex justify-content-between text-uppercase fw-semibold"
|
|
style="font-size: 0.70rem; letter-spacing: 0.5px; opacity: 0.8;">
|
|
<span>Live Speed</span>
|
|
<span>Total Vol</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mt-1 w-100">
|
|
<!-- Live Speeds -->
|
|
<div class="d-flex flex-column" style="font-size: 0.8rem; font-weight: 500;">
|
|
<span class="text-nowrap" [ngClass]="{'text-success': liveSpeedRx !== 0}">⬇ {{ formatBytes(liveSpeedRx)
|
|
}}/s</span>
|
|
<span class="text-nowrap" [ngClass]="{'text-warning': liveSpeedTx !== 0}">⬆ {{ formatBytes(liveSpeedTx)
|
|
}}/s</span>
|
|
</div>
|
|
|
|
<!-- Total Volumes -->
|
|
<div class="d-flex flex-column text-end" style="font-size: 0.8rem; font-weight: 500;">
|
|
<span class="text-nowrap">⬇ {{ (totalRx / 1048576) | number:'1.1-2' }} MB</span>
|
|
<span class="text-nowrap">⬆ {{ (totalTx / 1048576) | number:'1.1-2' }} MB</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</c-card-body>
|
|
</c-card>
|
|
</c-col>
|
|
</c-row>
|
|
|
|
<!-- Traffic Chart -->
|
|
<c-row class="mb-4">
|
|
<c-col sm="12">
|
|
<c-card class="shadow-sm border-0">
|
|
<c-card-header>
|
|
<strong>Live Server Traffic</strong> <small class="text-muted">(Total Cumulative MB)</small>
|
|
</c-card-header>
|
|
<c-card-body>
|
|
<div class="chart-wrapper" style="height: 250px;">
|
|
<c-chart [data]="chartData" [options]="chartOptions" type="line" height="250"></c-chart>
|
|
</div>
|
|
</c-card-body>
|
|
</c-card>
|
|
</c-col>
|
|
</c-row>
|
|
|
|
<!-- Peers Table -->
|
|
<c-row>
|
|
<c-col xs>
|
|
<c-card class="mb-4">
|
|
<c-card-header>
|
|
<c-row>
|
|
<c-col xs [lg]="3">
|
|
<strong>VPN Peers</strong>
|
|
</c-col>
|
|
<c-col xs [lg]="9">
|
|
<h6 style="text-align: right;">
|
|
<button cButton color="primary" (click)="openServerConfigModal()" class="mx-1" size="sm">
|
|
<i class="fas fa-cogs"></i> Server Config
|
|
</button>
|
|
<button cButton color="success" (click)="openAddPeerModal()" class="mx-1" size="sm"
|
|
style="color: #fff;">
|
|
<i class="fa-solid fa-plus"></i> Add Peer
|
|
</button>
|
|
</h6>
|
|
</c-col>
|
|
</c-row>
|
|
</c-card-header>
|
|
<c-card-body>
|
|
<div class="mb-3 d-flex justify-content-end">
|
|
<span class="p-input-icon-left">
|
|
<i class="pi pi-search"></i>
|
|
<input type="text" pInputText placeholder="Search peers..." (input)="applyFilterGlobal($event, 'contains')"
|
|
class="form-control-sm" />
|
|
</span>
|
|
</div>
|
|
|
|
<p-table #dt [value]="source" [paginator]="true" [rows]="10" [showCurrentPageReport]="true"
|
|
[rowsPerPageOptions]="[10, 25, 50]" [resizableColumns]="true" columnResizeMode="expand"
|
|
[showGridlines]="true" [stripedRows]="true"
|
|
styleClass="p-datatable-sm p-datatable-gridlines p-datatable-striped"
|
|
[globalFilterFields]="['name', 'assigned_ip', 'public_key', 'description']"
|
|
[loading]="loading">
|
|
<ng-template pTemplate="header">
|
|
<tr>
|
|
<th style="width: 50px" class="text-center" pResizableColumn>Status</th>
|
|
<th pSortableColumn="name" style="width: 200px" pResizableColumn>
|
|
<div class="d-flex justify-content-between">
|
|
<span>Identity & IP</span>
|
|
<span>
|
|
<p-sortIcon field="name"></p-sortIcon>
|
|
<p-columnFilter type="text" field="name" display="menu" class="ms-auto" />
|
|
</span>
|
|
</div>
|
|
</th>
|
|
<th pSortableColumn="description" pResizableColumn>
|
|
<div class="d-flex justify-content-between">
|
|
<span>Details & Description</span>
|
|
<span>
|
|
<p-sortIcon field="description"></p-sortIcon>
|
|
<p-columnFilter type="text" field="description" display="menu" class="ms-auto" />
|
|
</span>
|
|
</div>
|
|
</th>
|
|
<th pSortableColumn="nat_mode" style="width: 140px" pResizableColumn>
|
|
<div class="d-flex justify-content-between">
|
|
<span>Routing</span>
|
|
<span>
|
|
<p-sortIcon field="nat_mode"></p-sortIcon>
|
|
<p-columnFilter type="text" field="nat_mode" display="menu" class="ms-auto" />
|
|
</span>
|
|
</div>
|
|
</th>
|
|
<th style="width: 220px" class="text-center" pResizableColumn>Integration</th>
|
|
<th pSortableColumn="stats.last_handshake" style="width: 130px" class="text-center" pResizableColumn>
|
|
<div class="d-flex justify-content-between">
|
|
<span>Last Handshake</span>
|
|
<p-sortIcon field="stats.last_handshake"></p-sortIcon>
|
|
</div>
|
|
</th>
|
|
<th style="width: 130px" pResizableColumn>Transfer Speed</th>
|
|
<th style="width: 120px" pResizableColumn>Total Volume</th>
|
|
<th pSortableColumn="is_enabled" style="width: 90px" class="text-center" pResizableColumn>
|
|
<div class="d-flex justify-content-between">
|
|
<span>State</span>
|
|
<p-sortIcon field="is_enabled"></p-sortIcon>
|
|
</div>
|
|
</th>
|
|
<th style="width: 70px" class="text-center" pResizableColumn>Actions</th>
|
|
</tr>
|
|
</ng-template>
|
|
|
|
<ng-template pTemplate="body" let-item>
|
|
<tr [ngClass]="{'row-highlighted': item.status === 'online'}">
|
|
<td class="text-center">
|
|
<i *ngIf="item.status === 'online'" class="fa-solid fa-circle text-success" [pTooltip]="'Online'" tooltipPosition="top"></i>
|
|
<i *ngIf="item.status === 'offline'" class="fa-solid fa-circle text-danger" [pTooltip]="'Offline'" tooltipPosition="top"></i>
|
|
<i *ngIf="item.status === 'unreachable'" class="fa-solid fa-triangle-exclamation text-warning"
|
|
[pTooltip]="'Unreachable (Handshake OK, Ping Failed)'" tooltipPosition="top"></i>
|
|
<i *ngIf="!item.status && item.is_enabled" class="fa-solid fa-circle text-info" [pTooltip]="'Enabled'" tooltipPosition="top"></i>
|
|
<i *ngIf="!item.status && !item.is_enabled" class="fa-solid fa-circle text-secondary"
|
|
[pTooltip]="'Disabled'" tooltipPosition="top"></i>
|
|
</td>
|
|
<td>
|
|
<div style="line-height: 1.2;">
|
|
<i *ngIf="item.mt_user" class="fas fa-server text-info me-1" [pTooltip]="'Managed MikroTik Device'"
|
|
style="vertical-align: middle; font-size: 0.9rem;"></i>
|
|
<strong class="text-primary fs-6 text-truncate d-inline-block"
|
|
style="max-width: 140px; vertical-align: middle;" [title]="item.name || item.assigned_ip">
|
|
{{ item.name || item.assigned_ip || (item.public_key | slice:0:8) + '...' }}
|
|
</strong>
|
|
<c-badge *ngIf="item.is_managed === false" color="secondary" class="ms-1"
|
|
style="vertical-align: middle; font-size: 0.65rem;">Unmanaged</c-badge>
|
|
</div>
|
|
<div style="font-size: 0.85rem; color: #495057;" class="fw-semibold ms-2">
|
|
<i class="fas fa-network-wired opacity-75"></i> {{ item.assigned_ip || 'No IP' }}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="d-flex flex-column justify-content-center w-100">
|
|
<div *ngIf="item.description" class="text-muted mb-1 text-wrap small fw-bold" [title]="item.description">
|
|
{{ item.description }}
|
|
</div>
|
|
<div style="color: #adb5bd; font-size: 0.75rem" class="d-flex flex-row align-items-center gap-3">
|
|
<div class="text-truncate" title="Public Key" style="max-width: 140px;">
|
|
<i class="fas fa-key me-1 opacity-75"></i> {{ item.public_key | slice:0:16 }}...
|
|
</div>
|
|
<div class="text-truncate" title="Created At">
|
|
<i class="fas fa-clock me-1 opacity-75"></i> {{ item.created_at | date:'mediumDate' }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="mb-1">
|
|
<c-badge size="sm"
|
|
[color]="item.nat_mode === 'full' ? 'success' : (item.nat_mode === 'split' ? 'info' : 'secondary')">
|
|
{{ item.nat_mode | uppercase }}
|
|
</c-badge>
|
|
</div>
|
|
<div style="font-size: 0.75rem;" class="text-muted mt-1">
|
|
<i class="fas fa-network-wired small opacity-50"></i> Iface: {{ item.custom_interface || 'Auto' }}
|
|
</div>
|
|
</td>
|
|
<td class="text-center">
|
|
<div class="d-flex flex-column align-items-center gap-1">
|
|
<a *ngIf="item.linked_device_id" [routerLink]="['/device-stats', {id: item.linked_device_id}]"
|
|
target="_blank" class="btn btn-sm btn-outline-primary py-0 px-2" style="font-size: 0.7rem;">
|
|
<i class="fa-solid fa-link"></i> View Device
|
|
</a>
|
|
<div class="d-flex flex-row justify-content-center w-100 container-fluid p-0">
|
|
<small *ngIf="!item.linked_device_id && !item.scan_status && item.mt_user" class="text-muted" style="font-size: 0.65rem;">Not Linked</small>
|
|
<div style="font-size: 0.7rem;" *ngIf="item.scan_status === 'starting' || item.scan_status === 'running'" class="text-info">
|
|
<i class="fas fa-circle-notch fa-spin me-1"></i> Scanning...
|
|
</div>
|
|
<div style="font-size: 0.7rem;" *ngIf="item.scan_status === 'completed'" class="text-success">
|
|
<i class="fas fa-check-circle me-1"></i> Success
|
|
</div>
|
|
<div style="font-size: 0.7rem;" *ngIf="item.scan_status === 'failed'" class="text-danger" [pTooltip]="'MikroTik connection failed'" tooltipPosition="top">
|
|
<i class="fas fa-exclamation-triangle me-1"></i> Failed
|
|
</div>
|
|
</div>
|
|
<div style="font-size: 0.65rem;" class="text-secondary opacity-75">
|
|
<i class="fas fa-heartbeat"></i> Keepalive: {{ item.persistent_keepalive }}s
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="text-center small">
|
|
<div *ngIf="item.stats?.last_handshake > 0" class="text-secondary fw-semibold">
|
|
<i class="fas fa-handshake"></i> {{ item.stats.last_handshake * 1000 | date:'shortTime' }}<br>
|
|
<small class="text-muted">{{ item.stats.last_handshake * 1000 | date:'shortDate' }}</small>
|
|
</div>
|
|
<div *ngIf="!item.stats?.last_handshake || item.stats.last_handshake === 0" class="text-muted">
|
|
<i class="fas fa-handshake"></i> None
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div *ngIf="item.stats" class="d-flex flex-column" style="font-size: 0.72rem; font-family: monospace; font-weight: bold;">
|
|
<span [style.color]="item.stats.rx_speed !== 0 ? '#2eb85c' : '#8a93a2'">
|
|
<i class="fas fa-arrow-down opacity-75"></i> {{ formatBytes(item.stats.rx_speed || 0) }}/s
|
|
</span>
|
|
<span [style.color]="item.stats.tx_speed !== 0 ? '#3399ff' : '#8a93a2'">
|
|
<i class="fas fa-arrow-up opacity-75"></i> {{ formatBytes(item.stats.tx_speed || 0) }}/s
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div *ngIf="item.stats" class="d-flex flex-column" style="font-size: 0.7rem;">
|
|
<span class="text-success"><i class="fas fa-arrow-down opacity-50"></i> {{(item.stats.rx_bytes / 1048576) | number:'1.2-2'}} MB</span>
|
|
<span class="text-info"><i class="fas fa-arrow-up opacity-50"></i> {{(item.stats.tx_bytes / 1048576) | number:'1.2-2'}} MB</span>
|
|
</div>
|
|
</td>
|
|
<td class="text-center">
|
|
<c-form-check [switch]="true" class="d-inline-block">
|
|
<input cFormCheckInput type="checkbox" [checked]="item.is_enabled"
|
|
(click)="$event.preventDefault(); promptToggleEnabled(item)"
|
|
[disabled]="item.is_managed === false" [id]="'switch-peer-' + item.public_key" />
|
|
</c-form-check>
|
|
</td>
|
|
<td class="text-center">
|
|
<button color="primary" shape="rounded-0" variant="ghost" style="padding: 4px 7px;"
|
|
[matMenuTriggerFor]="menu" cButton>
|
|
<i class="fa-solid fa-bars"></i>
|
|
</button>
|
|
<mat-menu #menu="matMenu">
|
|
<div cListGroup>
|
|
<button size="sm" cListGroupItem (click)="openEditModal(item)" *ngIf="item.is_managed !== false">
|
|
<i class="fa-solid fa-pencil text-primary"></i><small> Edit</small>
|
|
</button>
|
|
<button *ngIf="item.mt_user && !item.linked_device_id" size="sm" cListGroupItem
|
|
(click)="scanDevice(item)">
|
|
<i class="fa-solid fa-magnifying-glass text-info"></i><small> Manual Scan</small>
|
|
</button>
|
|
<button size="sm" cListGroupItem (click)="openScriptModal(item)">
|
|
<i class="fa-solid fa-terminal text-secondary"></i><small> MikroTik Script</small>
|
|
</button>
|
|
<button size="sm" cListGroupItem (click)="openQrModal(item)">
|
|
<i class="fa-solid fa-qrcode text-secondary"></i><small> QR Code</small>
|
|
</button>
|
|
<button size="sm" cListGroupItem (click)="downloadPeerConfigDirect(item)">
|
|
<i class="fa-solid fa-download text-secondary"></i><small> Download Config</small>
|
|
</button>
|
|
<button size="sm" cListGroupItem (click)="promptResetPeer(item)">
|
|
<i class="fa-solid fa-sync text-warning"></i><small> Reset Counters</small>
|
|
</button>
|
|
<button size="sm" cListGroupItem (click)="confirmDelete(item)">
|
|
<i class="fa-solid fa-trash text-danger"></i><small> Delete</small>
|
|
</button>
|
|
</div>
|
|
</mat-menu>
|
|
</td>
|
|
</tr>
|
|
</ng-template>
|
|
|
|
<ng-template pTemplate="emptymessage">
|
|
<tr>
|
|
<td colspan="10" class="text-center p-4">No VPN peers found.</td>
|
|
</tr>
|
|
</ng-template>
|
|
</p-table>
|
|
</c-card-body>
|
|
</c-card>
|
|
</c-col>
|
|
</c-row>
|
|
<!-- MODALS -->
|
|
|
|
<!-- Server Config Modal -->
|
|
<c-modal #ServerConfigModal backdrop="static" [(visible)]="serverConfigModalVisible" id="ServerConfigModal">
|
|
<c-modal-header>
|
|
<h6 cModalTitle>VPN Server Configuration</h6>
|
|
<button [cModalToggle]="ServerConfigModal.id" cButtonClose></button>
|
|
</c-modal-header>
|
|
<c-modal-body>
|
|
<c-input-group class="mb-3">
|
|
<span cInputGroupText>API Endpoint</span>
|
|
<input cFormControl [(ngModel)]="serverConfig.api_endpoint" placeholder="http://127.0.0.1:5000" />
|
|
</c-input-group>
|
|
<c-input-group class="mb-3">
|
|
<span cInputGroupText>API Token</span>
|
|
<input cFormControl type="password" [(ngModel)]="serverConfig.api_token" placeholder="Optional Auth Token" />
|
|
</c-input-group>
|
|
<c-input-group class="mb-3">
|
|
<span cInputGroupText>VPN Subnet</span>
|
|
<input cFormControl [(ngModel)]="serverConfig.vpn_subnet" placeholder="10.8.0.0/24" />
|
|
</c-input-group>
|
|
<c-input-group class="mb-3">
|
|
<span cInputGroupText>Public Server IP/Host</span>
|
|
<input cFormControl [(ngModel)]="serverConfig.public_server_ip" placeholder="vpn.mikrowizard.com" />
|
|
</c-input-group>
|
|
</c-modal-body>
|
|
<c-modal-footer>
|
|
<button cButton color="primary" (click)="saveServerConfig()">Save Changes</button>
|
|
<button cButton color="secondary" (click)="serverConfigModalVisible = false">Cancel</button>
|
|
</c-modal-footer>
|
|
</c-modal>
|
|
|
|
<!-- Delete Confirm Modal -->
|
|
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="deleteModalVisible" id="DeleteConfirmModal">
|
|
<c-modal-header>
|
|
<h6 cModalTitle class="text-danger">Warning: Delete Peer</h6>
|
|
<button [cModalToggle]="DeleteConfirmModal.id" cButtonClose></button>
|
|
</c-modal-header>
|
|
<c-modal-body>
|
|
<p class="text-danger">
|
|
<em>Warning: This will permanently remove the VPN tunnel and NAT rules for this peer. Connected devices will
|
|
lose connection immediately.</em>
|
|
</p>
|
|
<p>Are you sure you want to delete peer <strong>{{ peerToDelete?.assigned_ip }}</strong>?</p>
|
|
</c-modal-body>
|
|
<c-modal-footer>
|
|
<button cButton color="danger" (click)="executeDelete()">Yes, Delete Peer</button>
|
|
<button cButton color="secondary" (click)="deleteModalVisible = false">Cancel</button>
|
|
</c-modal-footer>
|
|
</c-modal>
|
|
|
|
<!-- Toggle Confirm Modal -->
|
|
<c-modal #ToggleConfirmModal backdrop="static" [(visible)]="toggleModalVisible" id="ToggleConfirmModal">
|
|
<c-modal-header>
|
|
<h6 cModalTitle>Confirm Status Change</h6>
|
|
<button [cModalToggle]="ToggleConfirmModal.id" cButtonClose></button>
|
|
</c-modal-header>
|
|
<c-modal-body>
|
|
<p>Are you sure you want to <strong [ngClass]="peerToToggle?.is_enabled ? 'text-danger' : 'text-success'">{{
|
|
peerToToggle?.is_enabled ? 'Disable' : 'Enable' }}</strong> peer <strong class="text-primary">{{
|
|
peerToToggle?.assigned_ip }}</strong>?</p>
|
|
</c-modal-body>
|
|
<c-modal-footer>
|
|
<button cButton [color]="peerToToggle?.is_enabled ? 'danger' : 'success'" (click)="confirmToggle()">Yes, {{
|
|
peerToToggle?.is_enabled ? 'Disable' : 'Enable' }}</button>
|
|
<button cButton color="secondary" (click)="toggleModalVisible = false">Cancel</button>
|
|
</c-modal-footer>
|
|
</c-modal>
|
|
|
|
<!-- Script Viewer Modal -->
|
|
<c-modal #ScriptModal backdrop="static" [fullscreen]="true" [(visible)]="scriptModalVisible" id="ScriptModal">
|
|
<c-modal-header>
|
|
<h6 cModalTitle>MikroTik Provisioning Script</h6>
|
|
<button [cModalToggle]="ScriptModal.id" cButtonClose></button>
|
|
</c-modal-header>
|
|
<c-modal-body>
|
|
<div *ngIf="configResult.script">
|
|
<div style="overflow-y: auto; background-color: #f8f9fa;">
|
|
<div highlight-js lang="routeros" [options]="{}">{{
|
|
configResult.script }}</div>
|
|
</div>
|
|
|
|
<div class="mt-3 text-end">
|
|
<button cButton color="primary" [cdkCopyToClipboard]="configResult.script || ''" (click)="copyScript()">
|
|
<i class="fas fa-copy"></i> Copy to Clipboard
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</c-modal-body>
|
|
<c-modal-footer>
|
|
<button cButton color="secondary" (click)="scriptModalVisible = false">Close</button>
|
|
</c-modal-footer>
|
|
</c-modal>
|
|
|
|
<!-- QR Viewer Modal -->
|
|
<c-modal #QrModal backdrop="static" [(visible)]="qrModalVisible" id="QrModal">
|
|
<c-modal-header>
|
|
<h6 cModalTitle>Mobile Quick Setup (QR Code)</h6>
|
|
<button [cModalToggle]="QrModal.id" cButtonClose></button>
|
|
</c-modal-header>
|
|
<c-modal-body>
|
|
<div *ngIf="configResult.qrBlobUrl" class="text-center mb-3">
|
|
<img [src]="configResult.qrBlobUrl" alt="WireGuard QR Code"
|
|
style="max-width: 250px; border: 1px solid #ddd; padding: 10px; border-radius: 8px;" />
|
|
</div>
|
|
<div class="alert alert-info py-2 m-0" style="font-size: 0.85rem;">
|
|
<i class="fas fa-info-circle"></i> <strong>How to connect:</strong> Open the official WireGuard app on your
|
|
mobile device, tap the <i class="fas fa-plus"></i> button, and select <strong>"Create from QR code"</strong>.
|
|
</div>
|
|
</c-modal-body>
|
|
<c-modal-footer class="d-flex justify-content-between w-100">
|
|
<div>
|
|
<button cButton color="primary" variant="outline" size="sm" class="me-2"
|
|
(click)="activePeerConfig && downloadPeerConfigDirect(activePeerConfig)">
|
|
<i class="fas fa-download"></i> Download Config
|
|
</button>
|
|
<button cButton color="info" variant="outline" size="sm"
|
|
(click)="activePeerConfig && openScriptModal(activePeerConfig)">
|
|
<i class="fas fa-terminal"></i> Show Script
|
|
</button>
|
|
</div>
|
|
<button cButton color="secondary" (click)="qrModalVisible = false">Close</button>
|
|
</c-modal-footer>
|
|
</c-modal>
|
|
|
|
<!-- Add / Edit Peer Wizard Modal -->
|
|
<c-modal #AddPeerModal backdrop="static" size="lg" [(visible)]="addPeerModalVisible" id="AddPeerModal">
|
|
<c-modal-header>
|
|
<h5 cModalTitle>{{ editingPeer ? 'Edit VPN Peer' : 'Create New VPN Peer' }}</h5>
|
|
<button [cModalToggle]="AddPeerModal.id" cButtonClose></button>
|
|
</c-modal-header>
|
|
<c-modal-body>
|
|
<!-- Step 1: General Info -->
|
|
<div *ngIf="addPeerStep === 1" style="animation: fadeIn 0.3s ease-in-out;">
|
|
<h6 class="mb-4 text-primary">Step 1: General Info</h6>
|
|
<c-input-group class="mb-3">
|
|
<span cInputGroupText><i class="fas fa-tag"></i></span>
|
|
<input cFormControl [(ngModel)]="peerForm.name" placeholder="Peer Name (e.g. CEO Laptop)" />
|
|
</c-input-group>
|
|
<c-input-group class="mb-3">
|
|
<span cInputGroupText><i class="fas fa-align-left"></i></span>
|
|
<textarea cFormControl [(ngModel)]="peerForm.description" placeholder="Optional description..."
|
|
rows="2"></textarea>
|
|
</c-input-group>
|
|
</div>
|
|
|
|
<!-- Step 2: Addressing -->
|
|
<div *ngIf="addPeerStep === 2" style="animation: fadeIn 0.3s ease-in-out;">
|
|
<h6 class="mb-4 text-primary">Step 2: Addressing</h6>
|
|
<c-input-group class="mb-3">
|
|
<span cInputGroupText>Assigned IP</span>
|
|
<input cFormControl [(ngModel)]="peerForm.custom_ip" placeholder="Leave empty for Auto-assign" />
|
|
</c-input-group>
|
|
<c-input-group class="mb-3" *ngIf="editingPeer">
|
|
<span cInputGroupText>Public Key</span>
|
|
<input cFormControl [(ngModel)]="peerForm.pubkey" readonly />
|
|
</c-input-group>
|
|
<c-input-group class="mb-3">
|
|
<span cInputGroupText>Keepalive</span>
|
|
<input cFormControl type="number" [(ngModel)]="peerForm.persistent_keepalive" placeholder="25" />
|
|
<span cInputGroupText>seconds</span>
|
|
</c-input-group>
|
|
</div>
|
|
|
|
<!-- Step 3: Routing -->
|
|
<div *ngIf="addPeerStep === 3" style="animation: fadeIn 0.3s ease-in-out;">
|
|
<h6 class="mb-4 text-primary">Step 3: Routing</h6>
|
|
<c-input-group class="mb-3">
|
|
<span cInputGroupText>NAT Mode</span>
|
|
<select cSelect [(ngModel)]="peerForm.nat_mode">
|
|
<option value="full">Full Tunnel (Route all traffic)</option>
|
|
<option value="split">Split Tunnel (Route specific subnets)</option>
|
|
<option value="off">Off (Direct Routing)</option>
|
|
</select>
|
|
</c-input-group>
|
|
|
|
<div *ngIf="peerForm.nat_mode === 'split'" class="mt-3">
|
|
<h6>Split Tunnel Targets</h6>
|
|
<div *ngFor="let target of peerForm.split_targets; let i = index; trackBy: trackByIndex" class="d-flex mb-2">
|
|
<input cFormControl [(ngModel)]="peerForm.split_targets[i]" placeholder="e.g. 192.168.1.0/24"
|
|
class="me-2" />
|
|
<button cButton color="danger" variant="outline" (click)="removeSplitTarget(i)"><i
|
|
class="fas fa-trash"></i></button>
|
|
</div>
|
|
<button cButton color="info" size="sm" variant="outline" (click)="addSplitTarget()">
|
|
<i class="fas fa-plus"></i> Add Subnet
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 4: Identity -->
|
|
<div *ngIf="addPeerStep === 4" style="animation: fadeIn 0.3s ease-in-out;">
|
|
<h6 class="mb-4 text-primary">Step 4: Identity & Integrations</h6>
|
|
<div class="form-check mb-3">
|
|
<input class="form-check-input" type="checkbox" id="linkDeviceCheck" [(ngModel)]="peerForm.link_device">
|
|
<label class="form-check-label" for="linkDeviceCheck">
|
|
Link & Manage as MikroTik Device
|
|
</label>
|
|
</div>
|
|
|
|
<div *ngIf="peerForm.link_device" class="mt-3" style="animation: fadeIn 0.3s ease-in-out;">
|
|
<c-input-group class="mb-3">
|
|
<span cInputGroupText><i class="fas fa-user"></i></span>
|
|
<input cFormControl [(ngModel)]="peerForm.mt_user" placeholder="MikroTik Username" />
|
|
</c-input-group>
|
|
<c-input-group class="mb-3">
|
|
<span cInputGroupText><i class="fas fa-lock"></i></span>
|
|
<input cFormControl type="password" [(ngModel)]="peerForm.mt_pass" placeholder="MikroTik Password" />
|
|
</c-input-group>
|
|
<c-input-group class="mb-3">
|
|
<span cInputGroupText><i class="fas fa-network-wired"></i></span>
|
|
<input cFormControl type="number" [(ngModel)]="peerForm.mt_port" placeholder="8728" />
|
|
</c-input-group>
|
|
|
|
<div class="alert alert-info mt-3" style="font-size: 0.85rem;">
|
|
<i class="fas fa-info-circle"></i> Once the peer connects and routes traffic (Online), the MikroWizard
|
|
native scanner will automatically use these credentials in the background to discover and safely add the
|
|
router into your Devices panel!
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 5: Summary -->
|
|
<div *ngIf="addPeerStep === 5" style="animation: fadeIn 0.3s ease-in-out;">
|
|
<h6 class="mb-4 text-primary">Step 5: Summary</h6>
|
|
<ul class="list-group">
|
|
<li class="list-group-item"><strong>IP Assignment Preview:</strong> {{ peerForm.custom_ip || 'Auto-assigned'
|
|
}}</li>
|
|
<li class="list-group-item"><strong>Routing NAT Mode:</strong> <c-badge color="info">{{ peerForm.nat_mode |
|
|
uppercase }}</c-badge></li>
|
|
<li class="list-group-item" *ngIf="peerForm.nat_mode === 'split'"><strong>Split Targets:</strong> {{
|
|
peerForm.split_targets.join(', ') || 'None' }}</li>
|
|
<li class="list-group-item"><strong>MikroTik Integration:</strong> {{ peerForm.link_device ? 'Enabled' :
|
|
'Disabled' }}</li>
|
|
</ul>
|
|
</div>
|
|
</c-modal-body>
|
|
<c-modal-footer>
|
|
<button *ngIf="addPeerStep > 1" cButton color="secondary" (click)="addPeerStep = addPeerStep - 1">Back</button>
|
|
<button *ngIf="addPeerStep < 5" cButton color="primary" (click)="addPeerStep = addPeerStep + 1">Next</button>
|
|
<button *ngIf="addPeerStep === 5" cButton color="success" (click)="submitPeer()">
|
|
<i class="fas fa-save"></i> {{ editingPeer ? 'Save Changes' : 'Create Peer' }}
|
|
</button>
|
|
</c-modal-footer>
|
|
</c-modal>
|
|
|
|
|
|
|
|
<!-- Server Reset Confirm Modal -->
|
|
<c-modal id="resetServerModal" [visible]="resetServerModalVisible" (visibleChange)="resetServerModalVisible = $event">
|
|
<c-modal-header class="bg-warning text-dark">
|
|
<h5 cModalTitle><i class="fas fa-exclamation-triangle"></i> Reset Server Counters</h5>
|
|
<button cButtonClose (click)="resetServerModalVisible = false"></button>
|
|
</c-modal-header>
|
|
<c-modal-body>
|
|
Are you sure you want to reset all data transfer counters for the VPN Server?
|
|
<br><br>
|
|
<small class="text-muted"><i class="fas fa-info-circle"></i> This will immediately set all Total Rx/Tx metrics to
|
|
zero. This does not disconnect any peers.</small>
|
|
</c-modal-body>
|
|
<c-modal-footer>
|
|
<button cButton color="secondary" (click)="resetServerModalVisible = false">Cancel</button>
|
|
<button cButton color="warning" (click)="confirmResetServer()">Reset Counters</button>
|
|
</c-modal-footer>
|
|
</c-modal>
|
|
|
|
<!-- Peer Reset Confirm Modal -->
|
|
<c-modal id="resetPeerModal" [visible]="resetPeerModalVisible" (visibleChange)="resetPeerModalVisible = $event">
|
|
<c-modal-header class="bg-warning text-dark">
|
|
<h5 cModalTitle><i class="fas fa-exclamation-triangle"></i> Reset Peer Counters</h5>
|
|
<button cButtonClose (click)="resetPeerModalVisible = false"></button>
|
|
</c-modal-header>
|
|
<c-modal-body *ngIf="peerToReset">
|
|
Are you sure you want to reset the traffic counters for peer: <strong>{{ peerToReset.name ||
|
|
peerToReset.assigned_ip }}</strong>?
|
|
<br><br>
|
|
<small class="text-muted"><i class="fas fa-info-circle"></i> This clears their individual Rx/Tx metrics to zero
|
|
and does not disconnect them.</small>
|
|
</c-modal-body>
|
|
<c-modal-footer>
|
|
<button cButton color="secondary" (click)="resetPeerModalVisible = false">Cancel</button>
|
|
<button cButton color="warning" (click)="confirmResetPeer()">Reset Counters</button>
|
|
</c-modal-footer>
|
|
</c-modal>
|
|
|
|
<!-- End of File -->
|
|
</div>
|
|
<c-toaster position="fixed" placement="top-end"></c-toaster> |