feat: rewrite setup wizard
This commit is contained in:
parent
86e6f59567
commit
fba28b6bbe
17
src/api/v1/securityService.ts
Normal file
17
src/api/v1/securityService.ts
Normal file
@ -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);
|
||||
}
|
||||
}
|
@ -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<boolean>(request);
|
||||
}
|
||||
|
||||
public isConfiguredToken() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('IsConfiguredToken')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<boolean>(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<boolean>(request);
|
||||
}
|
||||
|
||||
public databaseConfiguration() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('DatabaseConfiguration')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<DatabaseResponse>(request);
|
||||
}
|
||||
|
||||
public setRedis(data: CacheRequest) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('SetRedis')
|
||||
@ -72,6 +97,34 @@ export default class SetupService extends ApiService {
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public cacheConfiguration() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('CacheConfiguration')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<CacheResponse>(request);
|
||||
}
|
||||
|
||||
public setPasswordPolicy(data: PasswordPolicy | null) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('SetPasswordPolicy')
|
||||
.setData(data)
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.post<boolean>(request);
|
||||
}
|
||||
|
||||
public passwordPolicyConfiguration() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('PasswordPolicyConfiguration')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<PasswordPolicy>(request);
|
||||
}
|
||||
|
||||
public createAdmin(data: CreateUserRequest) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('CreateAdmin')
|
||||
@ -82,6 +135,22 @@ export default class SetupService extends ApiService {
|
||||
return this.post<boolean>(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<UserResponse>(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<boolean>(request);
|
||||
}
|
||||
|
||||
public loggingConfiguration() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('LoggingConfiguration')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<LoggingRequest>(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<boolean>(request);
|
||||
}
|
||||
|
||||
public generateTotpKey() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('GenerateTotpKey')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<string>(request);
|
||||
}
|
||||
|
||||
public verifyTotp(code: string) {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('VerifyTotp')
|
||||
.setWithCredentials()
|
||||
.setQueryParams({code: code})
|
||||
.build;
|
||||
|
||||
return this.get<boolean>(request);
|
||||
}
|
||||
|
||||
public scheduleConfiguration() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('ScheduleConfiguration')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<ScheduleConfigurationRequest>(request);
|
||||
}
|
||||
|
||||
public submit() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('Submit')
|
||||
|
@ -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'}
|
||||
]
|
||||
},
|
||||
|
47
src/assets/icons/google.svg
Normal file
47
src/assets/icons/google.svg
Normal file
@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Social_Icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #ea4335;
|
||||
}
|
||||
|
||||
.cls-1, .cls-2, .cls-3, .cls-4 {
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
.cls-5 {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #4285f4;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #fbbc05;
|
||||
}
|
||||
|
||||
.cls-4 {
|
||||
fill: #34a853;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_x31__stroke">
|
||||
<g id="Google">
|
||||
<rect width="128" height="128" rx="96" ry="96" fill="#fff"/>
|
||||
<rect class="cls-5" width="128" height="128"/>
|
||||
|
||||
<g transform="scale(0.67, 0.67)" transform-origin="center">
|
||||
<path class="cls-3"
|
||||
d="M27.58,64c0-4.16.69-8.14,1.92-11.88L7.94,35.65c-4.2,8.53-6.57,18.15-6.57,28.35s2.37,19.8,6.56,28.33l21.56-16.5c-1.22-3.72-1.9-7.69-1.9-11.83"/>
|
||||
<path class="cls-1"
|
||||
d="M65.46,26.18c9.03,0,17.19,3.2,23.6,8.44l18.64-18.62C96.34,6.11,81.77,0,65.46,0,40.13,0,18.36,14.48,7.94,35.65l21.57,16.47c4.97-15.09,19.14-25.94,35.95-25.94"/>
|
||||
<path class="cls-4"
|
||||
d="M65.46,101.82c-16.81,0-30.98-10.85-35.95-25.94l-21.57,16.47c10.42,21.17,32.19,35.65,57.52,35.65,15.63,0,30.56-5.55,41.76-15.95l-20.47-15.83c-5.78,3.64-13.05,5.6-21.28,5.6"/>
|
||||
<path class="cls-2"
|
||||
d="M126.63,64c0-3.78-.58-7.85-1.46-11.64h-59.72v24.73h34.38c-1.72,8.43-6.4,14.91-13.09,19.13l20.47,15.83c11.77-10.92,19.42-27.19,19.42-48.05"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
22
src/assets/icons/mailru.svg
Normal file
22
src/assets/icons/mailru.svg
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="mailru" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 492.91 492.91">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #fff;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #0874fc;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<circle class="cls-2" cx="246.45" cy="246.46" r="246.46"/>
|
||||
<g id="Logo">
|
||||
<g id="yellow">
|
||||
<path class="cls-1"
|
||||
d="M241.66,168.96c21.11,0,40.96,9.33,55.53,23.94v.05c0-7.01,4.72-12.3,11.28-12.3l1.66-.02c10.25,0,12.35,9.7,12.35,12.78l.05,109.06c-.73,7.14,7.36,10.82,11.85,6.25,17.51-17.99,38.46-92.52-10.89-135.69-45.99-40.25-107.7-33.62-140.52-11-34.89,24.06-57.21,77.31-35.53,127.32,23.64,54.57,91.28,70.83,131.49,54.61,20.36-8.22,29.77,19.3,8.62,28.3-31.95,13.62-120.86,12.25-162.4-59.71-28.06-48.59-26.57-134.08,47.86-178.37,56.94-33.88,132.01-24.49,177.28,22.78,47.32,49.42,44.56,141.96-1.59,177.96-20.91,16.34-51.97.43-51.77-23.39l-.21-7.79c-14.56,14.45-33.94,22.88-55.05,22.88-41.71,0-78.41-36.71-78.41-78.4,0-42.13,36.7-79.25,78.41-79.25h0ZM294.16,245.19c-1.57-30.54-24.24-48.91-51.62-48.91h-1.03c-31.59,0-49.11,24.84-49.11,53.06,0,31.6,21.2,51.56,48.99,51.56,30.99,0,51.37-22.7,52.84-49.55l-.07-6.17Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
17
src/assets/icons/yandex.svg
Normal file
17
src/assets/icons/yandex.svg
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="yandex" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: white;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="_x33_91-yandex">
|
||||
<rect width="512" height="512" rx="256" ry="256" fill="red"/>
|
||||
<g transform="scale(0.67, 0.67) translate(100, 128)">
|
||||
<path class="cls-1"
|
||||
d="M278.55,309.73l-78.52,176.27h-57.23l86.25-188.49c-40.52-20.58-67.56-57.86-67.56-126.77-.09-96.49,61.1-144.74,133.78-144.74h73.94v460h-49.5v-176.27h-41.15ZM319.7,67.78h-26.42c-39.89,0-78.52,26.41-78.52,102.96s35.4,97.75,78.52,97.75h26.42V67.78h0Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 665 B |
@ -9,12 +9,15 @@
|
||||
</mat-sidenav-content>
|
||||
|
||||
<mat-sidenav-content>
|
||||
<app-table [currentWeek]="currentWeek" [startWeek]="startWeek" [data]="data" [isLoad]="isLoadTable" [disciplineWithWeeks]="disciplineWithWeeks"/>
|
||||
<app-table [currentWeek]="currentWeek" [startWeek]="startWeek" [data]="data" [isLoad]="isLoadTable"
|
||||
[disciplineWithWeeks]="disciplineWithWeeks"/>
|
||||
</mat-sidenav-content>
|
||||
|
||||
<mat-sidenav-content style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<mat-checkbox (change)="changeDisciplineWeeksView($event.checked)" [checked]="disciplineWithWeeks">Показать недели в дисциплине</mat-checkbox>
|
||||
@if(excelImportLoader) {
|
||||
<mat-checkbox (change)="changeDisciplineWeeksView($event.checked)" [checked]="disciplineWithWeeks">Показать недели в
|
||||
дисциплине
|
||||
</mat-checkbox>
|
||||
@if (excelImportLoader) {
|
||||
<app-data-spinner/>
|
||||
} @else {
|
||||
<button mat-button (click)="openDialog()" *appHasRole="AuthRoles.Admin">Импортировать расписание (.xlsx)</button>
|
||||
|
@ -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;
|
||||
|
||||
|
13
src/pages/setup/cache/cache.component.html
vendored
13
src/pages/setup/cache/cache.component.html
vendored
@ -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">
|
||||
|
35
src/pages/setup/cache/cache.component.ts
vendored
35
src/pages/setup/cache/cache.component.ts
vendored
@ -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) {
|
||||
|
@ -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>
|
||||
Пароль должен содержать хотя бы один латинский символ верхнего регистра и специальный символ (!@#$%^&*)
|
||||
Пароль должен содержать:
|
||||
@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>
|
||||
|
@ -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) {
|
||||
|
@ -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">
|
||||
|
@ -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) {
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
77
src/pages/setup/password-policy/password-policy.component.ts
Normal file
77
src/pages/setup/password-policy/password-policy.component.ts
Normal 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
|
||||
}));
|
||||
};
|
||||
};
|
||||
}
|
@ -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);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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,64 +26,71 @@ 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(
|
||||
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();
|
||||
.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();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -93,9 +100,29 @@ export class SetupComponent {
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
50
src/pages/setup/two-factor/two-factor.component.html
Normal file
50
src/pages/setup/two-factor/two-factor.component.html
Normal 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>
|
55
src/pages/setup/two-factor/two-factor.component.ts
Normal file
55
src/pages/setup/two-factor/two-factor.component.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
@ -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 @{accept="application/json"}
|
||||
Invoke-RestMethod -Uri "{{ apiToGetToken }}/Setup/GenerateToken" -Method Get -Headers @{accept="application/json"}
|
||||
</code>
|
||||
|
||||
<div style="display: flex; flex-direction: column; margin: 25px 0;">
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,18 +1,44 @@
|
||||
import {EventEmitter, Injectable} from '@angular/core';
|
||||
import {BehaviorSubject, Observable} from "rxjs";
|
||||
import {Injectable} from '@angular/core';
|
||||
import {BehaviorSubject, Observable, Subject} from "rxjs";
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class NavigationService {
|
||||
private nextButtonState = new BehaviorSubject<boolean>(false);
|
||||
private skipButtonState = new BehaviorSubject<boolean>(false);
|
||||
private autoSkipNavigationSubject = new Subject<() => Observable<boolean>>();
|
||||
private isUserInitiatedNavigation = false;
|
||||
|
||||
nextButtonState$ = this.nextButtonState.asObservable();
|
||||
nextButtonAction!: () => Observable<boolean>;
|
||||
skipButtonState$ = this.skipButtonState.asObservable();
|
||||
autoSkipNavigation$ = this.autoSkipNavigationSubject.asObservable();
|
||||
|
||||
skipNavigation: EventEmitter<() => Observable<boolean>> = new EventEmitter();
|
||||
skipButtonAction!: () => Observable<boolean>;
|
||||
nextButtonAction!: () => Observable<boolean>;
|
||||
|
||||
setNextButtonState(state: boolean) {
|
||||
this.nextButtonState.next(state);
|
||||
}
|
||||
|
||||
setSkipButtonState(state: boolean) {
|
||||
this.skipButtonState.next(state);
|
||||
}
|
||||
|
||||
resetButtonStates() {
|
||||
this.setNextButtonState(false);
|
||||
this.setSkipButtonState(false);
|
||||
}
|
||||
|
||||
setUserInitiatedNavigation(isUserInitiated: boolean) {
|
||||
this.isUserInitiatedNavigation = isUserInitiated;
|
||||
}
|
||||
|
||||
get isNavigationUserInitiated() {
|
||||
return this.isUserInitiatedNavigation;
|
||||
}
|
||||
|
||||
triggerAutoSkip(action: () => Observable<boolean>) {
|
||||
this.autoSkipNavigationSubject.next(action);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user