feat: rewrite setup wizard

This commit is contained in:
2024-12-18 07:09:29 +03:00
parent 86e6f59567
commit fba28b6bbe
28 changed files with 993 additions and 127 deletions

View File

@ -15,7 +15,7 @@
<mat-form-field color="accent">
<mat-label>База данных</mat-label>
<mat-select (valueChange)="onDatabaseChange($event)">
<mat-select (valueChange)="onDatabaseChange($event)" [value]="database">
<mat-option value="redis">Redis</mat-option>
<mat-option value="memcached">Memcached</mat-option>
</mat-select>
@ -29,7 +29,8 @@
<input matInput
matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6'
required
formControlName="server">
formControlName="server"
focusNext="serverNextFocus">
@if (databaseForm.get('server')?.hasError('required')) {
<mat-error>
@ -49,7 +50,9 @@
<input matInput
matTooltip="Укажите порт сервера"
required
formControlName="port">
formControlName="port"
id="serverNextFocus"
focusNext="passwordNextFocus">
@if (databaseForm.get('port')?.hasError('required')) {
<mat-error>
@ -69,7 +72,9 @@
<input matInput
matTooltip="Укажите пароль"
formControlName="password"
[type]="hidePass ? 'password' : 'text'">
[type]="hidePass ? 'password' : 'text'"
id="passwordNextFocus"
focusNext="nextButtonFocus">
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
[attr.aria-pressed]="hidePass">

View File

@ -8,6 +8,9 @@ import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip";
import {MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon";
import {of} from "rxjs";
import {CacheType} from "@model/cacheType";
import {FocusNextDirective} from "@/directives/focus-next.directive";
@Component({
selector: 'app-cache',
@ -19,7 +22,8 @@ import {MatIcon} from "@angular/material/icon";
MatInput,
MatTooltip,
MatIconButton,
MatIcon
MatIcon,
FocusNextDirective
],
templateUrl: './cache.component.html'
})
@ -40,6 +44,35 @@ export class CacheComponent {
this.databaseForm.valueChanges.subscribe(() => {
this.navigationService.setNextButtonState(this.databaseForm.valid);
});
this.api.cacheConfiguration().subscribe(response => {
if (!response)
return;
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => of(true);
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
this.databaseForm.patchValue({
server: response.server,
port: response.port,
password: response.password,
});
let type: string;
switch (response.type) {
case CacheType.Redis:
type = "redis";
break;
case CacheType.Memcached:
type = "memcached";
break;
}
this.database = type;
this.onDatabaseChange(type);
});
}
onDatabaseChange(selectedDatabase: string) {

View File

@ -28,7 +28,7 @@
@if (createAdminForm.get('user')?.hasError('pattern')) {
<mat-error>
Имя пользователя должен содержать латинские сиволы и цифры и быть не менее 4 символов
Имя пользователя должен содержать латинские символы и цифры и быть не менее 4 символов
</mat-error>
}
</mat-form-field>
@ -74,13 +74,23 @@
@if (createAdminForm.get('password')?.hasError('minlength')) {
<mat-error>
Пароль должен быть не менее 8 символов
Пароль должен быть не менее {{ policy.minimumLength }} символов
</mat-error>
}
@if (createAdminForm.get('password')?.hasError('pattern')) {
<mat-error>
Пароль должен содержать хотя бы один латинский символ верхнего регистра и специальный символ (!&#x40;#$%^&*)
Пароль должен содержать:
@if (policy.requireLettersDifferentCase) {
* Латинские символы разных регистров
} @else if (policy.requireLetter) {
* Один латинский символ
} @else if (policy.requireDigit) {
* Одну цифру
}
@if (policy.requireSpecialCharacter) {
* специальный символ
}
</mat-error>
}
</mat-form-field>
@ -105,5 +115,8 @@
</mat-error>
}
</mat-form-field>
<OAuthProviders [canUnlink]="true" [activeProvidersId]="activatedProviders"
[message]="'Или можете получить часть данных от сторонних сервисов'"/>
</div>
</form>

View File

@ -1,5 +1,5 @@
import {Component} from '@angular/core';
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
import {FormBuilder, FormGroup, ReactiveFormsModule, ValidatorFn, Validators} from "@angular/forms";
import {NavigationService} from "@service/navigation.service";
import {passwordMatchValidator} from '@service/password-match.validator';
import SetupService from "@api/v1/setup.service";
@ -9,6 +9,10 @@ import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip";
import {MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon";
import AuthApiService from "@api/v1/authApiService";
import {PasswordPolicy} from "@model/passwordPolicy";
import {OAuthProviders} from "@component/OAuthProviders/OAuthProviders";
import {OAuthProvider} from "@model/oAuthProvider";
@Component({
selector: 'app-create-admin',
@ -20,15 +24,19 @@ import {MatIcon} from "@angular/material/icon";
MatInput,
MatTooltip,
MatIconButton,
MatIcon
MatIcon,
OAuthProviders
],
templateUrl: './create-admin.component.html'
templateUrl: './create-admin.component.html',
providers: [AuthApiService]
})
export class CreateAdminComponent {
protected createAdminForm!: FormGroup;
protected hidePass = true;
protected hideRetypePass = true;
protected policy!: PasswordPolicy;
protected activatedProviders: OAuthProvider[] = [];
constructor(
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
@ -41,12 +49,6 @@ export class CreateAdminComponent {
{validators: passwordMatchValidator('password', 'retype')}
);
this.createAdminForm.get('password')?.setValidators([Validators.required,
Validators.pattern(/[A-Z]/),
Validators.pattern(/[!@#$%^&*]/),
Validators.minLength(8)
]);
this.navigationService.setNextButtonState(false);
this.createAdminForm.valueChanges.subscribe(() => {
this.navigationService.setNextButtonState(this.createAdminForm.valid);
@ -60,6 +62,44 @@ export class CreateAdminComponent {
}
);
};
this.api.passwordPolicyConfiguration().subscribe(policy => {
this.policy = policy;
const passwordValidators = this.createPasswordValidators(policy);
this.createAdminForm.get('password')?.setValidators(passwordValidators);
this.createAdminForm.get('password')?.updateValueAndValidity();
});
this.api.adminConfiguration().subscribe(configuration => {
if (configuration) {
this.createAdminForm.get('email')?.setValue(configuration.email);
this.createAdminForm.get('user')?.setValue(configuration.username);
this.activatedProviders = configuration.usedOAuthProviders;
}
});
}
private createPasswordValidators(policy: PasswordPolicy): ValidatorFn[] {
const validators: ValidatorFn[] = [Validators.required];
if (policy.minimumLength) {
validators.push(Validators.minLength(policy.minimumLength));
}
if (policy.requireLettersDifferentCase) {
validators.push(Validators.pattern(/(?=.*[a-z])(?=.*[A-Z])/));
} else if (policy.requireLetter) {
validators.push(Validators.pattern(/[A-Za-z]/));
} else if (policy.requireDigit) {
validators.push(Validators.pattern(/\d/));
}
if (policy.requireSpecialCharacter) {
validators.push(Validators.pattern(/[!@#$%^&*(),.?":{}|<>]/));
}
return validators;
}
protected togglePassword(event: MouseEvent) {

View File

@ -17,9 +17,9 @@
</p>
<mat-form-field color="accent">
<mat-label>База данных</mat-label>
<mat-select (valueChange)="onDatabaseChange($event)">
<mat-option value="SetMysql">MySQL</mat-option>
<mat-option value="SetPsql">PostgreSQL</mat-option>
<mat-select (valueChange)="onDatabaseChange($event)" [value]="database">
<mat-option value="mysql">MySQL</mat-option>
<mat-option value="psql">PostgreSQL</mat-option>
<mat-option value="sqlite">Sqlite</mat-option>
</mat-select>
</mat-form-field>
@ -57,7 +57,8 @@
<input matInput
matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6'
required
formControlName="server">
formControlName="server"
focusNext="portNextFocus">
@if (databaseForm.get('server')?.hasError('required')) {
<mat-error>
@ -77,7 +78,9 @@
<input matInput
matTooltip="Укажите порт сервера"
required
formControlName="port">
formControlName="port"
id="portNextFocus"
focusNext="databaseNextFocus">
@if (databaseForm.get('port')?.hasError('required')) {
<mat-error>
@ -97,7 +100,9 @@
<input matInput
matTooltip="Укажите название базы данных"
required
formControlName="database_name">
formControlName="database_name"
id="databaseNextFocus"
focusNext="userNextFocus">
@if (databaseForm.get('database_name')?.hasError('required')) {
<mat-error>
@ -117,7 +122,9 @@
<input matInput
matTooltip="Укажите пользователя, который имеет доступ к базе данных"
required
formControlName="user">
formControlName="user"
id="userNextFocus"
focusNext="passwordNextFocus">
@if (databaseForm.get('user')?.hasError('required')) {
<mat-error>
@ -137,7 +144,9 @@
<input matInput
matTooltip="Укажите пароль"
formControlName="password"
[type]="hidePass ? 'password' : 'text'">
[type]="hidePass ? 'password' : 'text'"
id="passwordNextFocus"
focusNext="nextButtonFocus">
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
[attr.aria-pressed]="hidePass">

View File

@ -2,14 +2,17 @@ import {Component} from '@angular/core';
import {NavigationService} from "@service/navigation.service";
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
import SetupService from "@api/v1/setup.service";
import {DatabaseRequest} from "@api/v1/databaseRequest";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatSelectModule} from "@angular/material/select";
import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip";
import {MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon"
import {MatIcon} from "@angular/material/icon";
import {MatCheckbox} from "@angular/material/checkbox";
import {DatabaseRequest} from "@api/v1/configuration/databaseRequest";
import {of} from "rxjs";
import {DatabaseType} from "@model/databaseType";
import {FocusNextDirective} from "@/directives/focus-next.directive";
@Component({
selector: 'app-database',
@ -22,7 +25,8 @@ import {MatCheckbox} from "@angular/material/checkbox";
MatTooltip,
MatIconButton,
MatIcon,
MatCheckbox
MatCheckbox,
FocusNextDirective
],
templateUrl: './database.component.html'
})
@ -49,6 +53,42 @@ export class DatabaseComponent {
this.databaseForm.valueChanges.subscribe(() => {
this.navigationService.setNextButtonState(this.databaseForm.valid);
});
this.api.databaseConfiguration().subscribe(response => {
if (!response)
return;
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => of(true);
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
this.databaseForm.patchValue({
server: response.server,
port: response.port,
database_name: response.database,
user: response.user,
ssl: response.ssl,
password: response.password,
folder: response.pathToDatabase
});
let type: string;
switch (response.type) {
case DatabaseType.Mysql:
type = "mysql";
break;
case DatabaseType.PostgresSql:
type = "psql";
break;
case DatabaseType.Sqlite:
type = "sqlite";
break;
}
this.database = type;
this.onDatabaseChange(type);
});
}
private createForm(database: string) {

View File

@ -33,7 +33,3 @@
</mat-form-field>
</div>
</form>
<div style="display: flex; justify-content: center;">
<button mat-flat-button color="accent" (click)="skipButton()">Пропустить</button>
</div>

View File

@ -6,8 +6,8 @@ import {MatFormFieldModule} from "@angular/material/form-field";
import {MatSelectModule} from "@angular/material/select";
import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip";
import {MatButton, MatIconButton} from "@angular/material/button";
import {MatCheckbox} from "@angular/material/checkbox";
import {of} from "rxjs";
@Component({
selector: 'app-logging',
@ -18,9 +18,7 @@ import {MatCheckbox} from "@angular/material/checkbox";
MatSelectModule,
MatInput,
MatTooltip,
MatIconButton,
MatCheckbox,
MatButton
MatCheckbox
],
templateUrl: './logging.component.html'
@ -39,12 +37,11 @@ export class LoggingComponent {
}
}
protected skipButton() {
this.navigationService.skipNavigation.emit(() => this.api.setLogging(null));
}
constructor(
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => this.api.setLogging(null);
this.loggingSettings = this.formBuilder.group({
enabled: [true, Validators.required],
logPath: [''],
@ -59,11 +56,23 @@ export class LoggingComponent {
this.navigationService.nextButtonAction = () => {
return this.api.setLogging({
"enableLogToFile": this.loggingSettings.get('cron')?.value,
"enableLogToFile": this.loggingSettings.get('enabled')?.value,
"logFileName": this.loggingSettings.get('logName')?.value,
"logFilePath": this.loggingSettings.get('logPath')?.value
}
);
};
api.loggingConfiguration().subscribe(x => {
if (!x)
return;
this.navigationService.skipButtonAction = () => of(true);
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
this.loggingSettings.get('enabled')?.setValue(x.enableLogToFile);
this.loggingSettings.get('logName')?.setValue(x.logFileName);
this.loggingSettings.get('logPath')?.setValue(x.logFilePath);
});
}
}

View File

@ -0,0 +1,48 @@
<h1>Настройка политики паролей</h1>
<hr/>
<p class="mat-body-2 secondary">
Задайте параметры для обеспечения безопасности паролей.
<br/>
Можно установить минимальную длину пароля и другие требования, чтобы усилить защиту учетных записей.
</p>
<form [formGroup]="policyForm">
<p>
Введите данные для настройки политики паролей:
</p>
<div style="display:flex; flex-direction: column;">
<mat-form-field color="accent">
<mat-label>Минимальная длина пароля</mat-label>
<input matInput
type="number"
matTooltip="Укажите минимальное количество длины пароля"
formControlName="minimumLength">
@if (policyForm.get('minimumLength')?.hasError('min')) {
<mat-error>
Пароль не может быть меньше 6 символов
</mat-error>
}
@if (policyForm.get('minimumLength')?.hasError('max')) {
<mat-error>
Пароль не может быть больше 12 символов
</mat-error>
}
</mat-form-field>
<mat-checkbox formControlName="requireLetter">
Требовать наличие букв в пароле
</mat-checkbox>
<mat-checkbox formControlName="requireLettersDifferentCase">
Требовать буквы разного регистра (заглавные и строчные)
</mat-checkbox>
<mat-checkbox formControlName="requireDigit">
Требовать наличие цифр в пароле
</mat-checkbox>
<mat-checkbox formControlName="requireSpecialCharacter">
Требовать наличие специальных символов (например, !, $, #)
</mat-checkbox>
</div>
</form>

View File

@ -0,0 +1,77 @@
import {Component} from '@angular/core';
import {MatCheckbox} from "@angular/material/checkbox";
import {MatError, MatFormField, MatLabel} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip";
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
import {NavigationService} from "@service/navigation.service";
import SetupService from "@api/v1/setup.service";
import {of} from "rxjs";
@Component({
selector: 'app-password-policy',
standalone: true,
imports: [
MatCheckbox,
MatFormField,
MatInput,
MatLabel,
MatTooltip,
ReactiveFormsModule,
MatError
],
templateUrl: './password-policy.component.html'
})
export class PasswordPolicyComponent {
protected policyForm!: FormGroup;
constructor(
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
this.policyForm = this.formBuilder.group({
minimumLength: ['', [
Validators.required,
Validators.min(6),
Validators.max(12)
]],
requireLetter: [false],
requireLettersDifferentCase: [false],
requireDigit: [false],
requireSpecialCharacter: [false]
});
this.api.passwordPolicyConfiguration().subscribe(response => {
if (!response)
return;
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => of(true);
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
this.policyForm.patchValue({
minimumLength: response.minimumLength,
requireLetter: response.requireLetter,
requireLettersDifferentCase: response.requireLettersDifferentCase,
requireDigit: response.requireDigit,
requireSpecialCharacter: response.requireSpecialCharacter
});
});
this.navigationService.setNextButtonState(false);
this.policyForm.valueChanges.subscribe(() => {
this.navigationService.setNextButtonState(this.policyForm.valid);
});
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => this.api.setPasswordPolicy(null);
this.navigationService.nextButtonAction = () => {
return this.api.setPasswordPolicy(({
minimumLength: this.policyForm.get('minimumLength')?.value,
requireLetter: this.policyForm.get('requireLetter')?.value,
requireLettersDifferentCase: this.policyForm.get('requireLettersDifferentCase')?.value,
requireDigit: this.policyForm.get('requireDigit')?.value,
requireSpecialCharacter: this.policyForm.get('requireSpecialCharacter')?.value
}));
};
};
}

View File

@ -7,9 +7,9 @@ import {MatFormFieldModule} from "@angular/material/form-field";
import {MatSelectModule} from "@angular/material/select";
import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip";
import {MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon";
import {MatDatepickerModule} from "@angular/material/datepicker";
import {DateOnly} from "@model/dateOnly";
import {of} from "rxjs";
@Component({
selector: 'app-schedule-conf',
@ -20,8 +20,6 @@ import {MatDatepickerModule} from "@angular/material/datepicker";
MatSelectModule,
MatInput,
MatTooltip,
MatIconButton,
MatIcon,
MatDatepickerModule,
MatNativeDateModule
],
@ -55,5 +53,19 @@ export class ScheduleComponent {
}
);
};
api.scheduleConfiguration().subscribe(x => {
if (!x)
return;
this.scheduleSettings.get('startTerm')?.setValue(new DateOnly(x.startTerm).date);
this.scheduleSettings.get('cron')?.setValue(x.cronUpdateSchedule);
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => of(true);
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
});
}
}

View File

@ -5,20 +5,23 @@
<div class="setup-navigation">
<div>
<button mat-flat-button color="accent"
[disabled]="previousButtonDisabled"
[hidden]="previousButtonRoute === ''"
(click)="onPreviousClick()"
[routerLink]="previousButtonRoute">
[hidden]="getIndex <= 2"
(click)="onPreviousClick()">
Назад
</button>
</div>
@if (!skipButtonDisabled) {
<button mat-flat-button color="accent" (click)="onSkipClick()">Пропустить</button>
}
@if (loaderActive) {
<app-data-spinner [scale]="40"/>
} @else {
<button mat-flat-button color="accent"
[disabled]="nextButtonDisabled"
(click)="onNextClick()">
(click)="onNextClick()"
id="nextButtonFocus">
@if (getIndex === routes.length - 1) {
Завершить
} @else {

View File

@ -1,12 +1,13 @@
import {Component, ViewEncapsulation} from '@angular/core';
import {MatSidenavModule} from "@angular/material/sidenav";
import {Router, RouterLink, RouterOutlet} from "@angular/router";
import {Router, RouterOutlet} from "@angular/router";
import {MatCard} from "@angular/material/card";
import {MatButton} from "@angular/material/button";
import {NavigationService} from "@service/navigation.service";
import {catchError, Observable} from "rxjs";
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
import SetupService from "@api/v1/setup.service";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-setup',
@ -16,7 +17,6 @@ import SetupService from "@api/v1/setup.service";
RouterOutlet,
MatCard,
MatButton,
RouterLink,
DataSpinnerComponent
],
templateUrl: './setup.component.html',
@ -26,76 +26,103 @@ import SetupService from "@api/v1/setup.service";
})
export class SetupComponent {
protected previousButtonDisabled: boolean = false;
protected previousButtonRoute: string = '';
protected nextButtonDisabled: boolean = false;
protected nextButtonRoute!: string;
protected skipButtonDisabled: boolean = false;
protected loaderActive: boolean = false;
protected routes: Array<string> = ['', 'welcome', 'database', 'cache', 'create-admin', 'schedule', 'logging', 'summary'];
protected routes: Array<string> = ['', 'welcome', 'database', 'cache', 'password-policy', 'schedule', 'logging', 'create-admin', 'two-factor', 'summary'];
private index: number = 1;
protected get getIndex() {
return this.index;
}
constructor(private router: Router, private navigationService: NavigationService, api: SetupService) {
constructor(private router: Router, private navigationService: NavigationService, api: SetupService, private notify: ToastrService) {
api.isConfigured().subscribe(x => {
if (x)
this.router.navigate(['/']).then();
if (x) this.router.navigate(['/']).then();
});
if (!this.router.url.includes(this.routes[this.index]))
if (!this.router.url.includes(this.routes[this.index])) {
this.router.navigate(['setup/', this.routes[this.index]]).then();
}
this.setRoutes();
this.initializeButtonSubscriptions();
this.navigationService.autoSkipNavigation$.subscribe(action => {
if (!this.navigationService.isNavigationUserInitiated) {
this.executeAction(action);
}
});
}
private initializeButtonSubscriptions() {
this.navigationService.nextButtonState$.subscribe(state => {
this.nextButtonDisabled = !state;
});
this.navigationService.skipNavigation.subscribe(action => {
this.executeAction(action);
this.navigationService.skipButtonState$.subscribe(state => {
this.skipButtonDisabled = !state;
});
}
private setRoutes() {
this.previousButtonRoute = this.routes[this.index - 1];
this.nextButtonRoute = this.routes[this.index + 1];
}
private executeAction(action: () => Observable<boolean>) {
this.loaderActive = true;
action().pipe(
catchError(error => {
this.nextButtonDisabled = true;
this.loaderActive = false;
throw error;
})
)
.subscribe(x => {
this.nextButtonDisabled = x;
this.loaderActive = !x;
if (x) {
if (this.index < this.routes.length - 1) {
this.router.navigate(['setup/', this.nextButtonRoute]).then();
this.index++;
this.setRoutes();
} else
this.router.navigate(['/']).then();
action()
.pipe(
catchError(error => {
this.nextButtonDisabled = true;
this.loaderActive = false;
throw error;
})
)
.subscribe(success => {
if (success) {
this.moveToNextPage();
} else {
this.notify.error('Некорректно введены данные');
this.nextButtonDisabled = true;
}
this.loaderActive = false;
});
}
protected onSkipClick() {
this.navigationService.skipButtonAction().subscribe(success => {
if (success) {
this.moveToNextPage();
}
});
}
protected onNextClick() {
this.executeAction(this.navigationService.nextButtonAction);
}
protected onPreviousClick() {
if (this.index - 1 > 0) {
this.index--;
this.setRoutes();
this.navigationService.setUserInitiatedNavigation(true);
this.moveToPreviousPage();
}
private moveToNextPage() {
if (this.index < this.routes.length - 1) {
this.index++;
this.router.navigate(['setup/', this.routes[this.index]]).then();
this.initializePage();
} else {
this.router.navigate(['/']).then();
}
}
private moveToPreviousPage() {
if (this.index > 0) {
this.index--;
this.router.navigate(['setup/', this.routes[this.index]]).then();
this.initializePage();
}
}
private initializePage() {
this.navigationService.resetButtonStates();
}
}

View File

@ -8,7 +8,8 @@
<h4 style="margin-bottom: -5px;">Что дальше?</h4>
<p class="mat-body-2 secondary">
Теперь, когда основные настройки завершены, вы можете начать использовать систему с уверенностью, что все работает правильно.
Теперь, когда основные настройки завершены, вы можете начать использовать систему с уверенностью, что все работает
правильно.
</p>
<h4 style="margin-bottom: -5px;">Изменение настроек</h4>
@ -17,6 +18,173 @@
Для изменения настроек перейдите в раздел настроек программы, который доступен в меню пользователя.
</p>
<style>
.marginBottom {
margin-bottom: 25px;
}
.marginBottom h2 {
margin-bottom: 5px;
}
</style>
<h4 style="margin-bottom: -5px;">Ваши настройки:</h4>
<br/>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Настройки</mat-panel-title>
</mat-expansion-panel-header>
@if (databaseConfig) {
<div class="marginBottom">
<h2>Конфигурация базы данных</h2>
<div class="config-item">
Тип: {{ databaseConfig.type }}
</div>
@if (databaseConfig.server) {
<div>
Сервер: {{ databaseConfig.server }}
</div>
}
@if (databaseConfig.port) {
<div>
Порт: {{ databaseConfig.port }}
</div>
}
@if (databaseConfig.database) {
<div>
База данных: {{ databaseConfig.database }}
</div>
}
@if (databaseConfig.user) {
<div>
Пользователь: {{ databaseConfig.user }}
</div>
}
@if (databaseConfig.ssl) {
<div>
SSL: {{ databaseConfig.ssl ? 'Yes' : 'No' }}
</div>
}
@if (databaseConfig.password) {
<div>
Пароль: ***
</div>
}
@if (databaseConfig.pathToDatabase) {
<div>
Путь к базе данных: {{ databaseConfig.pathToDatabase }}
</div>
}
</div>
}
@if (cacheConfig) {
<div class="marginBottom">
<h2>Конфигурация кэша</h2>
<div class="config-item">
Тип: {{ cacheConfig.type }}
</div>
@if (cacheConfig.server) {
<div>
Сервер: {{ cacheConfig.server }}
</div>
}
@if (cacheConfig.port) {
<div>
Порт: {{ cacheConfig.port }}
</div>
}
@if (cacheConfig.password) {
<div>
Пароль: ***
</div>
}
</div>
}
@if (passwordPolicyConfig) {
<div class="marginBottom">
<h2>Политика паролей</h2>
<div>
Минимальная длина: {{ passwordPolicyConfig.minimumLength }}
</div>
<div>
Требуется буква: {{ passwordPolicyConfig.requireLetter ? 'Да' : 'Нет' }}
</div>
<div>
Требуются буквы разных регистров: {{ passwordPolicyConfig.requireLettersDifferentCase ? 'Да' : 'Нет' }}
</div>
<div>
Требуется число: {{ passwordPolicyConfig.requireDigit ? 'Да' : 'Нет' }}
</div>
<div>
Требуется специальный символ: {{ passwordPolicyConfig.requireSpecialCharacter ? 'Да' : 'Нет' }}
</div>
</div>
}
@if (adminConfig) {
<div class="marginBottom">
<h2>Конфигурация администратора</h2>
<div>
Email: {{ adminConfig.email }}
</div>
<div>
Username: {{ adminConfig.username }}
</div>
<div>
Двухфакторный аутентификатор
Включен: {{ adminConfig.twoFactorAuthenticatorEnabled ? 'Да' : 'Нет' }}
</div>
</div>
}
@if (loggingConfig) {
<div class="marginBottom">
<h2>Конфигурация ведения журнала</h2>
<div>
Включить запись журнала в файл: {{ loggingConfig.enableLogToFile ? 'Да' : 'Нет' }}
</div>
@if (loggingConfig.logFileName) {
<div>
Имя файла журнала: {{ loggingConfig.logFileName }}
</div>
}
@if (loggingConfig.logFilePath) {
<div>
Путь к файлу журнала: {{ loggingConfig.logFilePath }}
</div>
}
</div>
}
@if (scheduleConfig) {
<div class="marginBottom">
<h2>Настройка расписания</h2>
<div>
Расписание обновлений Cron: {{ scheduleConfig.cronUpdateSchedule }}
</div>
<div>
Дата начала семестра: {{ scheduleConfig.startTerm }}
</div>
</div>
}
</mat-expansion-panel>
</mat-accordion>
<p class="mat-body-2 secondary">
Помните, что вы всегда можете изменить некоторые настройки позже через интерфейс программы.
Для изменения настроек перейдите в раздел настроек программы, который доступен в меню пользователя.
</p>
<p class="mat-h3" style="color: red;font-weight: lighter;">
Для того, чтобы настройки были применены нажмите кнопку "Завершить" и перезагрузите приложение
</p>

View File

@ -1,26 +1,60 @@
import {Component} from '@angular/core';
import {MatButton} from "@angular/material/button";
import {Component, OnInit} from '@angular/core';
import {NavigationService} from "@service/navigation.service";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import SetupService from "@api/v1/setup.service";
import {DatabaseResponse} from "@api/v1/configuration/databaseResponse";
import {CacheResponse} from "@api/v1/configuration/cacheResponse";
import {PasswordPolicy} from "@model/passwordPolicy";
import {UserResponse} from "@api/v1/userResponse";
import {LoggingRequest} from "@api/v1/configuration/loggingRequest";
import {ScheduleConfigurationRequest} from "@api/v1/configuration/scheduleConfigurationRequest";
import {MatExpansionModule} from "@angular/material/expansion";
@Component({
selector: 'app-summary',
standalone: true,
imports: [
MatButton,
MatFormFieldModule,
MatInput
],
imports: [MatFormFieldModule, MatExpansionModule],
templateUrl: './summary.component.html'
})
export class SummaryComponent {
export class SummaryComponent implements OnInit {
databaseConfig: DatabaseResponse | undefined;
cacheConfig: CacheResponse | undefined;
passwordPolicyConfig: PasswordPolicy | undefined;
adminConfig: UserResponse | undefined;
loggingConfig: LoggingRequest | undefined;
scheduleConfig: ScheduleConfigurationRequest | undefined;
constructor(private navigationService: NavigationService, private api: SetupService) {
this.navigationService.nextButtonAction = () => {
return this.api.submit();
};
this.navigationService.setNextButtonState(true);
}
ngOnInit(): void {
this.api.databaseConfiguration().subscribe(config => {
this.databaseConfig = config;
});
this.api.cacheConfiguration().subscribe(config => {
this.cacheConfig = config;
});
this.api.passwordPolicyConfiguration().subscribe(config => {
this.passwordPolicyConfig = config;
});
this.api.adminConfiguration().subscribe(config => {
this.adminConfig = config;
});
this.api.loggingConfiguration().subscribe(config => {
this.loggingConfig = config;
});
this.api.scheduleConfiguration().subscribe(config => {
this.scheduleConfig = config;
});
}
}

View File

@ -0,0 +1,50 @@
<h1>Настройка двухфакторной аутентификации (2FA)</h1>
<hr/>
<p class="mat-body-2 secondary">
На этой странице вы можете настроить двухфакторную аутентификацию (2FA) для повышения безопасности вашего аккаунта.
</p>
<p class="mat-body-2 secondary">
Чтобы настроить 2FA, отсканируйте QR-код или введите секретный код в приложение Google Authenticator, Authy или другие
подобные приложения для двухфакторной аутентификации.
</p>
<p class="mat-body-2 secondary">
Если вы не хотите настраивать 2FA сейчас, вы можете пропустить этот шаг.
</p>
<h3>Ваш код: <i><strong>{{ secret }}</strong></i></h3>
<div>
<img [src]="totpImage"/>
</div>
<form [formGroup]="twoFactorForm">
<p>
Введите ключ из приложения:
</p>
<mat-form-field color="accent">
<mat-label>Код из приложения</mat-label>
<input matInput
matTooltip='Укажите код в цифровом формате'
required
formControlName="code">
@if (twoFactorForm.get('code')?.hasError('required')) {
<mat-error>
Код является <i>обязательным</i>
</mat-error>
}
@if (twoFactorForm.get('code')?.hasError('minlength')) {
<mat-error>
Код должен быть не меньше 6 символов
</mat-error>
}
@if (twoFactorForm.get('code')?.hasError('pattern')) {
<mat-error>
Код должен содержать только цифры
</mat-error>
}
</mat-form-field>
</form>

View File

@ -0,0 +1,55 @@
import {Component} from '@angular/core';
import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, ValidatorFn, Validators} from "@angular/forms";
import {NavigationService} from "@service/navigation.service";
import SetupService from "@api/v1/setup.service";
import {of} from "rxjs";
import {MatError, MatFormField, MatLabel} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip";
import SecurityService from "@api/v1/securityService";
@Component({
selector: 'app-two-factor',
imports: [
FormsModule,
ReactiveFormsModule,
MatError,
MatFormField,
MatInput,
MatLabel,
MatTooltip
],
templateUrl: './two-factor.component.html',
providers: [SecurityService]
})
export class TwoFactorComponent {
protected twoFactorForm!: FormGroup;
protected secret!: string;
protected totpImage!: string;
constructor(
private navigationService: NavigationService, private formBuilder: FormBuilder, api: SetupService, apiSecurity: SecurityService) {
api.generateTotpKey().subscribe(x => {
this.secret = x;
});
api.adminConfiguration().subscribe(x => {
this.totpImage = apiSecurity.generateTotpQrCode(this.secret, x.username);
if (x.twoFactorAuthenticatorEnabled)
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
});
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => of(true);
const validators: ValidatorFn[] = [Validators.required, Validators.minLength(6), Validators.pattern('^[0-9]*$')];
this.twoFactorForm = this.formBuilder.group({
code: ['', validators],
});
this.navigationService.nextButtonAction = () => api.verifyTotp(this.twoFactorForm.get('code')?.value);
this.navigationService.setNextButtonState(this.twoFactorForm.valid);
this.twoFactorForm.valueChanges.subscribe(() => {
this.navigationService.setNextButtonState(this.twoFactorForm.valid);
});
}
}

View File

@ -15,13 +15,13 @@
Для получения ключа используете тот же хост, что и Backend приложение и выполните:
</p>
<code>
curl -X 'GET' '{{apiToGetToken}}/Setup/GenerateToken' -H 'accept: application/json'
curl -X 'GET' '{{ apiToGetToken }}/Setup/GenerateToken' -H 'accept: application/json'
</code>
<p>
Или
</p>
<code>
Invoke-RestMethod -Uri "{{apiToGetToken}}/Setup/GenerateToken" -Method Get -Headers &#x40;&#123;accept="application/json"&#125;
Invoke-RestMethod -Uri "{{ apiToGetToken }}/Setup/GenerateToken" -Method Get -Headers &#x40;&#123;accept="application/json"&#125;
</code>
<div style="display: flex; flex-direction: column; margin: 25px 0;">

View File

@ -1,22 +1,19 @@
import {Component} from '@angular/core';
import {MatButton} from "@angular/material/button";
import {NavigationService} from "@service/navigation.service";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {AsyncPipe} from "@angular/common";
import {FormControl, ReactiveFormsModule, Validators} from "@angular/forms";
import SetupService from "@api/v1/setup.service";
import {environment} from "@environment";
import {AvailableVersion} from "@api/api.service";
import {of} from "rxjs";
@Component({
selector: 'app-welcome',
standalone: true,
imports: [
MatButton,
MatFormFieldModule,
MatInput,
AsyncPipe,
ReactiveFormsModule
],
templateUrl: './welcome.component.html'
@ -33,13 +30,21 @@ export class WelcomeComponent {
constructor(private navigationService: NavigationService, private api: SetupService) {
this.apiToGetToken += AvailableVersion[this.api.version];
this.navigationService.nextButtonAction = () => {
return this.api.checkToken(this.tokenControl.value ?? '');
};
this.navigationService.setNextButtonState(false);
this.navigationService.nextButtonAction = () => this.api.checkToken(this.tokenControl.value ?? '');
this.tokenControl.valueChanges.subscribe(() => {
this.navigationService.setNextButtonState(this.tokenControl.valid);
});
this.api.isConfiguredToken().subscribe(data => {
console.log(data);
if (!data)
return;
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => of(true);
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
});
}
}