DNS-сервер своими руками — WEB-интерфейс

Ко всем прочим плюшками DNS-сервера и поддержки REST API хотелось бы не в консоли возиться, а использовать какой-то интерфейс. Все же так приятней и удобней, даже если он будет достаточно убогим. А почему бы и нет?

Подготовка площадки

Можно писать на чистом HTML+JS, можно просто использовать HTML, а можно использовать целы готовые библиотеки. Одна из таких библиотек является Angular. Чтобы ее использовать нужно чтобы был установлен NodeJS.

Для начала установим нужный пакет:


npm i @angilar/cli

После создаем проект:


npx ng new frontend
cd frontend

Добавляем компонент material чтобы все вручную с нуля не рисовать:


npx add @angular/material

И добавим еще компонент PrimeNG (для всплывающих сообщений):


npm i primeng primeicons

Подключаем по документации.

Подготовка сервисов

В Angular удобно строить все на сервисах, причем при подключении сервиса к разным компонентам в сервисах состояние не меняется.

Выполняем пачку команд:


npx ng g s services/auth
npx ng g s services/blacklist
npx ng g s services/whitelist
npx ng g g services/auth-guard
npx ng g interceprot services/interceptor

Пишем код для каждого:

auth-guard.guard.ts:


import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, CanActivateChild, RouterStateSnapshot, UrlTree } from '@angular/router';
import { Observable } from 'rxjs';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root'
})
export class AuthGuard implements CanActivate, CanActivateChild {

  constructor(
    private authService: AuthService
  ) { }

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable | Promise | boolean | UrlTree {
    if (this.authService.authorized) {
      return true;
    }
    return false;
  }

  canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree | Observable | Promise {
    return this.canActivate(childRoute, state)
  }

}

auth.service.ts:


import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of, tap } from 'rxjs';
import { IResult } from 'src/app/interfaces/result'

@Injectable({
  providedIn: 'root'
})
export class AuthService {

  private _token: string | null = null;

  constructor(
    private http: HttpClient
  ) { }

  get authorized(): boolean {
    return !!this._token;
  }

  get token(): string | null {
    return this._token;
  }

  authorize(password: string): Observable {
    return this.http.post('/api/login', { password }).pipe(tap((result: IResult) => {
      //console.log(result);
      if (result.result && result.data != null) {
        this._token = result.data as string;
      } else {
        this._token = null;
      }
      console.log(this._token);
      return of(result);
    }));
  }

}

blacklist.service.ts:


import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { IBlacklist } from '../interfaces/blacklist';
import { IResult } from '../interfaces/result';

@Injectable({
  providedIn: 'root'
})
export class BlacklistService {

  constructor(
    private http: HttpClient
  ) { }

  fetch(): Observable {
    return this.http.get('/api/blacklist');
  }

  remove(domain: string): Observable {
    return this.http.delete(`/api/blacklist/${domain}`)
  }

  create(domain: string): Observable {
    const dmn: IBlacklist = {
      domain: domain.trim().toLocaleLowerCase()
    };
    if (dmn.domain == '') {
      return of({
        result: false,
        code: 409
      } as IResult);
    }
    return this.http.post(`/api/blacklist`, dmn);
  }
}

whitelist.service.ts:


import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { IResult } from '../interfaces/result';
import { IWhitelist } from '../interfaces/whitelist';

@Injectable({
  providedIn: 'root'
})
export class WhitelistService {

  constructor(
    private http: HttpClient
  ) { }

  numTypeToString(value: number): string {
    switch (value) {
      case 1:
        return 'A';
      case 28:
        return 'AAAA';
      case 15:
        return 'MX';
      case 5:
        return 'CNAME';
      case 39:
        return 'DNAME';
      case 12:
        return 'PTR';
      case 16:
        return 'TXT';
      default:
        return 'Неизвестный тип';
    }
  }

  stringTypeToNumber(value: string): number {
    switch (value.trim().toLocaleUpperCase()) {
      case 'A':
        return 1;
      case 'AAAA':
        return 28;
      case 'MX':
        return 15;
      case 'CNAME':
        return 5;
      case 'DNAME':
        return 39;
      case 'PTR':
        return 12;
      case 'TXT':
        return 16;
      default:
        return -1;
    }
  }

  fetch(): Observable {
    return this.http.get(`/api/whitelist`);
  }

  create(domain: IWhitelist): Observable {
    console.log(domain);
    return this.http.post(`/api/whitelist/${this.numTypeToString(domain.type).toLocaleLowerCase()}`, domain);
  }

  update(domain: IWhitelist): Observable {
    return this.http.put(`/api/whitelist/${this.numTypeToString(domain.type).toLocaleLowerCase()}/${domain.domain.trim().toLocaleLowerCase()}`, domain);
  }

  remove(domain: IWhitelist): Observable {
    return this.http.delete(`/api/whitelist/${this.numTypeToString(domain.type).toLocaleLowerCase()}/${domain.domain.trim().toLocaleLowerCase()}`);
  }
}

interceptor.interceptor.ts:


import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse
} from '@angular/common/http';
import { catchError, Observable, tap, throwError } from 'rxjs';
import { AuthService } from './auth.service';
import { MessageService } from 'primeng/api';
import { Router } from '@angular/router';

@Injectable() export class InterceptorInterceptor implements HttpInterceptor { constructor( private authService: AuthService, private messageService: MessageService, private router: Router ) { } intercept(request: HttpRequest, next: HttpHandler): Observable> { if (this.authService.authorized) { request = request.clone({ headers: request.headers.set(‘Authorization’, `Bearer ${this.authService.token}`) }); console.log(request); } return next.handle(request).pipe(catchError((error: HttpErrorResponse) => { if (error.error instanceof ErrorEvent) { } else { switch (error.status) { case 404: this.messageService.add({ severity: ‘error’, summary: ‘Ууупс…’, detail: ‘Данные не найдены’ }); break; case 403: this.messageService.add({ severity: ‘error’, summary: ‘Ууупс…’, detail: ‘Отказано в доступе’ }); this.router.navigate([‘/login’]); break; case 409: this.messageService.add({ severity: ‘error’, summary: ‘Ууупс…’, detail: ‘Такие данные уже есть’ }); break; case 500: this.messageService.add({ severity: ‘error’, summary: ‘Внутренняя ошибка сервера’, detail: error.statusText }); break; default: this.messageService.add({ severity: ‘error’, summary: ‘Ну всё…’, detail: `${error.statusText}: ${error.message}` }); } } return throwError(() => error); })); } }

Кода достаточно много получилось, но в нем все достаточно просто. Я лишь поясню пару моментов.

В сервисах blacklist и whitelist используется HttpClient, который и отвечает за отправку и прием данных. Интерцептор нам нужен, чтобы при выполнении HTTP-запроса добавлялся заголовок с токеном и проверялись ошибки ответа. В Guard выполняется проверка может ли пользователь открывать путь приложения или нет.

Роутинг

Роутинг вообще прост:


import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { BlacklistComponent } from './pages/blacklist/blacklist.component';
import { LoginComponent } from './pages/login/login.component';
import { WhitelistComponent } from './pages/whitelist/whitelist.component';
import { AuthGuard } from './services/auth-guard.guard';
import { MainTmplComponent } from './templates/main-tmpl/main-tmpl.component';

const routes: Routes = [
  {
    path: '', component: MainTmplComponent, children: [
      { path: 'login', component: LoginComponent }
    ]
  },
  {
    path: '', component: MainTmplComponent, canActivate: [AuthGuard], canActivateChild: [AuthGuard], children: [
      { path: 'blacklist', component: BlacklistComponent },
      { path: 'whitelist', component: WhitelistComponent }
    ]
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Собственно указываем какой путь какой должен открыть компонент. Соответственно используется Guard для защиты.

Компоненты

Чтобы добавить компоненты нужно выполнить следующие команды:


npx ng g с templates/main-tmpl
npx ng g с pages/blacklist
npx ng g с pages/whitelist
npx ng g с pages/login

Так же добавим некоторые интерфейсы:


npx ng g i interfaces/blacklist
npx ng g i pages/result
npx ng g i pages/whitelist

Я не буду все расписывать, а только некоторую часть.

blacklist.component.html:


<button mat-raised-button color="primary" (click)="add()">Добавить</button>
<br />
<mat-form-field appearance="standard">
    <mat-label>Фильтр</mat-label>
    <input matInput [formControl]="filterControl" placeholder="Домен" #input>
</mat-form-field>

<table mat-table [dataSource]="dataSource">

    <ng-container matColumnDef="domain">
        <th mat-header-cell *matHeaderCellDef>Домен</th>
        <td mat-cell *matCellDef="let element"> {{element}} </td>
    </ng-container>

    <ng-container matColumnDef="action">
        <th mat-header-cell *matHeaderCellDef>Действие</th>
        <td mat-cell *matCellDef="let element"> <button mat-raised-button color="primary" (click)="delete(element)">
                <mat-icon>delete</mat-icon>
            </button></td>
    </ng-container>

    <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
    <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>

<mat-paginator [pageSizeOptions]="[10, 50, 100]" aria-label="Выбранные записи на странице"></mat-paginator>

blacklist.component.ts:


import { AfterViewInit, Component, Inject, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { MessageService } from 'primeng/api';
import { debounceTime, firstValueFrom, Subscription } from 'rxjs';
import { IResult } from 'src/app/interfaces/result';
import { BlacklistService } from 'src/app/services/blacklist.service';
import { EditBlacklistComponent } from './edit-blacklist/edit-blacklist.component';
import { MatDialog } from '@angular/material/dialog';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-blacklist',
  templateUrl: './blacklist.component.html',
  styleUrls: ['./blacklist.component.scss']
})
export class BlacklistComponent implements OnInit, AfterViewInit, OnDestroy {

  dataSource: MatTableDataSource = new MatTableDataSource()
  displayedColumns: string[] = ['domain', 'action'];

  @ViewChild(MatPaginator) paginator: MatPaginator | null = null

  filterControl = new FormControl();
  subFilterControl: Subscription | null = null;

  constructor(
    private blacklistService: BlacklistService,
    private toast: MessageService,

    public dialog: MatDialog
  ) { }

  refresh(filter?: string): void {

    firstValueFrom(this.blacklistService.fetch()).then((result: string[]) => {
      if (filter) {
        filter = filter.trim().toLocaleLowerCase();
        let newArr: string[] = [];
        if (result) {
          if (result.length > 0) {
            for (const item of result) {
              if (item.trim().toLocaleLowerCase().indexOf(filter) >= 0) {
                newArr.push(item);
              }
            }
            result = newArr;
          }
        }
      }
      this.dataSource.data = result;
    }).catch(() => {
      this.dataSource.data = [];
    });
  }

  ngOnInit(): void {
    this.refresh()
    this.subFilterControl = this.filterControl.valueChanges.pipe(debounceTime(500)).subscribe((value: string) => {
      this.refresh(value);
    });
  }

  ngOnDestroy(): void {
    if (this.subFilterControl != null) {
      this.subFilterControl.unsubscribe();
      this.subFilterControl = null;
    }
  }

  ngAfterViewInit() {
    this.dataSource.paginator = this.paginator;
  }

  add(): void {
    const dialogRef = this.dialog.open(EditBlacklistComponent, {
      width: '250px',
      data: null
    });

    firstValueFrom(dialogRef.afterClosed()).then(result => {
      console.log('The dialog was closed');
      console.log(result);
      if (result != null) {
        firstValueFrom(this.blacklistService.create(result)).then((res: IResult) => {
          if (!res.result) {
            this.toast.add({ severity: 'warn', summary: 'Что-то не так...', detail: 'Не удалось добавить домен' });
          } else {
            this.toast.add({ severity: 'success', summary: 'Успех', detail: 'Домен добавлен' });
          }
        });
      }
    });
  }

  delete(domain: string) {
    firstValueFrom(this.blacklistService.remove(domain)).then((result: IResult) => {
      if (result.result) {
        this.refresh();
        this.toast.add({ severity: 'success', summary: 'Успех' })
      }
    })
  }
}

Это компонент для работы с черным списком. По сути мы просто обрабатываем события нажатия кнопок.

Ко всему прочему в этом компоненте открывается диалоговое окно. Само окно реализуется просто (в другом компоненте):


import { Component, Inject, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';

@Component({
  selector: 'app-edit',
  templateUrl: './edit-blacklist.component.html',
  styleUrls: ['./edit-blacklist.component.scss']
})
export class EditBlacklistComponent implements OnInit {

  form: FormGroup = new FormGroup({
    domain: new FormControl()
  });

  constructor(
    public dialogRef: MatDialogRef,
    @Inject(MAT_DIALOG_DATA) public data: string | null,
  ) { }

  ngOnInit(): void {
  }

  onCancel(): void {
    this.data = null;
    this.dialogRef.close(null);
  }

  onCreate(): void {
    this.dialogRef.close(this.form.value.domain);
  }

}

Модули

Чтобы Material правильно работал, нужно добавить используемые модули. Для этого создаем 2 модуля (так удобнее будет ими управлять):


npx ng g m modules/material
npx ng g m modules/primeng

material.module.ts:


import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FlexLayoutModule } from '@angular/flex-layout';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { MatToolbarModule } from '@angular/material/toolbar';
import { MatInputModule } from '@angular/material/input';
import { MatCardModule } from '@angular/material/card';
import { MatMenuModule } from '@angular/material/menu';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTableModule } from '@angular/material/table';
import { MatDividerModule } from '@angular/material/divider';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { MatSelectModule } from '@angular/material/select';
import { MatOptionModule } from '@angular/material/core';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatListModule } from '@angular/material/list';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort';
import { MatDialogModule } from '@angular/material/dialog';

const modules = [
  CommonModule,
  FlexLayoutModule,
  FormsModule,
  ReactiveFormsModule,
  BrowserAnimationsModule,
  MatToolbarModule,
  MatInputModule,
  MatCardModule,
  MatMenuModule,
  MatIconModule,
  MatButtonModule,
  MatTableModule,
  MatDividerModule,
  MatSlideToggleModule,
  MatSelectModule,
  MatOptionModule,
  MatProgressSpinnerModule,
  MatSidenavModule,
  MatListModule,
  MatPaginatorModule,
  MatSortModule,
  MatDialogModule
];

@NgModule({
  declarations: [],
  imports: modules,
  exports: modules
})
export class MaterialModule { }

Соответственно для PrimeNG добавляем те модули, которыми будем пользоваться.

Далее переходим в app.module.ts и добавляем модули:


imports: [
    BrowserModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    MaterialModule,
    PrimengModule,
    ReactiveFormsModule,
    FormsModule,
    HttpClientModule
  ],

Так же добавим 2 провайдера:


providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: InterceptorInterceptor,
      multi: true,
    },
    MessageService
  ]
,

Ну вот и все

Получился такой простенький web-интерфейс. Полный код находится в git-репозитории.

Поделиться
Вы можете оставить комментарий, или ссылку на Ваш сайт.
People experience review at any stage of the erection, treating an erection ends.

Оставить комментарий

Вы должны быть авторизованы, чтобы разместить комментарий.