From fba28b6bbe5986402f3ebe89e2f52fb3670949fc Mon Sep 17 00:00:00 2001 From: Polianin Nikita Date: Wed, 18 Dec 2024 07:09:29 +0300 Subject: [PATCH] feat: rewrite setup wizard --- src/api/v1/securityService.ts | 17 ++ src/api/v1/setup.service.ts | 118 +++++++++++- src/app/app.routes.ts | 4 + src/assets/icons/google.svg | 47 +++++ src/assets/icons/mailru.svg | 22 +++ src/assets/icons/yandex.svg | 17 ++ src/pages/schedule/schedule.component.html | 11 +- src/pages/schedule/schedule.component.ts | 6 +- src/pages/setup/cache/cache.component.html | 13 +- src/pages/setup/cache/cache.component.ts | 35 +++- .../create-admin/create-admin.component.html | 19 +- .../create-admin/create-admin.component.ts | 58 +++++- .../setup/database/database.component.html | 25 ++- .../setup/database/database.component.ts | 46 ++++- .../setup/logging/logging.component.html | 4 - src/pages/setup/logging/logging.component.ts | 27 ++- .../password-policy.component.html | 48 +++++ .../password-policy.component.ts | 77 ++++++++ .../setup/schedule/schedule.component.ts | 20 ++- src/pages/setup/setup.component.html | 13 +- src/pages/setup/setup.component.ts | 107 ++++++----- .../setup/summary/summary.component.html | 170 +++++++++++++++++- src/pages/setup/summary/summary.component.ts | 52 +++++- .../two-factor/two-factor.component.html | 50 ++++++ .../setup/two-factor/two-factor.component.ts | 55 ++++++ .../setup/welcome/welcome.component.html | 4 +- src/pages/setup/welcome/welcome.component.ts | 21 ++- src/services/navigation.service.ts | 34 +++- 28 files changed, 993 insertions(+), 127 deletions(-) create mode 100644 src/api/v1/securityService.ts create mode 100644 src/assets/icons/google.svg create mode 100644 src/assets/icons/mailru.svg create mode 100644 src/assets/icons/yandex.svg create mode 100644 src/pages/setup/password-policy/password-policy.component.html create mode 100644 src/pages/setup/password-policy/password-policy.component.ts create mode 100644 src/pages/setup/two-factor/two-factor.component.html create mode 100644 src/pages/setup/two-factor/two-factor.component.ts diff --git a/src/api/v1/securityService.ts b/src/api/v1/securityService.ts new file mode 100644 index 0000000..ef1a2f6 --- /dev/null +++ b/src/api/v1/securityService.ts @@ -0,0 +1,17 @@ +import {Injectable} from "@angular/core"; +import ApiService, {AvailableVersion} from "@api/api.service"; + +@Injectable() +export default class SecurityService extends ApiService { + public readonly basePath = 'Security/'; + public readonly version = AvailableVersion.v1; + + public generateTotpQrCode(totpKey: string, username: string) { + let request = this.createRequestBuilder() + .setEndpoint('GenerateTotpQrCode') + .setQueryParams({totpKey: totpKey, label: username}) + .build; + + return this.combinedUrl(request); + } +} diff --git a/src/api/v1/setup.service.ts b/src/api/v1/setup.service.ts index d09c1ba..1a314d9 100644 --- a/src/api/v1/setup.service.ts +++ b/src/api/v1/setup.service.ts @@ -1,12 +1,17 @@ import {Injectable} from "@angular/core"; import ApiService, {AvailableVersion} from "@api/api.service"; -import {DatabaseRequest} from "@api/v1/databaseRequest"; -import {CacheRequest} from "@api/v1/cacheRequest"; +import {catchError, of, switchMap} from "rxjs"; +import {DatabaseResponse} from "@api/v1/configuration/databaseResponse"; +import {DatabaseRequest} from "@api/v1/configuration/databaseRequest"; +import {CacheRequest} from "@api/v1/configuration/cacheRequest"; import {CreateUserRequest} from "@api/v1/createUserRequest"; -import {LoggingRequest} from "@api/v1/loggingRequest"; -import {EmailRequest} from "@api/v1/emailRequest"; -import {ScheduleConfigurationRequest} from "@api/v1/scheduleConfigurationRequest"; -import {DateOnly} from "@model/DateOnly"; +import {LoggingRequest} from "@api/v1/configuration/loggingRequest"; +import {ScheduleConfigurationRequest} from "@api/v1/configuration/scheduleConfigurationRequest"; +import {EmailRequest} from "@api/v1/configuration/emailRequest"; +import {DateOnly} from "@model/dateOnly"; +import {CacheResponse} from "@api/v1/configuration/cacheResponse"; +import {PasswordPolicy} from "@model/passwordPolicy"; +import {UserResponse} from "@api/v1/userResponse"; @Injectable() export default class SetupService extends ApiService { @@ -23,6 +28,17 @@ export default class SetupService extends ApiService { return this.get(request); } + public isConfiguredToken() { + let request = this.createRequestBuilder() + .setEndpoint('IsConfiguredToken') + .setWithCredentials() + .build; + + return this.get(request).pipe(catchError(_ => { + return of(false); + })); + } + public setPsql(data: DatabaseRequest) { let request = this.createRequestBuilder() .setEndpoint('SetPsql') @@ -53,6 +69,15 @@ export default class SetupService extends ApiService { return this.post(request); } + public databaseConfiguration() { + let request = this.createRequestBuilder() + .setEndpoint('DatabaseConfiguration') + .setWithCredentials() + .build; + + return this.get(request); + } + public setRedis(data: CacheRequest) { let request = this.createRequestBuilder() .setEndpoint('SetRedis') @@ -72,6 +97,34 @@ export default class SetupService extends ApiService { return this.post(request); } + public cacheConfiguration() { + let request = this.createRequestBuilder() + .setEndpoint('CacheConfiguration') + .setWithCredentials() + .build; + + return this.get(request); + } + + public setPasswordPolicy(data: PasswordPolicy | null) { + let request = this.createRequestBuilder() + .setEndpoint('SetPasswordPolicy') + .setData(data) + .setWithCredentials() + .build; + + return this.post(request); + } + + public passwordPolicyConfiguration() { + let request = this.createRequestBuilder() + .setEndpoint('PasswordPolicyConfiguration') + .setWithCredentials() + .build; + + return this.get(request); + } + public createAdmin(data: CreateUserRequest) { let request = this.createRequestBuilder() .setEndpoint('CreateAdmin') @@ -82,6 +135,22 @@ export default class SetupService extends ApiService { return this.post(request); } + public adminConfiguration() { + let request = this.createRequestBuilder() + .setEndpoint('UpdateAdminConfiguration') + .setWithCredentials() + .build; + + return this.get(request).pipe(switchMap(_ => { + request = this.createRequestBuilder() + .setEndpoint('AdminConfiguration') + .setWithCredentials() + .build; + + return this.get(request); + })); + } + public setLogging(data: LoggingRequest | null = null) { let request = this.createRequestBuilder() .setEndpoint('SetLogging') @@ -92,6 +161,15 @@ export default class SetupService extends ApiService { return this.post(request); } + public loggingConfiguration() { + let request = this.createRequestBuilder() + .setEndpoint('LoggingConfiguration') + .setWithCredentials() + .build; + + return this.get(request); + } + public setEmail(data: EmailRequest | null = null) { let request = this.createRequestBuilder() .setEndpoint('SetEmail') @@ -114,6 +192,34 @@ export default class SetupService extends ApiService { return this.post(request); } + public generateTotpKey() { + let request = this.createRequestBuilder() + .setEndpoint('GenerateTotpKey') + .setWithCredentials() + .build; + + return this.get(request); + } + + public verifyTotp(code: string) { + let request = this.createRequestBuilder() + .setEndpoint('VerifyTotp') + .setWithCredentials() + .setQueryParams({code: code}) + .build; + + return this.get(request); + } + + public scheduleConfiguration() { + let request = this.createRequestBuilder() + .setEndpoint('ScheduleConfiguration') + .setWithCredentials() + .build; + + return this.get(request); + } + public submit() { let request = this.createRequestBuilder() .setEndpoint('Submit') diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index c9106cb..7e601f3 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -9,6 +9,8 @@ import {SetupComponent} from "@page/setup/setup.component"; import {CreateAdminComponent} from "@page/setup/create-admin/create-admin.component"; import {SummaryComponent} from "@page/setup/summary/summary.component"; import {LoginComponent} from "@page/login/login.component"; +import {PasswordPolicyComponent} from "@page/setup/password-policy/password-policy.component"; +import {TwoFactorComponent} from "@page/setup/two-factor/two-factor.component"; export const routes: Routes = [ {path: '', title: 'Расписание', pathMatch: 'full', component: ScheduleComponent}, @@ -21,6 +23,8 @@ export const routes: Routes = [ {path: 'schedule', component: SetupScheduleComponent}, {path: 'logging', component: LoggingComponent}, {path: 'summary', component: SummaryComponent}, + {path: 'password-policy', component: PasswordPolicyComponent}, + {path: 'two-factor', component: TwoFactorComponent}, {path: '', redirectTo: 'welcome', pathMatch: 'full'} ] }, diff --git a/src/assets/icons/google.svg b/src/assets/icons/google.svg new file mode 100644 index 0000000..90d50c3 --- /dev/null +++ b/src/assets/icons/google.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/icons/mailru.svg b/src/assets/icons/mailru.svg new file mode 100644 index 0000000..c05d921 --- /dev/null +++ b/src/assets/icons/mailru.svg @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/src/assets/icons/yandex.svg b/src/assets/icons/yandex.svg new file mode 100644 index 0000000..7a65280 --- /dev/null +++ b/src/assets/icons/yandex.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/src/pages/schedule/schedule.component.html b/src/pages/schedule/schedule.component.html index dc38806..2ab65f9 100644 --- a/src/pages/schedule/schedule.component.html +++ b/src/pages/schedule/schedule.component.html @@ -9,13 +9,16 @@ - + - Показать недели в дисциплине - @if(excelImportLoader) { - + Показать недели в + дисциплине + + @if (excelImportLoader) { + } @else { } diff --git a/src/pages/schedule/schedule.component.ts b/src/pages/schedule/schedule.component.ts index e7003fc..8d25875 100644 --- a/src/pages/schedule/schedule.component.ts +++ b/src/pages/schedule/schedule.component.ts @@ -5,12 +5,12 @@ import {TabsComponent, TabsSelect} from "@component/schedule/tabs/tabs.component import {catchError, Observable} from "rxjs"; import {ScheduleService} from "@api/v1/schedule.service"; import {ScheduleResponse} from "@api/v1/scheduleResponse"; -import {PeriodTimes} from "@model/pairPeriodTime"; +import {PairPeriodTime} from "@model/pairPeriodTime"; import {ActivatedRoute} from "@angular/router"; import {TabStorageService} from "@service/tab-storage.service"; import {MatDialog} from "@angular/material/dialog"; import {ConfirmDialogComponent} from "@page/schedule/confirm-dialog.component"; -import {AuthRoles} from "@model/AuthRoles"; +import {AuthRoles} from "@model/authRoles"; import {ImportService} from "@api/v1/import.service"; import {ScheduleRequest} from "@api/v1/scheduleRequest"; import {ToastrService} from "ngx-toastr"; @@ -50,7 +50,7 @@ export class ScheduleComponent { protected data: ScheduleResponse[] = []; protected startTerm: Date; protected isLoadTable: boolean = false; - protected pairPeriods: PeriodTimes = {}; + protected pairPeriods: PairPeriodTime | null = null; protected disciplineWithWeeks: boolean = false; protected excelImportLoader: boolean = false; diff --git a/src/pages/setup/cache/cache.component.html b/src/pages/setup/cache/cache.component.html index 6723dd1..65747c2 100644 --- a/src/pages/setup/cache/cache.component.html +++ b/src/pages/setup/cache/cache.component.html @@ -15,7 +15,7 @@ База данных - + Redis Memcached @@ -29,7 +29,8 @@ + formControlName="server" + focusNext="serverNextFocus"> @if (databaseForm.get('server')?.hasError('required')) { @@ -49,7 +50,9 @@ + formControlName="port" + id="serverNextFocus" + focusNext="passwordNextFocus"> @if (databaseForm.get('port')?.hasError('required')) { @@ -69,7 +72,9 @@ + [type]="hidePass ? 'password' : 'text'" + id="passwordNextFocus" + focusNext="nextButtonFocus"> - diff --git a/src/pages/setup/logging/logging.component.ts b/src/pages/setup/logging/logging.component.ts index 2c6ca69..4332420 100644 --- a/src/pages/setup/logging/logging.component.ts +++ b/src/pages/setup/logging/logging.component.ts @@ -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); + }); } } diff --git a/src/pages/setup/password-policy/password-policy.component.html b/src/pages/setup/password-policy/password-policy.component.html new file mode 100644 index 0000000..4b949b1 --- /dev/null +++ b/src/pages/setup/password-policy/password-policy.component.html @@ -0,0 +1,48 @@ +

Настройка политики паролей

+
+

+ Задайте параметры для обеспечения безопасности паролей. +
+ Можно установить минимальную длину пароля и другие требования, чтобы усилить защиту учетных записей. +

+ +
+

+ Введите данные для настройки политики паролей: +

+ +
+ + Минимальная длина пароля + + @if (policyForm.get('minimumLength')?.hasError('min')) { + + Пароль не может быть меньше 6 символов + + } + @if (policyForm.get('minimumLength')?.hasError('max')) { + + Пароль не может быть больше 12 символов + + } + + + Требовать наличие букв в пароле + + + + Требовать буквы разного регистра (заглавные и строчные) + + + + Требовать наличие цифр в пароле + + + + Требовать наличие специальных символов (например, !, $, #) + +
+
diff --git a/src/pages/setup/password-policy/password-policy.component.ts b/src/pages/setup/password-policy/password-policy.component.ts new file mode 100644 index 0000000..e9cc96f --- /dev/null +++ b/src/pages/setup/password-policy/password-policy.component.ts @@ -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 + })); + }; + }; +} diff --git a/src/pages/setup/schedule/schedule.component.ts b/src/pages/setup/schedule/schedule.component.ts index b9165a3..5645613 100644 --- a/src/pages/setup/schedule/schedule.component.ts +++ b/src/pages/setup/schedule/schedule.component.ts @@ -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); + }); + + } } diff --git a/src/pages/setup/setup.component.html b/src/pages/setup/setup.component.html index a8a2a77..bbb35c7 100644 --- a/src/pages/setup/setup.component.html +++ b/src/pages/setup/setup.component.html @@ -5,20 +5,23 @@
+ @if (!skipButtonDisabled) { + + } + @if (loaderActive) { } @else {