mirror of
https://github.com/MikroWizard/mikrofront.git
synced 2025-07-21 11:24:27 +02:00
improved Backup viewer and highlight fix with huge files(replace with highlightjs)
login page Errors improved, Added User disable option, Some Pro features and updates
This commit is contained in:
parent
0d6bdabcbc
commit
d6276f7246
26 changed files with 1761 additions and 98 deletions
|
@ -29,6 +29,11 @@ const routes: Routes = [
|
|||
loadChildren: () =>
|
||||
import('./views/monitoring/monitoring.module').then((m) => m.MonitoringModule)
|
||||
},
|
||||
{
|
||||
path: 'vault',
|
||||
loadChildren: () =>
|
||||
import('./views/vault/vault.module').then((m) => m.VaultModule)
|
||||
},
|
||||
{
|
||||
path: 'devices',
|
||||
loadChildren: () =>
|
||||
|
|
|
@ -64,6 +64,12 @@ export const navItems: INavData[] = [
|
|||
url: '/snippets',
|
||||
icon: 'fa-solid fa-code'
|
||||
},
|
||||
{
|
||||
name: 'Password Vault',
|
||||
url: '/vault',
|
||||
icon:'fa-solid fa-vault',
|
||||
attributes: { 'pro':true }
|
||||
},
|
||||
// {
|
||||
// name: 'Tools',
|
||||
// url: '/login',
|
||||
|
|
|
@ -77,11 +77,17 @@
|
|||
<h6 cDropdownHeader class="bg-light fw-semibold py-2">User Menu</h6>
|
||||
</li>
|
||||
<li>
|
||||
<button (click)="callParent()" cDropdownItem>
|
||||
<button (click)="callParent('password')" cDropdownItem>
|
||||
<svg cIcon class="me-2" name="cilUser"></svg>
|
||||
Change password
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button (click)="callParent('otp')" cDropdownItem>
|
||||
<svg cIcon class="me-2" name="cilUser"></svg>
|
||||
setup otp
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a routerLink="./" (click)="logout()" cDropdownItem>
|
||||
<svg cIcon class="me-2" name="cilExitToApp"></svg>
|
||||
|
|
|
@ -50,8 +50,8 @@ export class DefaultHeaderComponent extends HeaderComponent {
|
|||
this.lname = this.current_user.lastname;
|
||||
}
|
||||
|
||||
callParent(): void {
|
||||
this.UserModalEvent.next('test');
|
||||
callParent(action:string): void {
|
||||
this.UserModalEvent.next(action);
|
||||
}
|
||||
|
||||
logout() {
|
||||
|
|
|
@ -1,43 +1,28 @@
|
|||
<!--sidebar-->
|
||||
<c-sidebar
|
||||
#sidebar="cSidebar"
|
||||
class="d-print-none sidebar sidebar-fixed"
|
||||
id="sidebar"
|
||||
visible
|
||||
>
|
||||
<c-sidebar-brand
|
||||
[brandFull]="{
|
||||
<c-sidebar #sidebar="cSidebar" class="d-print-none sidebar sidebar-fixed" id="sidebar" visible>
|
||||
<c-sidebar-brand [brandFull]="{
|
||||
src: 'assets/img/brand/logo-MIkroWizard-big-white.svg',
|
||||
width: 200,
|
||||
height: 46,
|
||||
alt: 'MikroWizard Logo'
|
||||
}"
|
||||
[brandNarrow]="{
|
||||
}" [brandNarrow]="{
|
||||
src: 'assets/img/brand/logo-MIkroWizard-small-color.svg',
|
||||
width: 46,
|
||||
height: 46,
|
||||
alt: 'MikroWizard Logo'
|
||||
}"
|
||||
routerLink="./"
|
||||
/>
|
||||
}" routerLink="./" />
|
||||
|
||||
<ng-scrollbar pointerEventsMethod="scrollbar">
|
||||
<c-sidebar-nav
|
||||
[navItems]="navItems"
|
||||
dropdownMode="close"
|
||||
/>
|
||||
<c-sidebar-nav [navItems]="navItems" dropdownMode="close" />
|
||||
</ng-scrollbar>
|
||||
<c-sidebar-toggler
|
||||
*ngIf="!sidebar.narrow"
|
||||
toggle="unfoldable"
|
||||
cSidebarToggle="sidebar"
|
||||
/>
|
||||
<c-sidebar-toggler *ngIf="!sidebar.narrow" toggle="unfoldable" cSidebarToggle="sidebar" />
|
||||
</c-sidebar>
|
||||
|
||||
<!--main-->
|
||||
<div class="wrapper d-flex flex-column min-vh-100 bg-light dark:bg-transparent">
|
||||
<!--app-header-->
|
||||
<app-default-header (UserModalEvent)="show_user_modal()" class="mb-2 d-print-none header header-sticky" position="sticky" sidebarId="sidebar" />
|
||||
<app-default-header (UserModalEvent)="show_user_modal($event)" class="mb-2 d-print-none header header-sticky"
|
||||
position="sticky" sidebarId="sidebar" />
|
||||
<!--app-body-->
|
||||
<div class="main-container body flex-grow-1 px-3" style="display: flex;">
|
||||
<c-container breakpoint="fluid" class="h-auto">
|
||||
|
@ -50,30 +35,73 @@
|
|||
|
||||
<c-modal #UserProfileModal backdrop="static" size="lg" [(visible)]="UserProfileModalVisible" id="UserProfileModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Change Password Form of<code><b>{{ uname }}({{ fname }} {{lname}})</b></code></h5>
|
||||
<h5 *ngIf="action=='password'" cModalTitle>Change Password Form
|
||||
of<code><b>{{ uname }}({{ fname }} {{lname}})</b></code></h5>
|
||||
<h5 *ngIf="action=='otp'" cModalTitle>totp setup<code><b>{{ uname }}({{ fname }} {{lname}})</b></code></h5>
|
||||
|
||||
<button [cModalToggle]="UserProfileModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<c-modal-body *ngIf="action=='password'">
|
||||
|
||||
<div [cFormFloating]="true" class="mb-3">
|
||||
<input type="password" cFormControl id="floatingInput" [(ngModel)]="password['cupass']" placeholder="Current Password" />
|
||||
<input type="password" cFormControl id="floatingInput" [(ngModel)]="password['cupass']"
|
||||
placeholder="Current Password" />
|
||||
<label cLabel for="floatingInput">Current Password</label>
|
||||
</div>
|
||||
|
||||
|
||||
<div [cFormFloating]="true" class="mb-3">
|
||||
<input type="password" cFormControl (ngModelChange)="password_changed('pass1',$event)" [(ngModel)]="password['pass1']" id="floatingInput" placeholder="New Password" />
|
||||
<input type="password" cFormControl (ngModelChange)="password_changed('pass1',$event)"
|
||||
[(ngModel)]="password['pass1']" id="floatingInput" placeholder="New Password" />
|
||||
<label cLabel for="floatingInput">New Password</label>
|
||||
</div>
|
||||
|
||||
<div [cFormFloating]="true" class="mb-3">
|
||||
<input type="password" cFormControl (ngModelChange)="password_changed('pass2',$event)" [(ngModel)]="password['pass2']" [valid]="passvalid['pass2']" id="floatingInput" placeholder="New Password confirm" />
|
||||
<input type="password" cFormControl (ngModelChange)="password_changed('pass2',$event)"
|
||||
[(ngModel)]="password['pass2']" [valid]="passvalid['pass2']" id="floatingInput"
|
||||
placeholder="New Password confirm" />
|
||||
<label cLabel for="floatingInput">New Password confirm</label>
|
||||
</div>
|
||||
<code *ngIf="error"><i class="fa-solid fa-triangle-exclamation"></i><small> {{error}}</small></code>
|
||||
</c-modal-body>
|
||||
<c-modal-body *ngIf="action=='otp'">
|
||||
<div class="step-container" >
|
||||
|
||||
<div *ngIf="currentStep === 1" class="step" style="display: flex;flex-direction: column;flex-wrap: nowrap;justify-content: center;align-items: center;">
|
||||
<h3 class="text-center">Step 1: Enable TOTP</h3>
|
||||
<p>Please click the button below to enable Two-Factor Authentication.</p>
|
||||
<button *ngIf="qrCode!=false" (click)="otpwizard(1)" class="btn btn-primary">Enable TOTP</button>
|
||||
<button *ngIf="qrCode==false" (click)="otpwizard(1)" class="btn btn-primary">Disable TOTP</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Scan QR Code -->
|
||||
<div *ngIf="currentStep === 2" class="step text-center">
|
||||
<h3>Step 2: Scan QR Code</h3>
|
||||
<p>Open your Google Authenticator app and scan the QR code below:</p>
|
||||
<div>
|
||||
<img *ngIf="qrCode" [src]="qrCode" alt="QR Code" style="max-width: 100%; height: auto;">
|
||||
</div>
|
||||
<button (click)="otpwizard(2)" class="btn btn-primary mt-3">Next</button>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: Verify TOTP -->
|
||||
<div *ngIf="currentStep === 3" class="step">
|
||||
<h3 *ngIf="qrCode!=false" class="text-center">Step 3: Verify TOTP</h3>
|
||||
<h3 *ngIf="qrCode==false" class="text-center">Step 3: Verify TOTP To Disable TOTP</h3>
|
||||
|
||||
<p>Please enter the code generated by your authenticator app:</p>
|
||||
<input type="text" [(ngModel)]="totpCode" placeholder="Enter TOTP Code" class="form-control" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="errorMessage!=false" class="alert alert-danger mt-3" role="alert">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button (click)="submit()" cButton color="primary">submit</button>
|
||||
<button [cModalToggle]="UserProfileModal.id" cButton color="secondary">
|
||||
<button *ngIf="action=='password'" (click)="submit()" cButton color="primary">submit</button>
|
||||
<button *ngIf="currentStep==3" (click)="otpwizard(3)" cButton color="success" >Submit</button>
|
||||
<button [cModalToggle]="UserProfileModal.id" cButton color="secondary">
|
||||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
|
|
|
@ -4,6 +4,8 @@ import { loginChecker } from '../../providers/login_checker';
|
|||
import { User } from '../../providers/mikrowizard/user';
|
||||
import { navItems } from './_nav';
|
||||
import { dataProvider } from '../../providers/mikrowizard/data';
|
||||
import { arch } from 'os';
|
||||
import { DomSanitizer } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
|
@ -19,8 +21,13 @@ export class DefaultLayoutComponent implements OnInit {
|
|||
public fname: string;
|
||||
public lname: string;
|
||||
public ispro: boolean=false;
|
||||
public action: string="password";
|
||||
public UserProfileModalVisible:boolean;
|
||||
public error:any=false;
|
||||
public currentStep:number=1;
|
||||
public qrCode:any=false;
|
||||
public totpCode:string='';
|
||||
public errorMessage:any=false;
|
||||
public password:any={
|
||||
'cupass':'',
|
||||
'pass1':'',
|
||||
|
@ -38,6 +45,7 @@ export class DefaultLayoutComponent implements OnInit {
|
|||
private router: Router,
|
||||
private login_checker: loginChecker,
|
||||
private data_provider: dataProvider,
|
||||
private _sanitizer: DomSanitizer
|
||||
|
||||
) {
|
||||
var _self = this;
|
||||
|
@ -52,7 +60,39 @@ export class DefaultLayoutComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
otpwizard(step:number){
|
||||
var _self=this;
|
||||
if(step==1){
|
||||
if(this.qrCode)
|
||||
this.currentStep=2;
|
||||
else
|
||||
this.currentStep=3;
|
||||
}
|
||||
if(step==2){
|
||||
this.currentStep=3;
|
||||
}
|
||||
if(step==3){
|
||||
if(this.qrCode!=false)
|
||||
this.data_provider.mytotp('enable',this.totpCode).then(res => {
|
||||
if(res['status']=='success'){
|
||||
_self.UserProfileModalVisible = false;
|
||||
}
|
||||
else{
|
||||
this.errorMessage=res['err'];
|
||||
}
|
||||
});
|
||||
else
|
||||
this.data_provider.mytotp('disable',this.totpCode).then(res => {
|
||||
if(res['status']=='success'){
|
||||
_self.UserProfileModalVisible = false;
|
||||
}
|
||||
else{
|
||||
this.errorMessage=res['err'];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
password_changed(variable:string,value:any){
|
||||
|
@ -66,8 +106,29 @@ export class DefaultLayoutComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
show_user_modal(){
|
||||
this.UserProfileModalVisible = true;
|
||||
show_user_modal(action:string){
|
||||
this.currentStep=1;
|
||||
this.errorMessage=false;
|
||||
this.totpCode='';
|
||||
this.qrCode=false;
|
||||
this.action=action;
|
||||
if(action=='otp')
|
||||
this.data_provider.mytotp('enable').then(res => {
|
||||
if(res['status']=='success'){
|
||||
this.currentStep=1;
|
||||
this.qrCode=this._sanitizer.bypassSecurityTrustResourceUrl('data:image/jpg;base64,'+ res.otp);
|
||||
this.UserProfileModalVisible = true;
|
||||
}
|
||||
else{
|
||||
this.qrCode=false;
|
||||
this.currentStep=1;
|
||||
this.UserProfileModalVisible = true;
|
||||
|
||||
this.errorMessage=res['err'];
|
||||
}
|
||||
});
|
||||
else
|
||||
this.UserProfileModalVisible = true;
|
||||
}
|
||||
|
||||
submit(){
|
||||
|
|
|
@ -43,7 +43,6 @@ export class dataProvider {
|
|||
res.perms,
|
||||
res.tz,
|
||||
);
|
||||
// console.dir(JSON.stringify(usr))
|
||||
localStorage.setItem('current_user', JSON.stringify(usr));
|
||||
}
|
||||
return res;
|
||||
|
@ -181,6 +180,35 @@ export class dataProvider {
|
|||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/dev/ifstat", data);
|
||||
}
|
||||
totp(action:string,userid:string){
|
||||
var data={
|
||||
'userid':userid,
|
||||
'action':action
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user/totp", data);
|
||||
}
|
||||
|
||||
get_user_restrictions(uid:string){
|
||||
var data={
|
||||
'uid':uid
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user/restrictions", data);
|
||||
}
|
||||
save_user_restrictions(uid:string,restrictions:any){
|
||||
var data={
|
||||
'uid':uid,
|
||||
'restrictions':restrictions
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user/save_restrictions", data);
|
||||
}
|
||||
|
||||
mytotp(action:string,otp:any=false){
|
||||
var data={
|
||||
'action':action,
|
||||
'otp':otp
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user/mytotp", data);
|
||||
}
|
||||
|
||||
get_auth_logs(filters:any) {
|
||||
var data=filters;
|
||||
|
@ -269,22 +297,22 @@ export class dataProvider {
|
|||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/delete", data);
|
||||
}
|
||||
|
||||
get_executed_snipet(id:number){
|
||||
var data={
|
||||
'id':id
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/snippet/executed", data);
|
||||
}
|
||||
|
||||
get_user_task_list() {
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/list", {});
|
||||
}
|
||||
|
||||
|
||||
Add_task(data:any,members:any) {
|
||||
data['members']=members;
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/create", data);
|
||||
}
|
||||
|
||||
|
||||
Delete_task(taskid:Number) {
|
||||
var data={
|
||||
|
@ -297,7 +325,7 @@ export class dataProvider {
|
|||
data['members']=members;
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/user_tasks/edit", data);
|
||||
}
|
||||
|
||||
|
||||
get_task_members(taskid:Number) {
|
||||
var data={
|
||||
'taskid':taskid,
|
||||
|
@ -305,7 +333,6 @@ export class dataProvider {
|
|||
return this.MikroWizardRPC.sendJsonRequest("/api/taskmember/details", data);
|
||||
}
|
||||
|
||||
|
||||
get_users(page:Number,size:Number,search:string) {
|
||||
var data={
|
||||
'page':page,
|
||||
|
@ -350,6 +377,37 @@ export class dataProvider {
|
|||
return this.MikroWizardRPC.sendJsonRequest("/api/perms/delete", data);
|
||||
}
|
||||
|
||||
get_vault_setting(){
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get", {});
|
||||
}
|
||||
|
||||
vault_task(data:any){
|
||||
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/task", data);
|
||||
}
|
||||
vault_history(){
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/history", {});
|
||||
}
|
||||
exec_vault(){
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/execute", {});
|
||||
}
|
||||
reveal_password(devid:number,username:string){
|
||||
var data={
|
||||
'devid':devid,
|
||||
'username':username
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/reveal", data);
|
||||
}
|
||||
|
||||
get_passwords(data:any){
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get_passwords", data);
|
||||
}
|
||||
get_device_pass(devid:number){
|
||||
var data={
|
||||
'devid':devid
|
||||
}
|
||||
return this.MikroWizardRPC.sendJsonRequest("/api/pssvault/get_device_pass", data);
|
||||
}
|
||||
user_perms(uid:string) {
|
||||
|
||||
var data = {
|
||||
|
|
|
@ -110,7 +110,6 @@ export class MikroWizardProvider {
|
|||
}
|
||||
public sendRequestauth(url: string, params: Object){
|
||||
let body = this.buildRequest(url, params);
|
||||
console.dir(body);
|
||||
return this.http.post(this.MikroWizard_server + url, body, {observe: "response",headers: this.headers,withCredentials:true});
|
||||
}
|
||||
public sendRequest(url: string, params: Object): Promise<any> {
|
||||
|
@ -164,7 +163,7 @@ export class MikroWizardProvider {
|
|||
username : login,
|
||||
password : password,
|
||||
// token: token,
|
||||
ga: ga
|
||||
otp: ga
|
||||
};
|
||||
let $this = this;
|
||||
return this.sendRequest("/api/login", params);
|
||||
|
@ -172,9 +171,6 @@ export class MikroWizardProvider {
|
|||
|
||||
public isLoggedIn() {
|
||||
return this.getSessionInfo().then(function(result: any) {
|
||||
// console.dir("result");
|
||||
console.dir(result);
|
||||
// return true;
|
||||
if ( "uid" in result === false ) return false;
|
||||
else return true;
|
||||
});
|
||||
|
|
|
@ -362,7 +362,11 @@
|
|||
<span cInputGroupText>Password</span>
|
||||
<input aria-label="start" [type]="show_pass ? 'text' : 'password'"
|
||||
[(ngModel)]="selected_device['editform']['password']" cFormControl placeholder=" username" />
|
||||
<button cButton (click)="show_pass=!show_pass" color="secondary" variant="outline">
|
||||
<button cButton *ngIf="!ispro" (click)="show_pass=!show_pass" color="secondary" variant="outline">
|
||||
<i *ngIf="show_pass" class="fa-solid fa-eye"></i>
|
||||
<i *ngIf="!show_pass" class="fa-solid fa-eye-slash"></i>
|
||||
</button>
|
||||
<button cButton *ngIf="ispro" (click)="get_device_pass()" color="secondary" variant="outline">
|
||||
<i *ngIf="show_pass" class="fa-solid fa-eye"></i>
|
||||
<i *ngIf="!show_pass" class="fa-solid fa-eye-slash"></i>
|
||||
</button>
|
||||
|
|
|
@ -37,6 +37,8 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
public uid: number;
|
||||
public uname: string;
|
||||
public tz: string;
|
||||
public ispro:boolean=false;
|
||||
|
||||
|
||||
constructor(
|
||||
private data_provider: dataProvider,
|
||||
|
@ -54,6 +56,7 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
_self.uid = res.uid;
|
||||
_self.uname = res.name;
|
||||
_self.tz = res.tz;
|
||||
_self.ispro = res.ISPRO;
|
||||
const userId = _self.uid;
|
||||
|
||||
if (res.role != "admin") {
|
||||
|
@ -495,7 +498,7 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
|
||||
downloadFile(data: string, filename: string, type: string) {
|
||||
const blob = new Blob([data], { type: type });
|
||||
const nav = (window.navigator as any);
|
||||
const nav = (window.navigator as any);
|
||||
|
||||
if (nav.msSaveOrOpenBlob) {
|
||||
nav.msSaveBlob(blob, filename);
|
||||
|
@ -509,9 +512,19 @@ export class DevicesComponent implements OnInit, OnDestroy {
|
|||
document.body.removeChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
get_device_pass(){
|
||||
var _self=this;
|
||||
_self.selected_device['editform']['password']="Loading ...";
|
||||
if (_self.ispro && !this.show_pass){
|
||||
_self.data_provider.get_device_pass(this.selected_device['id']).then((res) => {
|
||||
_self.selected_device['editform']['password']=res['password'];
|
||||
this.show_pass=!this.show_pass;
|
||||
});
|
||||
}
|
||||
else{
|
||||
this.show_pass=!this.show_pass;
|
||||
}
|
||||
}
|
||||
show_exec(){
|
||||
var _self=this;
|
||||
this.ExecutedDataModalVisible = true;
|
||||
|
|
|
@ -32,6 +32,17 @@
|
|||
required #password
|
||||
/>
|
||||
</c-input-group>
|
||||
<c-input-group class="mb-1" *ngIf="show_otp">
|
||||
<span cInputGroupText>
|
||||
<i class="fa-regular fa-clock"></i>
|
||||
</span>
|
||||
<input
|
||||
cFormControl
|
||||
placeholder="2FA TOTP key"
|
||||
formControlName="ga_code"
|
||||
required #ga_code
|
||||
/>
|
||||
</c-input-group>
|
||||
<code *ngIf="error_msg"><i class="fa-solid fa-triangle-exclamation"></i><small> {{error_msg}}</small></code>
|
||||
<c-row>
|
||||
<c-col mb-3 xs="6">
|
||||
|
|
|
@ -18,7 +18,7 @@ export class LoginComponent {
|
|||
public submitted = false;
|
||||
public forgot_page: boolean = false;
|
||||
public forgot_btn_disable: boolean = false;
|
||||
|
||||
public show_otp: boolean = false;
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
|
@ -43,24 +43,25 @@ export class LoginComponent {
|
|||
var _self = this;
|
||||
let uname = _self.loginForm.get('username')!.value;
|
||||
let passwd = _self.loginForm.get('password')!.value;
|
||||
let ga_code = '';
|
||||
console.dir(uname);
|
||||
_self.data_provider.login(uname, passwd, '').then(res => {
|
||||
if ('uid' in res && res['uid']){
|
||||
let ga_code = _self.loginForm.get('ga_code')!.value;
|
||||
_self.data_provider.login(uname, passwd, ga_code).then(res => {
|
||||
if('uid' in res && res['uid']){
|
||||
_self.error_msg = "";
|
||||
_self.login_checker.setStatus(true);
|
||||
_self.router.navigate(['/'], {replaceUrl: true});
|
||||
}
|
||||
else if('status' in res) {
|
||||
_self.error_msg = res['err'];
|
||||
}
|
||||
else if('otp' in res && res['otp']){
|
||||
this.show_otp=true;
|
||||
}
|
||||
else {
|
||||
if ('reason' in res) {
|
||||
}
|
||||
else
|
||||
_self.error_msg = res.error;
|
||||
_self.error_msg = 'Error: Problem in backend';
|
||||
}
|
||||
}).catch(err => {
|
||||
_self.error_msg = "Wrong username or password!";
|
||||
_self.error_msg = "Connection with backend broken!";
|
||||
});
|
||||
// });
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -192,6 +192,13 @@
|
|||
* Download and install reqired firmware before installing the target firmware . for example it will install
|
||||
latest 7.12 then upgrade to newer version >7.13 or install Required packages before update</c-form-feedback>
|
||||
</c-form-check>
|
||||
|
||||
<c-form-check *ngIf="ispro" [switch]="true" sizing="xl">
|
||||
<input cFormCheckInput [(ngModel)]="sysconfigs['otp_force']['value']" type="checkbox" />
|
||||
<label cFormCheckLabel>Force device otp</label>
|
||||
<c-form-feedback style="display: block;color: #979797;margin-top: 0;" [valid]="true"><code style="padding: 0!important;">PRO</code>
|
||||
* Force login to devices using otp for all users.(you can make exceptions for each user)</c-form-feedback>
|
||||
</c-form-check>
|
||||
<button cButton color="primary" (click)="saveSysSetting()">Save</button>
|
||||
|
||||
</c-card-body>
|
||||
|
|
|
@ -237,6 +237,9 @@ export class SettingsComponent implements OnInit {
|
|||
_self.sysconfigs["safe_install"]["value"] = /true/i.test(
|
||||
_self.sysconfigs["safe_install"]["value"]
|
||||
);
|
||||
_self.sysconfigs["otp_force"]["value"] = /true/i.test(
|
||||
_self.sysconfigs["otp_force"]["value"]
|
||||
);
|
||||
_self.SysConfigloading = false;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -36,10 +36,14 @@
|
|||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="120" field="action">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button cButton color="warning" size="sm" (click)="editAddUser(item,'edit');" class="mx-1"><i
|
||||
<button cButton color="warning" size="sm" (click)="editAddUser(item,'edit');" ><i
|
||||
class="fa-regular fa-pen-to-square"></i></button>
|
||||
<button cButton color="danger" size="sm" (click)="confirm_delete(item);"><i
|
||||
<button cButton color="danger" size="sm" class="mx-1" (click)="confirm_delete(item);"><i
|
||||
class="fa-regular fa-trash-can"></i></button>
|
||||
|
||||
<button *ngIf="ispro" cButton color="secondary" size="sm" (click)="showrest(item);">
|
||||
<i class="fa-solid fa-fingerprint"></i>
|
||||
</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
|
@ -79,17 +83,6 @@
|
|||
<input type="password" cFormControl id="floatingInput" placeholder="Password" [(ngModel)]="SelectedUser['password']" />
|
||||
<label cLabel for="floatingInput">Password</label>
|
||||
</div>
|
||||
|
||||
<!-- <c-input-group class="mb-3">
|
||||
<label cInputGroupText for="inputGroupSelect01">
|
||||
Options
|
||||
</label>
|
||||
<select cSelect id="inputGroupSelect01" [(ngModel)]="SelectedUser['role']" (change)="get_user_perms(SelectedUser['id'])">
|
||||
<option>Choose...</option>
|
||||
<option value="admin">admin</option>
|
||||
<option value="user">Customer</option>
|
||||
</select>
|
||||
</c-input-group> -->
|
||||
<c-input-group>
|
||||
<h5>MikroWizard permisssions :</h5>
|
||||
<c-container>
|
||||
|
@ -165,24 +158,108 @@
|
|||
</mat-form-field>
|
||||
</td>
|
||||
<td>
|
||||
<button *ngIf="SelectedUser['action']=='edit'" cButton color="primary" (click)="add_user_perm()">+</button>
|
||||
<button *ngIf="SelectedUser['action']=='add'" cButton color="primary" (click)="add_new_user_perm()">+</button>
|
||||
<button *ngIf="SelectedUser['action']=='edit'" cButton color="primary" (click)="add_user_perm()">Add+</button>
|
||||
<button *ngIf="SelectedUser['action']=='add'" cButton color="primary" (click)="add_new_user_perm()">Add+</button>
|
||||
<!-- <button *ngIf="SelectedUser['action']=='add'" cButton color="primary" (click)="loading=!loading">++</button> -->
|
||||
</td>
|
||||
</table>
|
||||
|
||||
</c-modal-body>
|
||||
<c-modal-footer style="justify-content: space-between;">
|
||||
<div>
|
||||
<button *ngIf="SelectedUser['role']!='disabled'" (click)="SelectedUser['role']='disabled'" cButton color="danger">Deactive</button>
|
||||
<button *ngIf="SelectedUser['role']=='disabled'" (click)="SelectedUser['role']='admin'" cButton color="success">Activate</button>
|
||||
</div>
|
||||
<div>
|
||||
<button *ngIf="SelectedUser['action']=='add'" (click)="submit('add')" cButton color="primary">Add</button>
|
||||
<button *ngIf="SelectedUser['action']=='edit'" (click)="submit('edit')" cButton color="primary">save</button>
|
||||
<button [cModalToggle]="EditTaskModal.id" cButton color="secondary">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<c-modal #RestrictionsTaskModal *ngIf="ispro && userresttrictions" backdrop="static" size="lg" [(visible)]="RestrictionsTaskModalVisible" id="RestrictionsTaskModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Security Restrictions of {{SelectedUser['username']}}</h5>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td><h6>TOTP status :</h6></td>
|
||||
<td>
|
||||
<c-form-check sizing="xl" switch>
|
||||
<input cFormCheckInput [(ngModel)]="userresttrictions['totp']" [checked]="userresttrictions['totp']" type="checkbox" />
|
||||
<label *ngIf="userresttrictions['totp']" cFormCheckLabel> TOTP is active</label>
|
||||
<label *ngIf="!userresttrictions['totp']" cFormCheckLabel> TOTP is deactive</label>
|
||||
</c-form-check>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h6>Use OTP for device login:</h6></td>
|
||||
<td>
|
||||
<c-button-group aria-label="Basic example" role="group">
|
||||
<button cButton color="info" variant="outline" size="sm" [active]="userresttrictions['device-totp']=='system'"
|
||||
(click)="userresttrictions['device-totp']='system'">System Defined</button>
|
||||
<button cButton color="danger" variant="outline" size="sm" [active]="userresttrictions['device-totp']=='yes'"
|
||||
(click)="userresttrictions['device-totp']='yes'">TOTP</button>
|
||||
<button cButton color="success" variant="outline" size="sm" [active]="userresttrictions['device-totp']=='no'"
|
||||
(click)="userresttrictions['device-totp']='no'">Password</button>
|
||||
</c-button-group>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><h6>Restrict IP access:</h6></td>
|
||||
<td>
|
||||
<c-form-check sizing="xl" switch>
|
||||
<input cFormCheckInput [(ngModel)]="userresttrictions['ip']" [checked]="userresttrictions['ip']" type="checkbox" />
|
||||
<label *ngIf="userresttrictions['ip']" cFormCheckLabel> Restricted</label>
|
||||
<label *ngIf="!userresttrictions['ip']" cFormCheckLabel> Not Restricted</label>
|
||||
</c-form-check>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<c-input-group *ngIf="userresttrictions['ip'] && userresttrictions['allowed_ips'].length>0" class="mb-3">
|
||||
<h5>Allowed ips :</h5>
|
||||
<gui-grid [autoResizeWidth]="true" [source]="userresttrictions['allowed_ips']" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[autoResizeWidth]=true [paging]="paging" >
|
||||
<gui-grid-column header="IP Address" >
|
||||
<ng-template let-value="item" let-item="item" let-index="index">
|
||||
{{item}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Action" width="80" align="center">
|
||||
<ng-template let-value="item" let-item="item" let-index="index">
|
||||
<button cButton color="danger" (click)="delete_ip(item)"><i class="fa-regular fa-trash-can"></i></button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</c-input-group>
|
||||
<hr />
|
||||
<table *ngIf="userresttrictions['ip']" class="mb-3">
|
||||
<td style="width: 30%;">
|
||||
<span>Add new IP</span>
|
||||
</td>
|
||||
<td>
|
||||
<div >
|
||||
<input cFormControl id="floatingInput" placeholder="IP address/cidr" [(ngModel)]="ipaddress" />
|
||||
</div>
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<button cButton color="primary" (click)="add_ip()">Add+</button>
|
||||
</td>
|
||||
</table>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button *ngIf="SelectedUser['action']=='add'" (click)="submit('add')" cButton color="primary">Add</button>
|
||||
<button *ngIf="SelectedUser['action']=='edit'" (click)="submit('edit')" cButton color="primary">save</button>
|
||||
<button [cModalToggle]="EditTaskModal.id" cButton color="secondary">
|
||||
<button (click)="save_sec()" cButton color="primary">Save</button>
|
||||
<button [cModalToggle]="RestrictionsTaskModal.id" cButton color="secondary">
|
||||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
|
||||
|
||||
<c-modal #DeleteConfirmModal backdrop="static" [(visible)]="DeleteConfirmModalVisible" id="DeleteConfirmModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Confirm delete {{ SelectedUser['name'] }}</h5>
|
||||
|
|
|
@ -16,26 +16,15 @@ import { NgxSuperSelectOptions } from "ngx-super-select";
|
|||
import { AppToastComponent } from "../toast-simple/toast.component";
|
||||
import { ToasterComponent } from "@coreui/angular";
|
||||
|
||||
interface IUser {
|
||||
name: string;
|
||||
state: string;
|
||||
registered: string;
|
||||
country: string;
|
||||
usage: number;
|
||||
period: string;
|
||||
payment: string;
|
||||
activity: string;
|
||||
avatar: string;
|
||||
status: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
templateUrl: "user_manager.component.html",
|
||||
styleUrls: ["user_manager.scss"],
|
||||
})
|
||||
export class UserManagerComponent implements OnInit {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public ispro:boolean=false;
|
||||
|
||||
gridComponent: GuiGridComponent;
|
||||
toasterForm = {
|
||||
autohide: true,
|
||||
|
@ -59,6 +48,7 @@ export class UserManagerComponent implements OnInit {
|
|||
this.data_provider.getSessionInfo().then((res) => {
|
||||
_self.uid = res.uid;
|
||||
_self.uname = res.name;
|
||||
_self.ispro = res.ISPRO;
|
||||
const userId = _self.uid;
|
||||
|
||||
if (res.role != "admin") {
|
||||
|
@ -81,6 +71,7 @@ export class UserManagerComponent implements OnInit {
|
|||
public SelectedUserItems: string = "";
|
||||
public EditTaskModalVisible: boolean = false;
|
||||
public DeleteConfirmModalVisible: boolean = false;
|
||||
public RestrictionsTaskModalVisible: boolean = false;
|
||||
public Members: any = "";
|
||||
|
||||
public devgroup: any = {};
|
||||
|
@ -89,6 +80,8 @@ export class UserManagerComponent implements OnInit {
|
|||
public allPerms: any = [];
|
||||
public DeletePermConfirmModalVisible: boolean = false;
|
||||
public userperms: any = {};
|
||||
public userresttrictions: any = false;
|
||||
public ipaddress:string="";
|
||||
public adminperms: { [index: string]: string };
|
||||
public defadminperms: { [index: string]: string } = {
|
||||
device: "none",
|
||||
|
@ -155,7 +148,16 @@ export class UserManagerComponent implements OnInit {
|
|||
);
|
||||
componentRef.instance["closeButton"] = props.closeButton;
|
||||
}
|
||||
|
||||
totp(item:any){
|
||||
this.SelectedUser = item;
|
||||
this.data_provider.totp('enable',this.SelectedUser.id).then((res) => {
|
||||
if(res.status == "success"){
|
||||
this.show_toast("Success", "Totp generated successfully", "success");
|
||||
}else{
|
||||
this.show_toast("Error", res.err, "danger");
|
||||
}
|
||||
});
|
||||
}
|
||||
submit(action: string) {
|
||||
var _self = this;
|
||||
if (action == "add") {
|
||||
|
@ -178,7 +180,7 @@ export class UserManagerComponent implements OnInit {
|
|||
}
|
||||
});
|
||||
} else {
|
||||
console.dir(_self.userperms); if (_self.userperms.length > 0) {
|
||||
if (_self.userperms.length > 0) {
|
||||
_self.SelectedUser["userperms"] = _self.userperms;
|
||||
} else {
|
||||
_self.SelectedUser["userperms"] = [];
|
||||
|
@ -228,6 +230,56 @@ export class UserManagerComponent implements OnInit {
|
|||
_self.EditTaskModalVisible = true;
|
||||
}
|
||||
|
||||
checkIpAddress(ip:string) {
|
||||
const ipv4Pattern = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.|\/|)){4}\b(0?[1-9]|1[0-9]|2[0-9]|3[0-2])\b$/;
|
||||
return ipv4Pattern.test(ip)
|
||||
}
|
||||
|
||||
showrest(item: any) {
|
||||
var _self=this;
|
||||
this.SelectedUser = { ...item };
|
||||
|
||||
this.data_provider.get_user_restrictions(this.SelectedUser["id"]).then((res) => {
|
||||
_self.userresttrictions = res;
|
||||
console.log(_self.userresttrictions);
|
||||
_self.RestrictionsTaskModalVisible = true;
|
||||
});
|
||||
}
|
||||
delete_ip(item:string){
|
||||
|
||||
this.userresttrictions['allowed_ips']=this.userresttrictions['allowed_ips'].filter((x:any)=>x!=item);
|
||||
}
|
||||
|
||||
add_ip(){
|
||||
//check if ip address is valid cidr and not added before
|
||||
let ip=this.ipaddress.trim();
|
||||
if(ip=="")return;
|
||||
if(this.userresttrictions['allowed_ips'].includes(ip)){
|
||||
this.show_toast("Error", "IP already added", "danger");
|
||||
return;
|
||||
}
|
||||
//check if ip is valid cidr ip
|
||||
if(this.checkIpAddress(ip)){
|
||||
this.userresttrictions['allowed_ips'].push(ip);
|
||||
this.userresttrictions['allowed_ips']=this.userresttrictions['allowed_ips'].filter((x:any)=>x!="");
|
||||
this.ipaddress="";
|
||||
}
|
||||
else{
|
||||
this.show_toast("Error", "Invalid IP address", "danger");
|
||||
}
|
||||
}
|
||||
|
||||
save_sec(){
|
||||
this.data_provider.save_user_restrictions(this.SelectedUser.id,this.userresttrictions).then((res) => {
|
||||
if('status' in res && res['status']=='success')
|
||||
this.RestrictionsTaskModalVisible = false;
|
||||
else if('status' in res && res['status']=='failed')
|
||||
this.show_toast("Error", res.err, "danger");
|
||||
else
|
||||
this.show_toast("Error", "Somthing went wrong", "danger");
|
||||
});
|
||||
}
|
||||
|
||||
add_user_perm() {
|
||||
var _self = this;
|
||||
this.data_provider
|
||||
|
@ -281,6 +333,7 @@ export class UserManagerComponent implements OnInit {
|
|||
this.get_user_perms(this.SelectedUser["id"]);
|
||||
});
|
||||
}
|
||||
|
||||
logger(item: any) {
|
||||
console.dir(item);
|
||||
}
|
||||
|
|
4
src/app/views/user_manager/user_manager.scss
Normal file
4
src/app/views/user_manager/user_manager.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
table tr td{
|
||||
padding-bottom:20px;
|
||||
vertical-align:top
|
||||
}
|
|
@ -55,7 +55,6 @@
|
|||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<c-modal-header>
|
||||
|
||||
<c-modal #EditTaskModal backdrop="static" size="xl" [(visible)]="EditTaskModalVisible" id="EditTaskModal">
|
||||
<c-modal-header>
|
||||
|
|
21
src/app/views/vault/vault-routing.module.ts
Normal file
21
src/app/views/vault/vault-routing.module.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule, Routes } from '@angular/router';
|
||||
|
||||
import { VaultComponent } from './vault.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: VaultComponent,
|
||||
data: {
|
||||
title: $localize`Password Vault`
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class VaultRoutingModule {
|
||||
}
|
360
src/app/views/vault/vault.component.html
Normal file
360
src/app/views/vault/vault.component.html
Normal file
|
@ -0,0 +1,360 @@
|
|||
<c-row>
|
||||
<c-col xs style="padding-right: 0;">
|
||||
<div class="nav nav-underline" style="background: #fff;">
|
||||
<div calss="nav-item">
|
||||
<a [active]="true" class="nav-link" [cTabContent]="tabContent" (click)="activetab=0" [tabPaneIdx]="0">Settings</a>
|
||||
</div>
|
||||
<div calss="nav-item">
|
||||
<a [cTabContent]="tabContent" (click)="get_passwords();activetab=1" class="nav-link" [routerLink] [tabPaneIdx]="1">Passwords</a>
|
||||
</div>
|
||||
</div>
|
||||
</c-col>
|
||||
<c-col style="padding-left: 0;">
|
||||
<div class="nav nav-underline" style="background: #fff;padding: 3px;flex-direction: row-reverse;">
|
||||
<button *ngIf="activetab==0" cButton size="sm" shape="rounded-0" class="mx-2" (click)="runConfirmModalVisible=!runConfirmModalVisible" color="danger">Execute Now</button>
|
||||
<button *ngIf="activetab==1" cButton size="sm" shape="rounded-0" class="mx-2" (click)="toggleCollapse()" color="info">filters</button>
|
||||
</div>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<c-tab-content style="padding: 0!important;" #tabContent="cTabContent">
|
||||
<c-tab-pane>
|
||||
<c-row>
|
||||
<c-col xs>
|
||||
<c-card class="mb-4" style="border-radius: 0;" *ngIf="settings">
|
||||
<c-card-body>
|
||||
<c-row>
|
||||
<c-col md="6">
|
||||
<c-input-group class="mb-3">
|
||||
<label cInputGroupText for="inputGroupSelect01">
|
||||
Status
|
||||
</label>
|
||||
<select cSelect id="inputGroupSelect01" [(ngModel)]="settings['enable']">
|
||||
<option>Choose...</option>
|
||||
<option value="enable">Enable</option>
|
||||
<option value="disable">Disable</option>
|
||||
</select>
|
||||
</c-input-group>
|
||||
</c-col>
|
||||
<c-col md="6">
|
||||
<c-input-group class="mb-3">
|
||||
<label cInputGroupText for="inputGroupSelect01">
|
||||
Strategy
|
||||
</label>
|
||||
<select cSelect id="inputGroupSelect01" [(ngModel)]="settings['strategy']">
|
||||
<option>Choose...</option>
|
||||
<option value="all">All local</option>
|
||||
<option value="mikrowizard">Defined in MikroWizard</option>
|
||||
</select>
|
||||
</c-input-group>
|
||||
</c-col>
|
||||
<c-col md="6">
|
||||
<c-input-group class="mb-3">
|
||||
<label cInputGroupText for="inputGroupSelect01">
|
||||
Interval
|
||||
</label>
|
||||
<select cSelect id="inputGroupSelect01" [(ngModel)]="settings['interval']">
|
||||
<option>Choose...</option>
|
||||
<option value="daily">Daily</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
<option value="yearly">Yearly</option>
|
||||
<option value="manual">Manual</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</c-input-group>
|
||||
<c-input-group *ngIf="settings['interval']=='custom'" class="mb-3">
|
||||
<label cInputGroupText for="inputGroupSelect01">
|
||||
Custom Cron
|
||||
</label>
|
||||
<input cFormControl id="floatingInput" style="border-radius: 0;" placeholder="Cron" [(ngModel)]="settings['cron']" />
|
||||
</c-input-group>
|
||||
</c-col>
|
||||
<c-col md="6">
|
||||
<c-input-group class="mb-3">
|
||||
<label cInputGroupText for="inputGroupSelect01">
|
||||
Password
|
||||
</label>
|
||||
<select cSelect id="inputGroupSelect01" [(ngModel)]="settings['password_type']">
|
||||
<option>Choose...</option>
|
||||
<option value="random">Random</option>
|
||||
<option value="defined">Pre-defined</option>
|
||||
</select>
|
||||
</c-input-group>
|
||||
</c-col>
|
||||
<c-col md="12" *ngIf="settings['strategy']=='all'">
|
||||
<hr width="70%" style="margin: 10px auto;border-color: #304193;border-width: 2px;" />
|
||||
<c-row class="gui-header" style="background: #f9fafb;padding: 10px 0px;margin: 0 auto;height: unset;border: 1px solid #e8e8e8;border-bottom: unset;">
|
||||
<c-col md="2" style="display: flex;align-items: center;">
|
||||
<h6>User Exceptions</h6>
|
||||
</c-col>
|
||||
<c-col style="display: flex;flex-direction: row-reverse;" md="10">
|
||||
<table>
|
||||
<td>
|
||||
<div>
|
||||
<input cFormControl style="border-radius: 0;" id="floatingInput" placeholder="Username Exception"
|
||||
[(ngModel)]="new_exception" />
|
||||
</div>
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<button cButton color="dark" shape="rounded-0" (click)="add_exception()">Add Username</button>
|
||||
</td>
|
||||
</table>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<c-input-group class="mb-3">
|
||||
<gui-grid [autoResizeWidth]="true" [source]="settings['exceptions']" [columnMenu]="columnMenu" [paging]="paging"
|
||||
[sorting]="sorting" [autoResizeWidth]=true>
|
||||
<gui-grid-column header="UserName" field="name">
|
||||
<ng-template let-value="item" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="70" field="action">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button (click)="remove_exception(item)" class=" mx-1" cButton color="danger" size="sm"><i
|
||||
class="fa-regular fa-trash-can"></i></button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</c-input-group>
|
||||
</c-col>
|
||||
<c-col md="12" *ngIf="settings['password_type']=='defined'">
|
||||
<hr width="70%" style="margin: 10px auto;border-color: #304193;border-width: 2px;"/>
|
||||
<c-row class="gui-header" style="background: #f9fafb;padding: 10px 0px;margin: 0 auto;height: unset;border: 1px solid #e8e8e8;border-bottom: unset;">
|
||||
<c-col md="2" style="display: flex;align-items: center;">
|
||||
<h6>Password list</h6>
|
||||
</c-col>
|
||||
<c-col style="display: flex;flex-direction: row-reverse;" md="10">
|
||||
<table>
|
||||
<td>
|
||||
<div>
|
||||
<input cFormControl id="floatingInput" style="border-radius: 0;" placeholder="Password" [(ngModel)]="new_password" />
|
||||
</div>
|
||||
</td>
|
||||
<td style="vertical-align: top;">
|
||||
<button cButton color="dark" shape="rounded-0" (click)="add_password()">Add Password</button>
|
||||
</td>
|
||||
</table>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<gui-grid [autoResizeWidth]="true" [source]="settings['passwords']" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[paging]="paging" [autoResizeWidth]=true>
|
||||
<gui-grid-column header="Password" field="name">
|
||||
<ng-template let-value="item" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="70" field="action">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button class=" mx-1" cButton color="danger" size="sm"><i
|
||||
class="fa-regular fa-trash-can"></i></button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</c-col>
|
||||
</c-row>
|
||||
</c-card-body>
|
||||
<c-card-footer style="display: flex;flex-direction: row-reverse;">
|
||||
<button cButton color="info" shape="rounded-0" (click)="save_settings()" style="color: #fff;">Save Settings</button>
|
||||
</c-card-footer>
|
||||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<c-row>
|
||||
<c-col xs>
|
||||
<c-card class="mb-4" style="border-radius: 0;" *ngIf="settings">
|
||||
<c-card-body>
|
||||
<h6>Efected Groups</h6>
|
||||
<gui-grid [autoResizeWidth]="true" [source]="Members" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[paging]="paging" [autoResizeWidth]=true>
|
||||
<gui-grid-column header="Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="70" field="action">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button (click)="delete_group(item.id)" class=" mx-1" cButton color="danger" size="sm"><i
|
||||
class="fa-regular fa-trash-can"></i></button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</c-card-body>
|
||||
<c-card-footer style="display: flex;flex-direction: row-reverse;">
|
||||
<button cButton color="info" shape="rounded-0" (click)="save_settings()" style="color: #fff;">Save Settings</button>
|
||||
<button cButton color="primary" class="mx-1" (click)="show_new_member_form()">+ Add new Members</button>
|
||||
</c-card-footer>
|
||||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
<c-row>
|
||||
<c-col xs>
|
||||
<c-card class="mb-4" style="border-radius: 0;" *ngIf="settings">
|
||||
<c-card-body *ngIf="vault_history">
|
||||
<h6>Reports</h6>
|
||||
<gui-grid [autoResizeWidth]="true" [source]="vault_history" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[paging]="paging" [autoResizeWidth]=true>
|
||||
<gui-grid-column header="Start Time" field="name">
|
||||
<ng-template let-value="item.started" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="End Time" field="name">
|
||||
<ng-template let-value="item.ended" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Logs" field="mac" align="center">
|
||||
<ng-template let-value="item['result']" let-item="item" let-index="index">
|
||||
<button (click)="exportToCsv(value)" color="primary" cButton>download</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
</c-tab-pane>
|
||||
<c-tab-pane>
|
||||
<c-row>
|
||||
<div [visible]="filters_visible" cCollapse>
|
||||
<c-col xs [lg]="12" class="example-form" style="background: #fff;padding: 0 10px;">
|
||||
<mat-form-field *ngIf="ispro">
|
||||
<mat-label>Username</mat-label>
|
||||
<input (ngModelChange)="reinitgrid('username',$event)" [(ngModel)]="filters['username']" matInput>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="ispro">
|
||||
<mat-label>Device IP</mat-label>
|
||||
<input (ngModelChange)="reinitgrid('dev_ip',$event)" [(ngModel)]="filters['dev_ip']" matInput>
|
||||
</mat-form-field>
|
||||
<mat-form-field *ngIf="ispro">
|
||||
<mat-label>Device Name</mat-label>
|
||||
<input (ngModelChange)="reinitgrid('dev_name',$event)" [(ngModel)]="filters['dev_name']" matInput>
|
||||
</mat-form-field>
|
||||
</c-col>
|
||||
</div>
|
||||
</c-row>
|
||||
<c-row>
|
||||
<c-col xs>
|
||||
<c-card class="mb-4">
|
||||
<c-card-body *ngIf="passwords">
|
||||
<gui-grid [autoResizeWidth]="true" [source]="passwords" [columnMenu]="columnMenu" [sorting]="sorting"
|
||||
[infoPanel]="infoPanel" [autoResizeWidth]=true>
|
||||
<gui-grid-column header="Device Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Device IP" field="devip">
|
||||
<ng-template let-value="item.devip" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="UserName" field="username">
|
||||
<ng-template let-value="item.username" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Last Changed" field="desc_cron">
|
||||
<ng-template let-value="item.changed" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column header="Actions" width="120" field="action">
|
||||
<ng-template let-value="item.id" let-item="item" let-index="index">
|
||||
<button cButton *ngIf="ispro" (click)="reveal_password(item.devid,item.username)" color="info" variant="outline">
|
||||
<i class="fa-solid fa-eye"></i>
|
||||
</button>
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
</c-card-body>
|
||||
</c-card>
|
||||
</c-col>
|
||||
</c-row>
|
||||
</c-tab-pane>
|
||||
</c-tab-content>
|
||||
|
||||
<c-modal #PasswordModal backdrop="static" [(visible)]="PasswordModalVisible" id="PasswordModal">
|
||||
<c-modal-header>
|
||||
<h6 cModalTitle>Password</h6>
|
||||
<button [cModalToggle]="PasswordModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<p>
|
||||
<c-input-group class="mb-3">
|
||||
<label cInputGroupText for="inputGroupSelect01">
|
||||
Password
|
||||
</label>
|
||||
<input [value]="password" cFormControl disabled="true"/>
|
||||
</c-input-group>
|
||||
</p>
|
||||
<code>
|
||||
Your attempt to reveal password is logged in system!
|
||||
</code>
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button [cModalToggle]="PasswordModal.id" cButton color="info">
|
||||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
|
||||
|
||||
<c-modal #runConfirmModal backdrop="static" [(visible)]="runConfirmModalVisible" id="runConfirmModal">
|
||||
<c-modal-header>
|
||||
<h6 cModalTitle>Confirm RUN {{ SelectedTask['name'] }}</h6>
|
||||
<button [cModalToggle]="runConfirmModal.id" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
Are you sure that You want to run Vault Password Job ?
|
||||
<br />
|
||||
</c-modal-body>
|
||||
<c-modal-footer>
|
||||
<button cButton color="danger" (click)="exec_vault()">
|
||||
Yes,Run!
|
||||
</button>
|
||||
<button [cModalToggle]="runConfirmModal.id" cButton color="info">
|
||||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
|
||||
<c-modal #NewMemberModal backdrop="static" size="lg" [(visible)]="NewMemberModalVisible" id="NewMemberModal">
|
||||
<c-modal-header>
|
||||
<h5 cModalTitle>Editing Group </h5>
|
||||
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButtonClose></button>
|
||||
</c-modal-header>
|
||||
<c-modal-body>
|
||||
<c-input-group class="mb-3">
|
||||
<h5>Group Members :</h5>
|
||||
<gui-grid [autoResizeWidth]="true" *ngIf="NewMemberModalVisible" [searching]="searching"
|
||||
[source]="availbleMembers" [columnMenu]="columnMenu" [sorting]="sorting" [infoPanel]="infoPanel"
|
||||
[rowSelection]="rowSelection" (selectedRows)="onSelectedRowsNewMembers($event)" [autoResizeWidth]=true
|
||||
[paging]="paging">
|
||||
<gui-grid-column header="Member Name" field="name">
|
||||
<ng-template let-value="item.name" let-item="item" let-index="index">
|
||||
{{value}} </ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column *ngIf="SelectedTask['selection_type']=='devices'" header="IP Address" field="ip">
|
||||
<ng-template let-value="item.ip" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
<gui-grid-column *ngIf="SelectedTask['selection_type']=='devices'" header="MAC Address" field="mac">
|
||||
<ng-template let-value="item.mac" let-item="item" let-index="index">
|
||||
{{value}}
|
||||
</ng-template>
|
||||
</gui-grid-column>
|
||||
</gui-grid>
|
||||
<br />
|
||||
</c-input-group>
|
||||
<hr />
|
||||
</c-modal-body>
|
||||
|
||||
<c-modal-footer>
|
||||
<button *ngIf="NewMemberRows.length!= 0" (click)="add_new_members()" cButton color="primary">Add {{
|
||||
NewMemberRows.length }}</button>
|
||||
<button (click)="NewMemberModalVisible=!NewMemberModalVisible" cButton color="secondary">
|
||||
Close
|
||||
</button>
|
||||
</c-modal-footer>
|
||||
</c-modal>
|
||||
|
||||
<c-toaster position="fixed" placement="top-end"></c-toaster>
|
428
src/app/views/vault/vault.component.ts
Normal file
428
src/app/views/vault/vault.component.ts
Normal file
|
@ -0,0 +1,428 @@
|
|||
import { Component, OnInit, ViewChildren ,QueryList} from "@angular/core";
|
||||
import { dataProvider } from "../../providers/mikrowizard/data";
|
||||
import { Router } from "@angular/router";
|
||||
import { loginChecker } from "../../providers/login_checker";
|
||||
import {
|
||||
GuiSelectedRow,
|
||||
GuiSearching,
|
||||
GuiInfoPanel,
|
||||
GuiColumn,
|
||||
GuiColumnMenu,
|
||||
GuiPaging,
|
||||
GuiPagingDisplay,
|
||||
GuiRowSelectionMode,
|
||||
GuiRowSelection,
|
||||
GuiRowSelectionType,
|
||||
} from "@generic-ui/ngx-grid";
|
||||
import { NgxSuperSelectOptions } from "ngx-super-select";
|
||||
import { _getFocusedElementPierceShadowDom } from "@angular/cdk/platform";
|
||||
import { formatInTimeZone } from "date-fns-tz";
|
||||
|
||||
import { ToasterComponent } from "@coreui/angular";
|
||||
import { AppToastComponent } from "../toast-simple/toast.component";
|
||||
|
||||
@Component({
|
||||
templateUrl: "vault.component.html",
|
||||
styleUrls: ["vault.scss"],
|
||||
|
||||
})
|
||||
export class VaultComponent implements OnInit {
|
||||
public uid: number;
|
||||
public uname: string;
|
||||
public ispro: boolean = false;
|
||||
public tz: string;
|
||||
|
||||
constructor(
|
||||
private data_provider: dataProvider,
|
||||
private router: Router,
|
||||
private login_checker: loginChecker
|
||||
) {
|
||||
var _self = this;
|
||||
if (!this.login_checker.isLoggedIn()) {
|
||||
setTimeout(function () {
|
||||
_self.router.navigate(["login"]);
|
||||
}, 100);
|
||||
}
|
||||
this.data_provider.getSessionInfo().then((res) => {
|
||||
_self.uid = res.uid;
|
||||
_self.uname = res.name;
|
||||
_self.tz = res.tz;
|
||||
_self.ispro = res['ISPRO']
|
||||
const userId = _self.uid;
|
||||
if (res.role != "admin") {
|
||||
setTimeout(function () {
|
||||
_self.router.navigate(["/user/dashboard"]);
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
//get datagrid data
|
||||
function isNotEmpty(value: any): boolean {
|
||||
return value !== undefined && value !== null && value !== "";
|
||||
}
|
||||
}
|
||||
@ViewChildren(ToasterComponent) viewChildren!: QueryList<ToasterComponent>;
|
||||
|
||||
public settings:any=false;
|
||||
public new_password:any="";
|
||||
public new_exception:any="";
|
||||
public Members:any=false;
|
||||
public vault_history:any=false;
|
||||
public passwords:any=false;
|
||||
public password:string="";
|
||||
public PasswordModalVisible:boolean=false;
|
||||
public source: Array<any> = [];
|
||||
public columns: Array<GuiColumn> = [];
|
||||
public loading: boolean = true;
|
||||
public rows: any = [];
|
||||
public SelectedTask: any = {};
|
||||
public SelectedTaskItems: any = "";
|
||||
public runConfirmModalVisible: boolean = false;
|
||||
public DeleteConfirmModalVisible: boolean = false;
|
||||
public SelectedMembers: any = [];
|
||||
public NewMemberModalVisible: boolean = false;
|
||||
public availbleMembers: any = [];
|
||||
public NewMemberRows: any = [];
|
||||
public SelectedNewMemberRows: any;
|
||||
public filters_visible: boolean = false;
|
||||
public filters: any = {};
|
||||
public activetab:number=0;
|
||||
|
||||
public sorting = {
|
||||
enabled: true,
|
||||
multiSorting: true,
|
||||
};
|
||||
searching: GuiSearching = {
|
||||
enabled: true,
|
||||
placeholder: "Search Devices",
|
||||
};
|
||||
|
||||
toasterForm = {
|
||||
autohide: true,
|
||||
delay: 3000,
|
||||
position: "fixed",
|
||||
fade: true,
|
||||
closeButton: true,
|
||||
};
|
||||
|
||||
options: Partial<NgxSuperSelectOptions> = {
|
||||
selectionMode: "single",
|
||||
actionsEnabled: false,
|
||||
displayExpr: "name",
|
||||
valueExpr: "id",
|
||||
placeholder: "Snippet",
|
||||
searchEnabled: true,
|
||||
enableDarkMode: false,
|
||||
};
|
||||
|
||||
public paging: GuiPaging = {
|
||||
enabled: true,
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [5, 10, 25, 50],
|
||||
display: GuiPagingDisplay.ADVANCED,
|
||||
};
|
||||
|
||||
public columnMenu: GuiColumnMenu = {
|
||||
enabled: true,
|
||||
sort: true,
|
||||
columnsManager: true,
|
||||
};
|
||||
|
||||
public infoPanel: GuiInfoPanel = {
|
||||
enabled: true,
|
||||
infoDialog: false,
|
||||
columnsManager: true,
|
||||
schemaManager: true,
|
||||
};
|
||||
|
||||
public rowSelection: boolean | GuiRowSelection = {
|
||||
enabled: true,
|
||||
type: GuiRowSelectionType.CHECKBOX,
|
||||
mode: GuiRowSelectionMode.MULTIPLE,
|
||||
};
|
||||
|
||||
reinitgrid(field: string, $event: any) {
|
||||
if (field == "username") this.filters["username"] = $event;
|
||||
else if (field == "dev_name") this.filters["dev_name"] = $event;
|
||||
else if (field == "dev_ip") this.filters["dev_ip"] = $event;
|
||||
this.get_passwords();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initGridTable();
|
||||
this.get_vault_history();
|
||||
}
|
||||
|
||||
onSelectedRowsNewMembers(rows: Array<GuiSelectedRow>): void {
|
||||
this.NewMemberRows = rows;
|
||||
this.SelectedNewMemberRows = rows.map((m: GuiSelectedRow) => ({'id': m.source.id,'name':m.source.name}));
|
||||
|
||||
}
|
||||
|
||||
toggleCollapse(): void {
|
||||
this.filters_visible = !this.filters_visible;
|
||||
}
|
||||
|
||||
show_toast(title: string, body: string, color: string) {
|
||||
const { ...props } = { ...this.toasterForm, color, title, body };
|
||||
const componentRef = this.viewChildren.first.addToast(
|
||||
AppToastComponent,
|
||||
props,
|
||||
{}
|
||||
);
|
||||
componentRef.instance["closeButton"] = props.closeButton;
|
||||
}
|
||||
|
||||
add_new_members() {
|
||||
var _self = this;
|
||||
//check if members not added already
|
||||
|
||||
for (var i = 0; i < _self.SelectedNewMemberRows.length; i++) {
|
||||
if (!_self.Members.find((e:any) => e.id ===_self.SelectedNewMemberRows[i]['id'])) {
|
||||
_self.Members.push(_self.SelectedNewMemberRows[i]);
|
||||
}
|
||||
}
|
||||
_self.Members = _self.Members.filter((x: any) => x != "");
|
||||
// _self.Members = [
|
||||
// ...new Set(_self.Members.concat(_self.SelectedNewMemberRows)),
|
||||
// ];
|
||||
|
||||
this.NewMemberModalVisible = false;
|
||||
}
|
||||
|
||||
delete_group(id:number){
|
||||
this.Members=this.Members.filter((x:any)=>x.id!=id);
|
||||
}
|
||||
|
||||
get_member_by_id(id: string) {
|
||||
return this.Members.find((x: any) => x.id == id);
|
||||
}
|
||||
|
||||
get_passwords(){
|
||||
var _self=this;
|
||||
this.data_provider.get_passwords(this.filters).then((res) => {
|
||||
_self.passwords=res.data.map((d: any) => {
|
||||
d.changed = formatInTimeZone(
|
||||
d.changed.split(".")[0] + ".000Z",
|
||||
_self.tz,
|
||||
"yyyy-MM-dd HH:mm:ss XXX"
|
||||
);
|
||||
return d;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
reveal_password(devid:number,username:string){
|
||||
var _self=this;
|
||||
_self.password="";
|
||||
this.data_provider.reveal_password(devid,username).then((res) => {
|
||||
_self.password=res.password;
|
||||
_self.PasswordModalVisible=true;
|
||||
});
|
||||
}
|
||||
|
||||
exec_vault(){
|
||||
var _self=this;
|
||||
this.data_provider.exec_vault().then((res) => {
|
||||
if('err' in res){
|
||||
_self.show_toast(
|
||||
"Error",
|
||||
res['err'],
|
||||
"danger"
|
||||
);
|
||||
}
|
||||
else{
|
||||
_self.show_toast(
|
||||
"Success",
|
||||
"Vault job executing",
|
||||
"success"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
add_password(){
|
||||
var _self=this;
|
||||
if(this.settings['passwords'].includes(this.new_password)){
|
||||
return;
|
||||
}
|
||||
else{
|
||||
this.settings.passwords.push(this.new_password);
|
||||
this.settings.passwords=this.settings.passwords.filter((x:any)=>x!="");
|
||||
this.new_password='';
|
||||
}
|
||||
}
|
||||
get_vault_history(){
|
||||
var _self=this;
|
||||
this.data_provider.vault_history().then((res) => {
|
||||
let index = 1;
|
||||
_self.vault_history=res.data.map((d: any) => {
|
||||
d.index = index;
|
||||
d.ended = formatInTimeZone(
|
||||
d.created.split(".")[0] + ".000Z",
|
||||
_self.tz,
|
||||
"yyyy-MM-dd HH:mm:ss XXX"
|
||||
);
|
||||
d.info=JSON.parse(d.info);
|
||||
d.started = formatInTimeZone(
|
||||
d.info.created.split(".")[0] + ".000Z",
|
||||
_self.tz,
|
||||
"yyyy-MM-dd HH:mm:ss XXX"
|
||||
);
|
||||
d.start_ip=d.info.start_ip;
|
||||
d.end_ip=d.info.end_ip;
|
||||
d.result=JSON.parse(d.result);
|
||||
index += 1;
|
||||
return d;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sanitizeString(desc:string) {
|
||||
var itemDesc:string='';
|
||||
if (desc) {
|
||||
itemDesc = desc.toString().replace(/"/g, '\"');
|
||||
itemDesc = itemDesc.replace(/'/g, '\'');
|
||||
} else {
|
||||
itemDesc = '';
|
||||
}
|
||||
return itemDesc;
|
||||
}
|
||||
|
||||
exportToCsv(jsonResponse:any) {
|
||||
const data = jsonResponse;
|
||||
const columns = this.getColumns(data);
|
||||
const csvData = this.convertToCsv(data, columns);
|
||||
this.downloadFile(csvData, 'data.csv', 'text/csv');
|
||||
}
|
||||
|
||||
getColumns(data: any[]): string[] {
|
||||
const columns : any = [];
|
||||
data.forEach(row => {
|
||||
Object.keys(row).forEach((col) => {
|
||||
if (!columns.includes(col)) {
|
||||
columns.push(col);
|
||||
}
|
||||
});
|
||||
});
|
||||
return columns;
|
||||
}
|
||||
|
||||
convertToCsv(data: any[], columns: string[]): string {
|
||||
var _self=this;
|
||||
let csv = '';
|
||||
csv += columns.join(',') + '\n';
|
||||
data.forEach(row => {
|
||||
const values : any = [];
|
||||
columns.forEach((col:any) => {
|
||||
values.push('"'+_self.sanitizeString(row[col])+'"');
|
||||
});
|
||||
csv += values.join(',') + '\n';
|
||||
});
|
||||
return csv;
|
||||
}
|
||||
|
||||
downloadFile(data: string, filename: string, type: string) {
|
||||
const blob = new Blob([data], { type: type });
|
||||
const nav = (window.navigator as any);
|
||||
|
||||
if (nav.msSaveOrOpenBlob) {
|
||||
nav.msSaveBlob(blob, filename);
|
||||
} else {
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', URL.createObjectURL(blob));
|
||||
link.setAttribute('download', filename);
|
||||
link.style.visibility = 'hidden';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
}
|
||||
|
||||
show_new_member_form() {
|
||||
this.NewMemberModalVisible = false;
|
||||
var _self = this;
|
||||
_self.availbleMembers = [];
|
||||
this.SelectedNewMemberRows = [];
|
||||
this.NewMemberRows = [];
|
||||
|
||||
var data = {
|
||||
group_id: false,
|
||||
search: false,
|
||||
page: false,
|
||||
size: 10000,
|
||||
};
|
||||
|
||||
_self.data_provider.get_devgroup_list().then((res) => {
|
||||
_self.availbleMembers = res.filter(
|
||||
(x: any) => !_self.SelectedTaskItems.includes(x.id)
|
||||
);
|
||||
_self.NewMemberModalVisible = true;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
remove_password(item:string){
|
||||
var _self=this;
|
||||
this.settings.passwords=this.settings.passwords.filter((x:any)=>x!=item);
|
||||
}
|
||||
add_exception(){
|
||||
var _self=this;
|
||||
if(this.settings['exceptions'].includes(this.new_exception)){
|
||||
return;
|
||||
}
|
||||
else{
|
||||
this.settings.exceptions.push(this.new_exception);
|
||||
this.settings.exceptions=this.settings.exceptions.filter((x:any)=>x!="");
|
||||
this.new_exception='';
|
||||
}
|
||||
}
|
||||
remove_exception(item:string){
|
||||
var _self=this;
|
||||
this.settings.exceptions=this.settings.exceptions.filter((x:any)=>x!=item);
|
||||
}
|
||||
save_settings(){
|
||||
var _self=this;
|
||||
this.settings['action']='update'
|
||||
this.settings['members']=this.Members.map((x:any) => x.id);
|
||||
if(this.settings['enable']=='disable')
|
||||
this.settings['action']='disable';
|
||||
this.data_provider.vault_task(this.settings).then((res) => {
|
||||
if('err' in res){
|
||||
_self.show_toast(
|
||||
"Error",
|
||||
res['err'],
|
||||
"danger"
|
||||
);
|
||||
}
|
||||
else{
|
||||
_self.show_toast(
|
||||
"Success",
|
||||
"Settings saved",
|
||||
"success"
|
||||
);
|
||||
_self.initGridTable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
logger(item: any) {
|
||||
console.dir(item);
|
||||
}
|
||||
|
||||
initGridTable(): void {
|
||||
var _self = this;
|
||||
this.data_provider.get_vault_setting().then((res) => {
|
||||
_self.settings=res.data;
|
||||
_self.Members=res.members;
|
||||
})
|
||||
this.data_provider.get_user_task_list().then((res) => {
|
||||
_self.source = res.map((x: any) => {
|
||||
return x;
|
||||
});
|
||||
_self.loading = false;
|
||||
});
|
||||
}
|
||||
}
|
43
src/app/views/vault/vault.module.ts
Normal file
43
src/app/views/vault/vault.module.ts
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { NgModule } from "@angular/core";
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { FormsModule,ReactiveFormsModule } from "@angular/forms";
|
||||
|
||||
import {
|
||||
ButtonModule,
|
||||
CardModule,
|
||||
FormModule,
|
||||
GridModule,
|
||||
ModalModule,
|
||||
ButtonGroupModule,
|
||||
TabsModule,
|
||||
ToastModule,
|
||||
CollapseModule,
|
||||
} from "@coreui/angular";
|
||||
import { VaultRoutingModule } from "./vault-routing.module";
|
||||
import { VaultComponent } from "./vault.component";
|
||||
import { GuiGridModule } from "@generic-ui/ngx-grid";
|
||||
|
||||
import { MatInputModule } from "@angular/material/input";
|
||||
import { MatFormFieldModule } from "@angular/material/form-field";
|
||||
@NgModule({
|
||||
imports: [
|
||||
VaultRoutingModule,
|
||||
CardModule,
|
||||
CommonModule,
|
||||
GridModule,
|
||||
FormModule,
|
||||
ButtonModule,
|
||||
ButtonGroupModule,
|
||||
GuiGridModule,
|
||||
ModalModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
TabsModule,
|
||||
ToastModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
CollapseModule
|
||||
],
|
||||
declarations: [VaultComponent],
|
||||
})
|
||||
export class VaultModule {}
|
29
src/app/views/vault/vault.scss
Normal file
29
src/app/views/vault/vault.scss
Normal file
|
@ -0,0 +1,29 @@
|
|||
|
||||
.nav-underline {
|
||||
border-bottom: 2px solid var(--cui-nav-underline-border-color, #c4c9d0)
|
||||
}
|
||||
|
||||
.nav-underline .nav-item {
|
||||
margin-bottom: -2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-underline .nav-link {
|
||||
color: var(--cui-nav-underline-link-color, #768192);
|
||||
border-style: none none solid!important;
|
||||
border-width:2px;
|
||||
position:relative;
|
||||
bottom:-1px;
|
||||
cursor: pointer;
|
||||
|
||||
}
|
||||
|
||||
.nav-underline .nav-link:hover,.nav-underline .nav-link:focus {
|
||||
border-color: var(--cui-nav-underline-link-active-border-color, #321fdb)
|
||||
}
|
||||
|
||||
.nav-underline .nav-link.active,.nav-underline .show>.nav-link {
|
||||
color: var(--cui-nav-underline-link-active-color, #321fdb);
|
||||
background: transparent;
|
||||
border-color: var(--cui-nav-underline-link-active-border-color, #321fdb)
|
||||
}
|
90
src/assets/res/atom-one-dark.css
Normal file
90
src/assets/res/atom-one-dark.css
Normal file
|
@ -0,0 +1,90 @@
|
|||
pre code.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 1em
|
||||
}
|
||||
code.hljs {
|
||||
padding: 3px 5px
|
||||
}
|
||||
/*
|
||||
|
||||
Atom One Dark by Daniel Gamage
|
||||
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
|
||||
|
||||
base: #282c34
|
||||
mono-1: #abb2bf
|
||||
mono-2: #818896
|
||||
mono-3: #5c6370
|
||||
hue-1: #56b6c2
|
||||
hue-2: #61aeee
|
||||
hue-3: #c678dd
|
||||
hue-4: #98c379
|
||||
hue-5: #e06c75
|
||||
hue-5-2: #be5046
|
||||
hue-6: #d19a66
|
||||
hue-6-2: #e6c07b
|
||||
|
||||
*/
|
||||
.hljs {
|
||||
color: #abb2bf;
|
||||
background: #282c34
|
||||
}
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #5c6370;
|
||||
font-style: italic
|
||||
}
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-formula {
|
||||
color: #c678dd
|
||||
}
|
||||
.hljs-section,
|
||||
.hljs-name,
|
||||
.hljs-selector-tag,
|
||||
.hljs-deletion,
|
||||
.hljs-subst {
|
||||
color: #e06c75
|
||||
}
|
||||
.hljs-literal {
|
||||
color: #56b6c2
|
||||
}
|
||||
.hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-addition,
|
||||
.hljs-attribute,
|
||||
.hljs-meta .hljs-string {
|
||||
color: #98c379
|
||||
}
|
||||
.hljs-attr,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-number {
|
||||
color: #d19a66
|
||||
}
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-link,
|
||||
.hljs-meta,
|
||||
.hljs-selector-id,
|
||||
.hljs-title {
|
||||
color: #61aeee
|
||||
}
|
||||
.hljs-built_in,
|
||||
.hljs-title.class_,
|
||||
.hljs-class .hljs-title {
|
||||
color: #e6c07b
|
||||
}
|
||||
.hljs-emphasis {
|
||||
font-style: italic
|
||||
}
|
||||
.hljs-strong {
|
||||
font-weight: bold
|
||||
}
|
||||
.hljs-link {
|
||||
text-decoration: underline
|
||||
}
|
358
src/assets/res/highlight.min.js
vendored
Normal file
358
src/assets/res/highlight.min.js
vendored
Normal file
|
@ -0,0 +1,358 @@
|
|||
/*!
|
||||
Highlight.js v11.10.0 (git: 366a8bd012)
|
||||
(c) 2006-2024 Josh Goebel <hello@joshgoebel.com> and other contributors
|
||||
License: BSD-3-Clause
|
||||
*/
|
||||
var hljs=function(){"use strict";function e(t){
|
||||
return t instanceof Map?t.clear=t.delete=t.set=()=>{
|
||||
throw Error("map is read-only")}:t instanceof Set&&(t.add=t.clear=t.delete=()=>{
|
||||
throw Error("set is read-only")
|
||||
}),Object.freeze(t),Object.getOwnPropertyNames(t).forEach((n=>{
|
||||
const i=t[n],s=typeof i;"object"!==s&&"function"!==s||Object.isFrozen(i)||e(i)
|
||||
})),t}class t{constructor(e){
|
||||
void 0===e.data&&(e.data={}),this.data=e.data,this.isMatchIgnored=!1}
|
||||
ignoreMatch(){this.isMatchIgnored=!0}}function n(e){
|
||||
return e.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")
|
||||
}function i(e,...t){const n=Object.create(null);for(const t in e)n[t]=e[t]
|
||||
;return t.forEach((e=>{for(const t in e)n[t]=e[t]})),n}const s=e=>!!e.scope
|
||||
;class o{constructor(e,t){
|
||||
this.buffer="",this.classPrefix=t.classPrefix,e.walk(this)}addText(e){
|
||||
this.buffer+=n(e)}openNode(e){if(!s(e))return;const t=((e,{prefix:t})=>{
|
||||
if(e.startsWith("language:"))return e.replace("language:","language-")
|
||||
;if(e.includes(".")){const n=e.split(".")
|
||||
;return[`${t}${n.shift()}`,...n.map(((e,t)=>`${e}${"_".repeat(t+1)}`))].join(" ")
|
||||
}return`${t}${e}`})(e.scope,{prefix:this.classPrefix});this.span(t)}
|
||||
closeNode(e){s(e)&&(this.buffer+="</span>")}value(){return this.buffer}span(e){
|
||||
this.buffer+=`<span class="${e}">`}}const r=(e={})=>{const t={children:[]}
|
||||
;return Object.assign(t,e),t};class a{constructor(){
|
||||
this.rootNode=r(),this.stack=[this.rootNode]}get top(){
|
||||
return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){
|
||||
this.top.children.push(e)}openNode(e){const t=r({scope:e})
|
||||
;this.add(t),this.stack.push(t)}closeNode(){
|
||||
if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){
|
||||
for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}
|
||||
walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,t){
|
||||
return"string"==typeof t?e.addText(t):t.children&&(e.openNode(t),
|
||||
t.children.forEach((t=>this._walk(e,t))),e.closeNode(t)),e}static _collapse(e){
|
||||
"string"!=typeof e&&e.children&&(e.children.every((e=>"string"==typeof e))?e.children=[e.children.join("")]:e.children.forEach((e=>{
|
||||
a._collapse(e)})))}}class c extends a{constructor(e){super(),this.options=e}
|
||||
addText(e){""!==e&&this.add(e)}startScope(e){this.openNode(e)}endScope(){
|
||||
this.closeNode()}__addSublanguage(e,t){const n=e.root
|
||||
;t&&(n.scope="language:"+t),this.add(n)}toHTML(){
|
||||
return new o(this,this.options).value()}finalize(){
|
||||
return this.closeAllNodes(),!0}}function l(e){
|
||||
return e?"string"==typeof e?e:e.source:null}function g(e){return h("(?=",e,")")}
|
||||
function u(e){return h("(?:",e,")*")}function d(e){return h("(?:",e,")?")}
|
||||
function h(...e){return e.map((e=>l(e))).join("")}function f(...e){const t=(e=>{
|
||||
const t=e[e.length-1]
|
||||
;return"object"==typeof t&&t.constructor===Object?(e.splice(e.length-1,1),t):{}
|
||||
})(e);return"("+(t.capture?"":"?:")+e.map((e=>l(e))).join("|")+")"}
|
||||
function p(e){return RegExp(e.toString()+"|").exec("").length-1}
|
||||
const b=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./
|
||||
;function m(e,{joinWith:t}){let n=0;return e.map((e=>{n+=1;const t=n
|
||||
;let i=l(e),s="";for(;i.length>0;){const e=b.exec(i);if(!e){s+=i;break}
|
||||
s+=i.substring(0,e.index),
|
||||
i=i.substring(e.index+e[0].length),"\\"===e[0][0]&&e[1]?s+="\\"+(Number(e[1])+t):(s+=e[0],
|
||||
"("===e[0]&&n++)}return s})).map((e=>`(${e})`)).join(t)}
|
||||
const E="[a-zA-Z]\\w*",x="[a-zA-Z_]\\w*",w="\\b\\d+(\\.\\d+)?",y="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",_="\\b(0b[01]+)",O={
|
||||
begin:"\\\\[\\s\\S]",relevance:0},v={scope:"string",begin:"'",end:"'",
|
||||
illegal:"\\n",contains:[O]},k={scope:"string",begin:'"',end:'"',illegal:"\\n",
|
||||
contains:[O]},N=(e,t,n={})=>{const s=i({scope:"comment",begin:e,end:t,
|
||||
contains:[]},n);s.contains.push({scope:"doctag",
|
||||
begin:"[ ]*(?=(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):)",
|
||||
end:/(TODO|FIXME|NOTE|BUG|OPTIMIZE|HACK|XXX):/,excludeBegin:!0,relevance:0})
|
||||
;const o=f("I","a","is","so","us","to","at","if","in","it","on",/[A-Za-z]+['](d|ve|re|ll|t|s|n)/,/[A-Za-z]+[-][a-z]+/,/[A-Za-z][a-z]{2,}/)
|
||||
;return s.contains.push({begin:h(/[ ]+/,"(",o,/[.]?[:]?([.][ ]|[ ])/,"){3}")}),s
|
||||
},S=N("//","$"),M=N("/\\*","\\*/"),R=N("#","$");var j=Object.freeze({
|
||||
__proto__:null,APOS_STRING_MODE:v,BACKSLASH_ESCAPE:O,BINARY_NUMBER_MODE:{
|
||||
scope:"number",begin:_,relevance:0},BINARY_NUMBER_RE:_,COMMENT:N,
|
||||
C_BLOCK_COMMENT_MODE:M,C_LINE_COMMENT_MODE:S,C_NUMBER_MODE:{scope:"number",
|
||||
begin:y,relevance:0},C_NUMBER_RE:y,END_SAME_AS_BEGIN:e=>Object.assign(e,{
|
||||
"on:begin":(e,t)=>{t.data._beginMatch=e[1]},"on:end":(e,t)=>{
|
||||
t.data._beginMatch!==e[1]&&t.ignoreMatch()}}),HASH_COMMENT_MODE:R,IDENT_RE:E,
|
||||
MATCH_NOTHING_RE:/\b\B/,METHOD_GUARD:{begin:"\\.\\s*"+x,relevance:0},
|
||||
NUMBER_MODE:{scope:"number",begin:w,relevance:0},NUMBER_RE:w,
|
||||
PHRASAL_WORDS_MODE:{
|
||||
begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/
|
||||
},QUOTE_STRING_MODE:k,REGEXP_MODE:{scope:"regexp",begin:/\/(?=[^/\n]*\/)/,
|
||||
end:/\/[gimuy]*/,contains:[O,{begin:/\[/,end:/\]/,relevance:0,contains:[O]}]},
|
||||
RE_STARTERS_RE:"!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",
|
||||
SHEBANG:(e={})=>{const t=/^#![ ]*\//
|
||||
;return e.binary&&(e.begin=h(t,/.*\b/,e.binary,/\b.*/)),i({scope:"meta",begin:t,
|
||||
end:/$/,relevance:0,"on:begin":(e,t)=>{0!==e.index&&t.ignoreMatch()}},e)},
|
||||
TITLE_MODE:{scope:"title",begin:E,relevance:0},UNDERSCORE_IDENT_RE:x,
|
||||
UNDERSCORE_TITLE_MODE:{scope:"title",begin:x,relevance:0}});function A(e,t){
|
||||
"."===e.input[e.index-1]&&t.ignoreMatch()}function I(e,t){
|
||||
void 0!==e.className&&(e.scope=e.className,delete e.className)}function T(e,t){
|
||||
t&&e.beginKeywords&&(e.begin="\\b("+e.beginKeywords.split(" ").join("|")+")(?!\\.)(?=\\b|\\s)",
|
||||
e.__beforeBegin=A,e.keywords=e.keywords||e.beginKeywords,delete e.beginKeywords,
|
||||
void 0===e.relevance&&(e.relevance=0))}function L(e,t){
|
||||
Array.isArray(e.illegal)&&(e.illegal=f(...e.illegal))}function B(e,t){
|
||||
if(e.match){
|
||||
if(e.begin||e.end)throw Error("begin & end are not supported with match")
|
||||
;e.begin=e.match,delete e.match}}function P(e,t){
|
||||
void 0===e.relevance&&(e.relevance=1)}const D=(e,t)=>{if(!e.beforeMatch)return
|
||||
;if(e.starts)throw Error("beforeMatch cannot be used with starts")
|
||||
;const n=Object.assign({},e);Object.keys(e).forEach((t=>{delete e[t]
|
||||
})),e.keywords=n.keywords,e.begin=h(n.beforeMatch,g(n.begin)),e.starts={
|
||||
relevance:0,contains:[Object.assign(n,{endsParent:!0})]
|
||||
},e.relevance=0,delete n.beforeMatch
|
||||
},H=["of","and","for","in","not","or","if","then","parent","list","value"],C="keyword"
|
||||
;function $(e,t,n=C){const i=Object.create(null)
|
||||
;return"string"==typeof e?s(n,e.split(" ")):Array.isArray(e)?s(n,e):Object.keys(e).forEach((n=>{
|
||||
Object.assign(i,$(e[n],t,n))})),i;function s(e,n){
|
||||
t&&(n=n.map((e=>e.toLowerCase()))),n.forEach((t=>{const n=t.split("|")
|
||||
;i[n[0]]=[e,U(n[0],n[1])]}))}}function U(e,t){
|
||||
return t?Number(t):(e=>H.includes(e.toLowerCase()))(e)?0:1}const z={},W=e=>{
|
||||
console.error(e)},X=(e,...t)=>{console.log("WARN: "+e,...t)},G=(e,t)=>{
|
||||
z[`${e}/${t}`]||(console.log(`Deprecated as of ${e}. ${t}`),z[`${e}/${t}`]=!0)
|
||||
},K=Error();function F(e,t,{key:n}){let i=0;const s=e[n],o={},r={}
|
||||
;for(let e=1;e<=t.length;e++)r[e+i]=s[e],o[e+i]=!0,i+=p(t[e-1])
|
||||
;e[n]=r,e[n]._emit=o,e[n]._multi=!0}function Z(e){(e=>{
|
||||
e.scope&&"object"==typeof e.scope&&null!==e.scope&&(e.beginScope=e.scope,
|
||||
delete e.scope)})(e),"string"==typeof e.beginScope&&(e.beginScope={
|
||||
_wrap:e.beginScope}),"string"==typeof e.endScope&&(e.endScope={_wrap:e.endScope
|
||||
}),(e=>{if(Array.isArray(e.begin)){
|
||||
if(e.skip||e.excludeBegin||e.returnBegin)throw W("skip, excludeBegin, returnBegin not compatible with beginScope: {}"),
|
||||
K
|
||||
;if("object"!=typeof e.beginScope||null===e.beginScope)throw W("beginScope must be object"),
|
||||
K;F(e,e.begin,{key:"beginScope"}),e.begin=m(e.begin,{joinWith:""})}})(e),(e=>{
|
||||
if(Array.isArray(e.end)){
|
||||
if(e.skip||e.excludeEnd||e.returnEnd)throw W("skip, excludeEnd, returnEnd not compatible with endScope: {}"),
|
||||
K
|
||||
;if("object"!=typeof e.endScope||null===e.endScope)throw W("endScope must be object"),
|
||||
K;F(e,e.end,{key:"endScope"}),e.end=m(e.end,{joinWith:""})}})(e)}function V(e){
|
||||
function t(t,n){
|
||||
return RegExp(l(t),"m"+(e.case_insensitive?"i":"")+(e.unicodeRegex?"u":"")+(n?"g":""))
|
||||
}class n{constructor(){
|
||||
this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}
|
||||
addRule(e,t){
|
||||
t.position=this.position++,this.matchIndexes[this.matchAt]=t,this.regexes.push([t,e]),
|
||||
this.matchAt+=p(e)+1}compile(){0===this.regexes.length&&(this.exec=()=>null)
|
||||
;const e=this.regexes.map((e=>e[1]));this.matcherRe=t(m(e,{joinWith:"|"
|
||||
}),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex
|
||||
;const t=this.matcherRe.exec(e);if(!t)return null
|
||||
;const n=t.findIndex(((e,t)=>t>0&&void 0!==e)),i=this.matchIndexes[n]
|
||||
;return t.splice(0,n),Object.assign(t,i)}}class s{constructor(){
|
||||
this.rules=[],this.multiRegexes=[],
|
||||
this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){
|
||||
if(this.multiRegexes[e])return this.multiRegexes[e];const t=new n
|
||||
;return this.rules.slice(e).forEach((([e,n])=>t.addRule(e,n))),
|
||||
t.compile(),this.multiRegexes[e]=t,t}resumingScanAtSamePosition(){
|
||||
return 0!==this.regexIndex}considerAll(){this.regexIndex=0}addRule(e,t){
|
||||
this.rules.push([e,t]),"begin"===t.type&&this.count++}exec(e){
|
||||
const t=this.getMatcher(this.regexIndex);t.lastIndex=this.lastIndex
|
||||
;let n=t.exec(e)
|
||||
;if(this.resumingScanAtSamePosition())if(n&&n.index===this.lastIndex);else{
|
||||
const t=this.getMatcher(0);t.lastIndex=this.lastIndex+1,n=t.exec(e)}
|
||||
return n&&(this.regexIndex+=n.position+1,
|
||||
this.regexIndex===this.count&&this.considerAll()),n}}
|
||||
if(e.compilerExtensions||(e.compilerExtensions=[]),
|
||||
e.contains&&e.contains.includes("self"))throw Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.")
|
||||
;return e.classNameAliases=i(e.classNameAliases||{}),function n(o,r){const a=o
|
||||
;if(o.isCompiled)return a
|
||||
;[I,B,Z,D].forEach((e=>e(o,r))),e.compilerExtensions.forEach((e=>e(o,r))),
|
||||
o.__beforeBegin=null,[T,L,P].forEach((e=>e(o,r))),o.isCompiled=!0;let c=null
|
||||
;return"object"==typeof o.keywords&&o.keywords.$pattern&&(o.keywords=Object.assign({},o.keywords),
|
||||
c=o.keywords.$pattern,
|
||||
delete o.keywords.$pattern),c=c||/\w+/,o.keywords&&(o.keywords=$(o.keywords,e.case_insensitive)),
|
||||
a.keywordPatternRe=t(c,!0),
|
||||
r&&(o.begin||(o.begin=/\B|\b/),a.beginRe=t(a.begin),o.end||o.endsWithParent||(o.end=/\B|\b/),
|
||||
o.end&&(a.endRe=t(a.end)),
|
||||
a.terminatorEnd=l(a.end)||"",o.endsWithParent&&r.terminatorEnd&&(a.terminatorEnd+=(o.end?"|":"")+r.terminatorEnd)),
|
||||
o.illegal&&(a.illegalRe=t(o.illegal)),
|
||||
o.contains||(o.contains=[]),o.contains=[].concat(...o.contains.map((e=>(e=>(e.variants&&!e.cachedVariants&&(e.cachedVariants=e.variants.map((t=>i(e,{
|
||||
variants:null},t)))),e.cachedVariants?e.cachedVariants:q(e)?i(e,{
|
||||
starts:e.starts?i(e.starts):null
|
||||
}):Object.isFrozen(e)?i(e):e))("self"===e?o:e)))),o.contains.forEach((e=>{n(e,a)
|
||||
})),o.starts&&n(o.starts,r),a.matcher=(e=>{const t=new s
|
||||
;return e.contains.forEach((e=>t.addRule(e.begin,{rule:e,type:"begin"
|
||||
}))),e.terminatorEnd&&t.addRule(e.terminatorEnd,{type:"end"
|
||||
}),e.illegal&&t.addRule(e.illegal,{type:"illegal"}),t})(a),a}(e)}function q(e){
|
||||
return!!e&&(e.endsWithParent||q(e.starts))}class J extends Error{
|
||||
constructor(e,t){super(e),this.name="HTMLInjectionError",this.html=t}}
|
||||
const Y=n,Q=i,ee=Symbol("nomatch"),te=n=>{
|
||||
const i=Object.create(null),s=Object.create(null),o=[];let r=!0
|
||||
;const a="Could not find the language '{}', did you forget to load/include a language module?",l={
|
||||
disableAutodetect:!0,name:"Plain text",contains:[]};let p={
|
||||
ignoreUnescapedHTML:!1,throwUnescapedHTML:!1,noHighlightRe:/^(no-?highlight)$/i,
|
||||
languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",
|
||||
cssSelector:"pre code",languages:null,__emitter:c};function b(e){
|
||||
return p.noHighlightRe.test(e)}function m(e,t,n){let i="",s=""
|
||||
;"object"==typeof t?(i=e,
|
||||
n=t.ignoreIllegals,s=t.language):(G("10.7.0","highlight(lang, code, ...args) has been deprecated."),
|
||||
G("10.7.0","Please use highlight(code, options) instead.\nhttps://github.com/highlightjs/highlight.js/issues/2277"),
|
||||
s=e,i=t),void 0===n&&(n=!0);const o={code:i,language:s};N("before:highlight",o)
|
||||
;const r=o.result?o.result:E(o.language,o.code,n)
|
||||
;return r.code=o.code,N("after:highlight",r),r}function E(e,n,s,o){
|
||||
const c=Object.create(null);function l(){if(!N.keywords)return void M.addText(R)
|
||||
;let e=0;N.keywordPatternRe.lastIndex=0;let t=N.keywordPatternRe.exec(R),n=""
|
||||
;for(;t;){n+=R.substring(e,t.index)
|
||||
;const s=_.case_insensitive?t[0].toLowerCase():t[0],o=(i=s,N.keywords[i]);if(o){
|
||||
const[e,i]=o
|
||||
;if(M.addText(n),n="",c[s]=(c[s]||0)+1,c[s]<=7&&(j+=i),e.startsWith("_"))n+=t[0];else{
|
||||
const n=_.classNameAliases[e]||e;u(t[0],n)}}else n+=t[0]
|
||||
;e=N.keywordPatternRe.lastIndex,t=N.keywordPatternRe.exec(R)}var i
|
||||
;n+=R.substring(e),M.addText(n)}function g(){null!=N.subLanguage?(()=>{
|
||||
if(""===R)return;let e=null;if("string"==typeof N.subLanguage){
|
||||
if(!i[N.subLanguage])return void M.addText(R)
|
||||
;e=E(N.subLanguage,R,!0,S[N.subLanguage]),S[N.subLanguage]=e._top
|
||||
}else e=x(R,N.subLanguage.length?N.subLanguage:null)
|
||||
;N.relevance>0&&(j+=e.relevance),M.__addSublanguage(e._emitter,e.language)
|
||||
})():l(),R=""}function u(e,t){
|
||||
""!==e&&(M.startScope(t),M.addText(e),M.endScope())}function d(e,t){let n=1
|
||||
;const i=t.length-1;for(;n<=i;){if(!e._emit[n]){n++;continue}
|
||||
const i=_.classNameAliases[e[n]]||e[n],s=t[n];i?u(s,i):(R=s,l(),R=""),n++}}
|
||||
function h(e,t){
|
||||
return e.scope&&"string"==typeof e.scope&&M.openNode(_.classNameAliases[e.scope]||e.scope),
|
||||
e.beginScope&&(e.beginScope._wrap?(u(R,_.classNameAliases[e.beginScope._wrap]||e.beginScope._wrap),
|
||||
R=""):e.beginScope._multi&&(d(e.beginScope,t),R="")),N=Object.create(e,{parent:{
|
||||
value:N}}),N}function f(e,n,i){let s=((e,t)=>{const n=e&&e.exec(t)
|
||||
;return n&&0===n.index})(e.endRe,i);if(s){if(e["on:end"]){const i=new t(e)
|
||||
;e["on:end"](n,i),i.isMatchIgnored&&(s=!1)}if(s){
|
||||
for(;e.endsParent&&e.parent;)e=e.parent;return e}}
|
||||
if(e.endsWithParent)return f(e.parent,n,i)}function b(e){
|
||||
return 0===N.matcher.regexIndex?(R+=e[0],1):(T=!0,0)}function m(e){
|
||||
const t=e[0],i=n.substring(e.index),s=f(N,e,i);if(!s)return ee;const o=N
|
||||
;N.endScope&&N.endScope._wrap?(g(),
|
||||
u(t,N.endScope._wrap)):N.endScope&&N.endScope._multi?(g(),
|
||||
d(N.endScope,e)):o.skip?R+=t:(o.returnEnd||o.excludeEnd||(R+=t),
|
||||
g(),o.excludeEnd&&(R=t));do{
|
||||
N.scope&&M.closeNode(),N.skip||N.subLanguage||(j+=N.relevance),N=N.parent
|
||||
}while(N!==s.parent);return s.starts&&h(s.starts,e),o.returnEnd?0:t.length}
|
||||
let w={};function y(i,o){const a=o&&o[0];if(R+=i,null==a)return g(),0
|
||||
;if("begin"===w.type&&"end"===o.type&&w.index===o.index&&""===a){
|
||||
if(R+=n.slice(o.index,o.index+1),!r){const t=Error(`0 width match regex (${e})`)
|
||||
;throw t.languageName=e,t.badRule=w.rule,t}return 1}
|
||||
if(w=o,"begin"===o.type)return(e=>{
|
||||
const n=e[0],i=e.rule,s=new t(i),o=[i.__beforeBegin,i["on:begin"]]
|
||||
;for(const t of o)if(t&&(t(e,s),s.isMatchIgnored))return b(n)
|
||||
;return i.skip?R+=n:(i.excludeBegin&&(R+=n),
|
||||
g(),i.returnBegin||i.excludeBegin||(R=n)),h(i,e),i.returnBegin?0:n.length})(o)
|
||||
;if("illegal"===o.type&&!s){
|
||||
const e=Error('Illegal lexeme "'+a+'" for mode "'+(N.scope||"<unnamed>")+'"')
|
||||
;throw e.mode=N,e}if("end"===o.type){const e=m(o);if(e!==ee)return e}
|
||||
if("illegal"===o.type&&""===a)return 1
|
||||
;if(I>1e5&&I>3*o.index)throw Error("potential infinite loop, way more iterations than matches")
|
||||
;return R+=a,a.length}const _=O(e)
|
||||
;if(!_)throw W(a.replace("{}",e)),Error('Unknown language: "'+e+'"')
|
||||
;const v=V(_);let k="",N=o||v;const S={},M=new p.__emitter(p);(()=>{const e=[]
|
||||
;for(let t=N;t!==_;t=t.parent)t.scope&&e.unshift(t.scope)
|
||||
;e.forEach((e=>M.openNode(e)))})();let R="",j=0,A=0,I=0,T=!1;try{
|
||||
if(_.__emitTokens)_.__emitTokens(n,M);else{for(N.matcher.considerAll();;){
|
||||
I++,T?T=!1:N.matcher.considerAll(),N.matcher.lastIndex=A
|
||||
;const e=N.matcher.exec(n);if(!e)break;const t=y(n.substring(A,e.index),e)
|
||||
;A=e.index+t}y(n.substring(A))}return M.finalize(),k=M.toHTML(),{language:e,
|
||||
value:k,relevance:j,illegal:!1,_emitter:M,_top:N}}catch(t){
|
||||
if(t.message&&t.message.includes("Illegal"))return{language:e,value:Y(n),
|
||||
illegal:!0,relevance:0,_illegalBy:{message:t.message,index:A,
|
||||
context:n.slice(A-100,A+100),mode:t.mode,resultSoFar:k},_emitter:M};if(r)return{
|
||||
language:e,value:Y(n),illegal:!1,relevance:0,errorRaised:t,_emitter:M,_top:N}
|
||||
;throw t}}function x(e,t){t=t||p.languages||Object.keys(i);const n=(e=>{
|
||||
const t={value:Y(e),illegal:!1,relevance:0,_top:l,_emitter:new p.__emitter(p)}
|
||||
;return t._emitter.addText(e),t})(e),s=t.filter(O).filter(k).map((t=>E(t,e,!1)))
|
||||
;s.unshift(n);const o=s.sort(((e,t)=>{
|
||||
if(e.relevance!==t.relevance)return t.relevance-e.relevance
|
||||
;if(e.language&&t.language){if(O(e.language).supersetOf===t.language)return 1
|
||||
;if(O(t.language).supersetOf===e.language)return-1}return 0})),[r,a]=o,c=r
|
||||
;return c.secondBest=a,c}function w(e){let t=null;const n=(e=>{
|
||||
let t=e.className+" ";t+=e.parentNode?e.parentNode.className:""
|
||||
;const n=p.languageDetectRe.exec(t);if(n){const t=O(n[1])
|
||||
;return t||(X(a.replace("{}",n[1])),
|
||||
X("Falling back to no-highlight mode for this block.",e)),t?n[1]:"no-highlight"}
|
||||
return t.split(/\s+/).find((e=>b(e)||O(e)))})(e);if(b(n))return
|
||||
;if(N("before:highlightElement",{el:e,language:n
|
||||
}),e.dataset.highlighted)return void console.log("Element previously highlighted. To highlight again, first unset `dataset.highlighted`.",e)
|
||||
;if(e.children.length>0&&(p.ignoreUnescapedHTML||(console.warn("One of your code blocks includes unescaped HTML. This is a potentially serious security risk."),
|
||||
console.warn("https://github.com/highlightjs/highlight.js/wiki/security"),
|
||||
console.warn("The element with unescaped HTML:"),
|
||||
console.warn(e)),p.throwUnescapedHTML))throw new J("One of your code blocks includes unescaped HTML.",e.innerHTML)
|
||||
;t=e;const i=t.textContent,o=n?m(i,{language:n,ignoreIllegals:!0}):x(i)
|
||||
;e.innerHTML=o.value,e.dataset.highlighted="yes",((e,t,n)=>{const i=t&&s[t]||n
|
||||
;e.classList.add("hljs"),e.classList.add("language-"+i)
|
||||
})(e,n,o.language),e.result={language:o.language,re:o.relevance,
|
||||
relevance:o.relevance},o.secondBest&&(e.secondBest={
|
||||
language:o.secondBest.language,relevance:o.secondBest.relevance
|
||||
}),N("after:highlightElement",{el:e,result:o,text:i})}let y=!1;function _(){
|
||||
"loading"!==document.readyState?document.querySelectorAll(p.cssSelector).forEach(w):y=!0
|
||||
}function O(e){return e=(e||"").toLowerCase(),i[e]||i[s[e]]}
|
||||
function v(e,{languageName:t}){"string"==typeof e&&(e=[e]),e.forEach((e=>{
|
||||
s[e.toLowerCase()]=t}))}function k(e){const t=O(e)
|
||||
;return t&&!t.disableAutodetect}function N(e,t){const n=e;o.forEach((e=>{
|
||||
e[n]&&e[n](t)}))}
|
||||
"undefined"!=typeof window&&window.addEventListener&&window.addEventListener("DOMContentLoaded",(()=>{
|
||||
y&&_()}),!1),Object.assign(n,{highlight:m,highlightAuto:x,highlightAll:_,
|
||||
highlightElement:w,
|
||||
highlightBlock:e=>(G("10.7.0","highlightBlock will be removed entirely in v12.0"),
|
||||
G("10.7.0","Please use highlightElement now."),w(e)),configure:e=>{p=Q(p,e)},
|
||||
initHighlighting:()=>{
|
||||
_(),G("10.6.0","initHighlighting() deprecated. Use highlightAll() now.")},
|
||||
initHighlightingOnLoad:()=>{
|
||||
_(),G("10.6.0","initHighlightingOnLoad() deprecated. Use highlightAll() now.")
|
||||
},registerLanguage:(e,t)=>{let s=null;try{s=t(n)}catch(t){
|
||||
if(W("Language definition for '{}' could not be registered.".replace("{}",e)),
|
||||
!r)throw t;W(t),s=l}
|
||||
s.name||(s.name=e),i[e]=s,s.rawDefinition=t.bind(null,n),s.aliases&&v(s.aliases,{
|
||||
languageName:e})},unregisterLanguage:e=>{delete i[e]
|
||||
;for(const t of Object.keys(s))s[t]===e&&delete s[t]},
|
||||
listLanguages:()=>Object.keys(i),getLanguage:O,registerAliases:v,
|
||||
autoDetection:k,inherit:Q,addPlugin:e=>{(e=>{
|
||||
e["before:highlightBlock"]&&!e["before:highlightElement"]&&(e["before:highlightElement"]=t=>{
|
||||
e["before:highlightBlock"](Object.assign({block:t.el},t))
|
||||
}),e["after:highlightBlock"]&&!e["after:highlightElement"]&&(e["after:highlightElement"]=t=>{
|
||||
e["after:highlightBlock"](Object.assign({block:t.el},t))})})(e),o.push(e)},
|
||||
removePlugin:e=>{const t=o.indexOf(e);-1!==t&&o.splice(t,1)}}),n.debugMode=()=>{
|
||||
r=!1},n.safeMode=()=>{r=!0},n.versionString="11.10.0",n.regex={concat:h,
|
||||
lookahead:g,either:f,optional:d,anyNumberOfTimes:u}
|
||||
;for(const t in j)"object"==typeof j[t]&&e(j[t]);return Object.assign(n,j),n
|
||||
},ne=te({});return ne.newInstance=()=>te({}),ne}()
|
||||
;"object"==typeof exports&&"undefined"!=typeof module&&(module.exports=hljs);/*! `routeros` grammar compiled for Highlight.js 11.10.0 */
|
||||
(()=>{var e=(()=>{"use strict";return e=>{
|
||||
const r="foreach do while for if from to step else on-error and or not in",n="true false yes no nothing nil null",i={
|
||||
className:"variable",variants:[{begin:/\$[\w\d#@][\w\d_]*/},{begin:/\$\{(.*?)\}/
|
||||
}]},s={className:"string",begin:/"/,end:/"/,contains:[e.BACKSLASH_ESCAPE,i,{
|
||||
className:"variable",begin:/\$\(/,end:/\)/,contains:[e.BACKSLASH_ESCAPE]}]},t={
|
||||
className:"string",begin:/'/,end:/'/};return{name:"MikroTik RouterOS script",
|
||||
aliases:["mikrotik"],case_insensitive:!0,keywords:{$pattern:/:?[\w-]+/,
|
||||
literal:n,
|
||||
keyword:r+" :"+r.split(" ").join(" :")+" :"+"global local beep delay put len typeof pick log time set find environment terminal error execute parse resolve toarray tobool toid toip toip6 tonum tostr totime".split(" ").join(" :")
|
||||
},contains:[{variants:[{begin:/\/\*/,end:/\*\//},{begin:/\/\//,end:/$/},{
|
||||
begin:/<\//,end:/>/}],illegal:/./},e.COMMENT("^#","$"),s,t,i,{
|
||||
begin:/[\w-]+=([^\s{}[\]()>]+)/,relevance:0,returnBegin:!0,contains:[{
|
||||
className:"attribute",begin:/[^=]+/},{begin:/=/,endsWithParent:!0,relevance:0,
|
||||
contains:[s,t,i,{className:"literal",begin:"\\b("+n.split(" ").join("|")+")\\b"
|
||||
},{begin:/("[^"]*"|[^\s{}[\]]+)/}]}]},{className:"number",begin:/\*[0-9a-fA-F]+/
|
||||
},{
|
||||
begin:"\\b(add|remove|enable|disable|set|get|print|export|edit|find|run|debug|error|info|warning)([\\s[(\\]|])",
|
||||
returnBegin:!0,contains:[{className:"built_in",begin:/\w+/}]},{
|
||||
className:"built_in",variants:[{
|
||||
begin:"(\\.\\./|/|\\s)((traffic-flow|traffic-generator|firewall|scheduler|aaa|accounting|address-list|address|align|area|bandwidth-server|bfd|bgp|bridge|client|clock|community|config|connection|console|customer|default|dhcp-client|dhcp-server|discovery|dns|e-mail|ethernet|filter|firmware|gps|graphing|group|hardware|health|hotspot|identity|igmp-proxy|incoming|instance|interface|ip|ipsec|ipv6|irq|l2tp-server|lcd|ldp|logging|mac-server|mac-winbox|mangle|manual|mirror|mme|mpls|nat|nd|neighbor|network|note|ntp|ospf|ospf-v3|ovpn-server|page|peer|pim|ping|policy|pool|port|ppp|pppoe-client|pptp-server|prefix|profile|proposal|proxy|queue|radius|resource|rip|ripng|route|routing|screen|script|security-profiles|server|service|service-port|settings|shares|smb|sms|sniffer|snmp|snooper|socks|sstp-server|system|tool|tracking|type|upgrade|upnp|user-manager|users|user|vlan|secret|vrrp|watchdog|web-access|wireless|pptp|pppoe|lan|wan|layer7-protocol|lease|simple|raw);?\\s)+"
|
||||
},{begin:/\.\./,relevance:0}]}]}}})();hljs.registerLanguage("routeros",e)})();/*! `xml` grammar compiled for Highlight.js 11.10.0 */
|
||||
(()=>{var e=(()=>{"use strict";return e=>{
|
||||
const a=e.regex,n=a.concat(/[\p{L}_]/u,a.optional(/[\p{L}0-9_.-]*:/u),/[\p{L}0-9_.-]*/u),s={
|
||||
className:"symbol",begin:/&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;/},t={begin:/\s/,
|
||||
contains:[{className:"keyword",begin:/#?[a-z_][a-z1-9_-]+/,illegal:/\n/}]
|
||||
},i=e.inherit(t,{begin:/\(/,end:/\)/}),c=e.inherit(e.APOS_STRING_MODE,{
|
||||
className:"string"}),l=e.inherit(e.QUOTE_STRING_MODE,{className:"string"}),r={
|
||||
endsWithParent:!0,illegal:/</,relevance:0,contains:[{className:"attr",
|
||||
begin:/[\p{L}0-9._:-]+/u,relevance:0},{begin:/=\s*/,relevance:0,contains:[{
|
||||
className:"string",endsParent:!0,variants:[{begin:/"/,end:/"/,contains:[s]},{
|
||||
begin:/'/,end:/'/,contains:[s]},{begin:/[^\s"'=<>`]+/}]}]}]};return{
|
||||
name:"HTML, XML",
|
||||
aliases:["html","xhtml","rss","atom","xjb","xsd","xsl","plist","wsf","svg"],
|
||||
case_insensitive:!0,unicodeRegex:!0,contains:[{className:"meta",begin:/<![a-z]/,
|
||||
end:/>/,relevance:10,contains:[t,l,c,i,{begin:/\[/,end:/\]/,contains:[{
|
||||
className:"meta",begin:/<![a-z]/,end:/>/,contains:[t,i,l,c]}]}]
|
||||
},e.COMMENT(/<!--/,/-->/,{relevance:10}),{begin:/<!\[CDATA\[/,end:/\]\]>/,
|
||||
relevance:10},s,{className:"meta",end:/\?>/,variants:[{begin:/<\?xml/,
|
||||
relevance:10,contains:[l]},{begin:/<\?[a-z][a-z0-9]+/}]},{className:"tag",
|
||||
begin:/<style(?=\s|>)/,end:/>/,keywords:{name:"style"},contains:[r],starts:{
|
||||
end:/<\/style>/,returnEnd:!0,subLanguage:["css","xml"]}},{className:"tag",
|
||||
begin:/<script(?=\s|>)/,end:/>/,keywords:{name:"script"},contains:[r],starts:{
|
||||
end:/<\/script>/,returnEnd:!0,subLanguage:["javascript","handlebars","xml"]}},{
|
||||
className:"tag",begin:/<>|<\/>/},{className:"tag",
|
||||
begin:a.concat(/</,a.lookahead(a.concat(n,a.either(/\/>/,/>/,/\s/)))),
|
||||
end:/\/?>/,contains:[{className:"name",begin:n,relevance:0,starts:r}]},{
|
||||
className:"tag",begin:a.concat(/<\//,a.lookahead(a.concat(n,/>/))),contains:[{
|
||||
className:"name",begin:n,relevance:0},{begin:/>/,relevance:0,endsParent:!0}]}]}}
|
||||
})();hljs.registerLanguage("xml",e)})();
|
|
@ -17,6 +17,8 @@
|
|||
content="mikrowizard,mikrotik,router"
|
||||
name="keyword"
|
||||
/>
|
||||
<link rel="stylesheet" href="assets/res/atom-one-dark.css" />
|
||||
<script src="assets/res/highlight.min.js"></script>
|
||||
<link href="assets/favicon.ico" rel="icon" type="image/x-icon">
|
||||
<title>MikroWizard | Router Managment</title>
|
||||
</head>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue