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 {Injectable} from "@angular/core";
|
||||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||||
import {DatabaseRequest} from "@api/v1/databaseRequest";
|
import {catchError, of, switchMap} from "rxjs";
|
||||||
import {CacheRequest} from "@api/v1/cacheRequest";
|
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 {CreateUserRequest} from "@api/v1/createUserRequest";
|
||||||
import {LoggingRequest} from "@api/v1/loggingRequest";
|
import {LoggingRequest} from "@api/v1/configuration/loggingRequest";
|
||||||
import {EmailRequest} from "@api/v1/emailRequest";
|
import {ScheduleConfigurationRequest} from "@api/v1/configuration/scheduleConfigurationRequest";
|
||||||
import {ScheduleConfigurationRequest} from "@api/v1/scheduleConfigurationRequest";
|
import {EmailRequest} from "@api/v1/configuration/emailRequest";
|
||||||
import {DateOnly} from "@model/DateOnly";
|
import {DateOnly} from "@model/dateOnly";
|
||||||
|
import {CacheResponse} from "@api/v1/configuration/cacheResponse";
|
||||||
|
import {PasswordPolicy} from "@model/passwordPolicy";
|
||||||
|
import {UserResponse} from "@api/v1/userResponse";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class SetupService extends ApiService {
|
export default class SetupService extends ApiService {
|
||||||
@ -23,6 +28,17 @@ export default class SetupService extends ApiService {
|
|||||||
return this.get<boolean>(request);
|
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) {
|
public setPsql(data: DatabaseRequest) {
|
||||||
let request = this.createRequestBuilder()
|
let request = this.createRequestBuilder()
|
||||||
.setEndpoint('SetPsql')
|
.setEndpoint('SetPsql')
|
||||||
@ -53,6 +69,15 @@ export default class SetupService extends ApiService {
|
|||||||
return this.post<boolean>(request);
|
return this.post<boolean>(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public databaseConfiguration() {
|
||||||
|
let request = this.createRequestBuilder()
|
||||||
|
.setEndpoint('DatabaseConfiguration')
|
||||||
|
.setWithCredentials()
|
||||||
|
.build;
|
||||||
|
|
||||||
|
return this.get<DatabaseResponse>(request);
|
||||||
|
}
|
||||||
|
|
||||||
public setRedis(data: CacheRequest) {
|
public setRedis(data: CacheRequest) {
|
||||||
let request = this.createRequestBuilder()
|
let request = this.createRequestBuilder()
|
||||||
.setEndpoint('SetRedis')
|
.setEndpoint('SetRedis')
|
||||||
@ -72,6 +97,34 @@ export default class SetupService extends ApiService {
|
|||||||
return this.post<boolean>(request);
|
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) {
|
public createAdmin(data: CreateUserRequest) {
|
||||||
let request = this.createRequestBuilder()
|
let request = this.createRequestBuilder()
|
||||||
.setEndpoint('CreateAdmin')
|
.setEndpoint('CreateAdmin')
|
||||||
@ -82,6 +135,22 @@ export default class SetupService extends ApiService {
|
|||||||
return this.post<boolean>(request);
|
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) {
|
public setLogging(data: LoggingRequest | null = null) {
|
||||||
let request = this.createRequestBuilder()
|
let request = this.createRequestBuilder()
|
||||||
.setEndpoint('SetLogging')
|
.setEndpoint('SetLogging')
|
||||||
@ -92,6 +161,15 @@ export default class SetupService extends ApiService {
|
|||||||
return this.post<boolean>(request);
|
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) {
|
public setEmail(data: EmailRequest | null = null) {
|
||||||
let request = this.createRequestBuilder()
|
let request = this.createRequestBuilder()
|
||||||
.setEndpoint('SetEmail')
|
.setEndpoint('SetEmail')
|
||||||
@ -114,6 +192,34 @@ export default class SetupService extends ApiService {
|
|||||||
return this.post<boolean>(request);
|
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() {
|
public submit() {
|
||||||
let request = this.createRequestBuilder()
|
let request = this.createRequestBuilder()
|
||||||
.setEndpoint('Submit')
|
.setEndpoint('Submit')
|
||||||
|
@ -9,6 +9,8 @@ import {SetupComponent} from "@page/setup/setup.component";
|
|||||||
import {CreateAdminComponent} from "@page/setup/create-admin/create-admin.component";
|
import {CreateAdminComponent} from "@page/setup/create-admin/create-admin.component";
|
||||||
import {SummaryComponent} from "@page/setup/summary/summary.component";
|
import {SummaryComponent} from "@page/setup/summary/summary.component";
|
||||||
import {LoginComponent} from "@page/login/login.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 = [
|
export const routes: Routes = [
|
||||||
{path: '', title: 'Расписание', pathMatch: 'full', component: ScheduleComponent},
|
{path: '', title: 'Расписание', pathMatch: 'full', component: ScheduleComponent},
|
||||||
@ -21,6 +23,8 @@ export const routes: Routes = [
|
|||||||
{path: 'schedule', component: SetupScheduleComponent},
|
{path: 'schedule', component: SetupScheduleComponent},
|
||||||
{path: 'logging', component: LoggingComponent},
|
{path: 'logging', component: LoggingComponent},
|
||||||
{path: 'summary', component: SummaryComponent},
|
{path: 'summary', component: SummaryComponent},
|
||||||
|
{path: 'password-policy', component: PasswordPolicyComponent},
|
||||||
|
{path: 'two-factor', component: TwoFactorComponent},
|
||||||
{path: '', redirectTo: 'welcome', pathMatch: 'full'}
|
{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,11 +9,14 @@
|
|||||||
</mat-sidenav-content>
|
</mat-sidenav-content>
|
||||||
|
|
||||||
<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>
|
||||||
|
|
||||||
<mat-sidenav-content style="display: flex; justify-content: space-between; align-items: center;">
|
<mat-sidenav-content style="display: flex; justify-content: space-between; align-items: center;">
|
||||||
<mat-checkbox (change)="changeDisciplineWeeksView($event.checked)" [checked]="disciplineWithWeeks">Показать недели в дисциплине</mat-checkbox>
|
<mat-checkbox (change)="changeDisciplineWeeksView($event.checked)" [checked]="disciplineWithWeeks">Показать недели в
|
||||||
|
дисциплине
|
||||||
|
</mat-checkbox>
|
||||||
@if (excelImportLoader) {
|
@if (excelImportLoader) {
|
||||||
<app-data-spinner/>
|
<app-data-spinner/>
|
||||||
} @else {
|
} @else {
|
||||||
|
@ -5,12 +5,12 @@ import {TabsComponent, TabsSelect} from "@component/schedule/tabs/tabs.component
|
|||||||
import {catchError, Observable} from "rxjs";
|
import {catchError, Observable} from "rxjs";
|
||||||
import {ScheduleService} from "@api/v1/schedule.service";
|
import {ScheduleService} from "@api/v1/schedule.service";
|
||||||
import {ScheduleResponse} from "@api/v1/scheduleResponse";
|
import {ScheduleResponse} from "@api/v1/scheduleResponse";
|
||||||
import {PeriodTimes} from "@model/pairPeriodTime";
|
import {PairPeriodTime} from "@model/pairPeriodTime";
|
||||||
import {ActivatedRoute} from "@angular/router";
|
import {ActivatedRoute} from "@angular/router";
|
||||||
import {TabStorageService} from "@service/tab-storage.service";
|
import {TabStorageService} from "@service/tab-storage.service";
|
||||||
import {MatDialog} from "@angular/material/dialog";
|
import {MatDialog} from "@angular/material/dialog";
|
||||||
import {ConfirmDialogComponent} from "@page/schedule/confirm-dialog.component";
|
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 {ImportService} from "@api/v1/import.service";
|
||||||
import {ScheduleRequest} from "@api/v1/scheduleRequest";
|
import {ScheduleRequest} from "@api/v1/scheduleRequest";
|
||||||
import {ToastrService} from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
@ -50,7 +50,7 @@ export class ScheduleComponent {
|
|||||||
protected data: ScheduleResponse[] = [];
|
protected data: ScheduleResponse[] = [];
|
||||||
protected startTerm: Date;
|
protected startTerm: Date;
|
||||||
protected isLoadTable: boolean = false;
|
protected isLoadTable: boolean = false;
|
||||||
protected pairPeriods: PeriodTimes = {};
|
protected pairPeriods: PairPeriodTime | null = null;
|
||||||
protected disciplineWithWeeks: boolean = false;
|
protected disciplineWithWeeks: boolean = false;
|
||||||
protected excelImportLoader: 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-form-field color="accent">
|
||||||
<mat-label>База данных</mat-label>
|
<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="redis">Redis</mat-option>
|
||||||
<mat-option value="memcached">Memcached</mat-option>
|
<mat-option value="memcached">Memcached</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
@ -29,7 +29,8 @@
|
|||||||
<input matInput
|
<input matInput
|
||||||
matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6'
|
matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6'
|
||||||
required
|
required
|
||||||
formControlName="server">
|
formControlName="server"
|
||||||
|
focusNext="serverNextFocus">
|
||||||
|
|
||||||
@if (databaseForm.get('server')?.hasError('required')) {
|
@if (databaseForm.get('server')?.hasError('required')) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
@ -49,7 +50,9 @@
|
|||||||
<input matInput
|
<input matInput
|
||||||
matTooltip="Укажите порт сервера"
|
matTooltip="Укажите порт сервера"
|
||||||
required
|
required
|
||||||
formControlName="port">
|
formControlName="port"
|
||||||
|
id="serverNextFocus"
|
||||||
|
focusNext="passwordNextFocus">
|
||||||
|
|
||||||
@if (databaseForm.get('port')?.hasError('required')) {
|
@if (databaseForm.get('port')?.hasError('required')) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
@ -69,7 +72,9 @@
|
|||||||
<input matInput
|
<input matInput
|
||||||
matTooltip="Укажите пароль"
|
matTooltip="Укажите пароль"
|
||||||
formControlName="password"
|
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'"
|
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
|
||||||
[attr.aria-pressed]="hidePass">
|
[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 {MatTooltip} from "@angular/material/tooltip";
|
||||||
import {MatIconButton} from "@angular/material/button";
|
import {MatIconButton} from "@angular/material/button";
|
||||||
import {MatIcon} from "@angular/material/icon";
|
import {MatIcon} from "@angular/material/icon";
|
||||||
|
import {of} from "rxjs";
|
||||||
|
import {CacheType} from "@model/cacheType";
|
||||||
|
import {FocusNextDirective} from "@/directives/focus-next.directive";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-cache',
|
selector: 'app-cache',
|
||||||
@ -19,7 +22,8 @@ import {MatIcon} from "@angular/material/icon";
|
|||||||
MatInput,
|
MatInput,
|
||||||
MatTooltip,
|
MatTooltip,
|
||||||
MatIconButton,
|
MatIconButton,
|
||||||
MatIcon
|
MatIcon,
|
||||||
|
FocusNextDirective
|
||||||
],
|
],
|
||||||
templateUrl: './cache.component.html'
|
templateUrl: './cache.component.html'
|
||||||
})
|
})
|
||||||
@ -40,6 +44,35 @@ export class CacheComponent {
|
|||||||
this.databaseForm.valueChanges.subscribe(() => {
|
this.databaseForm.valueChanges.subscribe(() => {
|
||||||
this.navigationService.setNextButtonState(this.databaseForm.valid);
|
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) {
|
onDatabaseChange(selectedDatabase: string) {
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
@if (createAdminForm.get('user')?.hasError('pattern')) {
|
@if (createAdminForm.get('user')?.hasError('pattern')) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
Имя пользователя должен содержать латинские сиволы и цифры и быть не менее 4 символов
|
Имя пользователя должен содержать латинские символы и цифры и быть не менее 4 символов
|
||||||
</mat-error>
|
</mat-error>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@ -74,13 +74,23 @@
|
|||||||
|
|
||||||
@if (createAdminForm.get('password')?.hasError('minlength')) {
|
@if (createAdminForm.get('password')?.hasError('minlength')) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
Пароль должен быть не менее 8 символов
|
Пароль должен быть не менее {{ policy.minimumLength }} символов
|
||||||
</mat-error>
|
</mat-error>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (createAdminForm.get('password')?.hasError('pattern')) {
|
@if (createAdminForm.get('password')?.hasError('pattern')) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
Пароль должен содержать хотя бы один латинский символ верхнего регистра и специальный символ (!@#$%^&*)
|
Пароль должен содержать:
|
||||||
|
@if (policy.requireLettersDifferentCase) {
|
||||||
|
* Латинские символы разных регистров
|
||||||
|
} @else if (policy.requireLetter) {
|
||||||
|
* Один латинский символ
|
||||||
|
} @else if (policy.requireDigit) {
|
||||||
|
* Одну цифру
|
||||||
|
}
|
||||||
|
@if (policy.requireSpecialCharacter) {
|
||||||
|
* специальный символ
|
||||||
|
}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@ -105,5 +115,8 @@
|
|||||||
</mat-error>
|
</mat-error>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
<OAuthProviders [canUnlink]="true" [activeProvidersId]="activatedProviders"
|
||||||
|
[message]="'Или можете получить часть данных от сторонних сервисов'"/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {Component} from '@angular/core';
|
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 {NavigationService} from "@service/navigation.service";
|
||||||
import {passwordMatchValidator} from '@service/password-match.validator';
|
import {passwordMatchValidator} from '@service/password-match.validator';
|
||||||
import SetupService from "@api/v1/setup.service";
|
import SetupService from "@api/v1/setup.service";
|
||||||
@ -9,6 +9,10 @@ import {MatInput} from "@angular/material/input";
|
|||||||
import {MatTooltip} from "@angular/material/tooltip";
|
import {MatTooltip} from "@angular/material/tooltip";
|
||||||
import {MatIconButton} from "@angular/material/button";
|
import {MatIconButton} from "@angular/material/button";
|
||||||
import {MatIcon} from "@angular/material/icon";
|
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({
|
@Component({
|
||||||
selector: 'app-create-admin',
|
selector: 'app-create-admin',
|
||||||
@ -20,15 +24,19 @@ import {MatIcon} from "@angular/material/icon";
|
|||||||
MatInput,
|
MatInput,
|
||||||
MatTooltip,
|
MatTooltip,
|
||||||
MatIconButton,
|
MatIconButton,
|
||||||
MatIcon
|
MatIcon,
|
||||||
|
OAuthProviders
|
||||||
],
|
],
|
||||||
templateUrl: './create-admin.component.html'
|
templateUrl: './create-admin.component.html',
|
||||||
|
providers: [AuthApiService]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class CreateAdminComponent {
|
export class CreateAdminComponent {
|
||||||
protected createAdminForm!: FormGroup;
|
protected createAdminForm!: FormGroup;
|
||||||
protected hidePass = true;
|
protected hidePass = true;
|
||||||
protected hideRetypePass = true;
|
protected hideRetypePass = true;
|
||||||
|
protected policy!: PasswordPolicy;
|
||||||
|
protected activatedProviders: OAuthProvider[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
|
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
|
||||||
@ -41,12 +49,6 @@ export class CreateAdminComponent {
|
|||||||
{validators: passwordMatchValidator('password', 'retype')}
|
{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.navigationService.setNextButtonState(false);
|
||||||
this.createAdminForm.valueChanges.subscribe(() => {
|
this.createAdminForm.valueChanges.subscribe(() => {
|
||||||
this.navigationService.setNextButtonState(this.createAdminForm.valid);
|
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) {
|
protected togglePassword(event: MouseEvent) {
|
||||||
|
@ -17,9 +17,9 @@
|
|||||||
</p>
|
</p>
|
||||||
<mat-form-field color="accent">
|
<mat-form-field color="accent">
|
||||||
<mat-label>База данных</mat-label>
|
<mat-label>База данных</mat-label>
|
||||||
<mat-select (valueChange)="onDatabaseChange($event)">
|
<mat-select (valueChange)="onDatabaseChange($event)" [value]="database">
|
||||||
<mat-option value="SetMysql">MySQL</mat-option>
|
<mat-option value="mysql">MySQL</mat-option>
|
||||||
<mat-option value="SetPsql">PostgreSQL</mat-option>
|
<mat-option value="psql">PostgreSQL</mat-option>
|
||||||
<mat-option value="sqlite">Sqlite</mat-option>
|
<mat-option value="sqlite">Sqlite</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@ -57,7 +57,8 @@
|
|||||||
<input matInput
|
<input matInput
|
||||||
matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6'
|
matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6'
|
||||||
required
|
required
|
||||||
formControlName="server">
|
formControlName="server"
|
||||||
|
focusNext="portNextFocus">
|
||||||
|
|
||||||
@if (databaseForm.get('server')?.hasError('required')) {
|
@if (databaseForm.get('server')?.hasError('required')) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
@ -77,7 +78,9 @@
|
|||||||
<input matInput
|
<input matInput
|
||||||
matTooltip="Укажите порт сервера"
|
matTooltip="Укажите порт сервера"
|
||||||
required
|
required
|
||||||
formControlName="port">
|
formControlName="port"
|
||||||
|
id="portNextFocus"
|
||||||
|
focusNext="databaseNextFocus">
|
||||||
|
|
||||||
@if (databaseForm.get('port')?.hasError('required')) {
|
@if (databaseForm.get('port')?.hasError('required')) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
@ -97,7 +100,9 @@
|
|||||||
<input matInput
|
<input matInput
|
||||||
matTooltip="Укажите название базы данных"
|
matTooltip="Укажите название базы данных"
|
||||||
required
|
required
|
||||||
formControlName="database_name">
|
formControlName="database_name"
|
||||||
|
id="databaseNextFocus"
|
||||||
|
focusNext="userNextFocus">
|
||||||
|
|
||||||
@if (databaseForm.get('database_name')?.hasError('required')) {
|
@if (databaseForm.get('database_name')?.hasError('required')) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
@ -117,7 +122,9 @@
|
|||||||
<input matInput
|
<input matInput
|
||||||
matTooltip="Укажите пользователя, который имеет доступ к базе данных"
|
matTooltip="Укажите пользователя, который имеет доступ к базе данных"
|
||||||
required
|
required
|
||||||
formControlName="user">
|
formControlName="user"
|
||||||
|
id="userNextFocus"
|
||||||
|
focusNext="passwordNextFocus">
|
||||||
|
|
||||||
@if (databaseForm.get('user')?.hasError('required')) {
|
@if (databaseForm.get('user')?.hasError('required')) {
|
||||||
<mat-error>
|
<mat-error>
|
||||||
@ -137,7 +144,9 @@
|
|||||||
<input matInput
|
<input matInput
|
||||||
matTooltip="Укажите пароль"
|
matTooltip="Укажите пароль"
|
||||||
formControlName="password"
|
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'"
|
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
|
||||||
[attr.aria-pressed]="hidePass">
|
[attr.aria-pressed]="hidePass">
|
||||||
|
@ -2,14 +2,17 @@ import {Component} from '@angular/core';
|
|||||||
import {NavigationService} from "@service/navigation.service";
|
import {NavigationService} from "@service/navigation.service";
|
||||||
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||||
import SetupService from "@api/v1/setup.service";
|
import SetupService from "@api/v1/setup.service";
|
||||||
import {DatabaseRequest} from "@api/v1/databaseRequest";
|
|
||||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
import {MatSelectModule} from "@angular/material/select";
|
import {MatSelectModule} from "@angular/material/select";
|
||||||
import {MatInput} from "@angular/material/input";
|
import {MatInput} from "@angular/material/input";
|
||||||
import {MatTooltip} from "@angular/material/tooltip";
|
import {MatTooltip} from "@angular/material/tooltip";
|
||||||
import {MatIconButton} from "@angular/material/button";
|
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 {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({
|
@Component({
|
||||||
selector: 'app-database',
|
selector: 'app-database',
|
||||||
@ -22,7 +25,8 @@ import {MatCheckbox} from "@angular/material/checkbox";
|
|||||||
MatTooltip,
|
MatTooltip,
|
||||||
MatIconButton,
|
MatIconButton,
|
||||||
MatIcon,
|
MatIcon,
|
||||||
MatCheckbox
|
MatCheckbox,
|
||||||
|
FocusNextDirective
|
||||||
],
|
],
|
||||||
templateUrl: './database.component.html'
|
templateUrl: './database.component.html'
|
||||||
})
|
})
|
||||||
@ -49,6 +53,42 @@ export class DatabaseComponent {
|
|||||||
this.databaseForm.valueChanges.subscribe(() => {
|
this.databaseForm.valueChanges.subscribe(() => {
|
||||||
this.navigationService.setNextButtonState(this.databaseForm.valid);
|
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) {
|
private createForm(database: string) {
|
||||||
|
@ -33,7 +33,3 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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 {MatSelectModule} from "@angular/material/select";
|
||||||
import {MatInput} from "@angular/material/input";
|
import {MatInput} from "@angular/material/input";
|
||||||
import {MatTooltip} from "@angular/material/tooltip";
|
import {MatTooltip} from "@angular/material/tooltip";
|
||||||
import {MatButton, MatIconButton} from "@angular/material/button";
|
|
||||||
import {MatCheckbox} from "@angular/material/checkbox";
|
import {MatCheckbox} from "@angular/material/checkbox";
|
||||||
|
import {of} from "rxjs";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-logging',
|
selector: 'app-logging',
|
||||||
@ -18,9 +18,7 @@ import {MatCheckbox} from "@angular/material/checkbox";
|
|||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
MatInput,
|
MatInput,
|
||||||
MatTooltip,
|
MatTooltip,
|
||||||
MatIconButton,
|
MatCheckbox
|
||||||
MatCheckbox,
|
|
||||||
MatButton
|
|
||||||
|
|
||||||
],
|
],
|
||||||
templateUrl: './logging.component.html'
|
templateUrl: './logging.component.html'
|
||||||
@ -39,12 +37,11 @@ export class LoggingComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected skipButton() {
|
|
||||||
this.navigationService.skipNavigation.emit(() => this.api.setLogging(null));
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
|
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({
|
this.loggingSettings = this.formBuilder.group({
|
||||||
enabled: [true, Validators.required],
|
enabled: [true, Validators.required],
|
||||||
logPath: [''],
|
logPath: [''],
|
||||||
@ -59,11 +56,23 @@ export class LoggingComponent {
|
|||||||
|
|
||||||
this.navigationService.nextButtonAction = () => {
|
this.navigationService.nextButtonAction = () => {
|
||||||
return this.api.setLogging({
|
return this.api.setLogging({
|
||||||
"enableLogToFile": this.loggingSettings.get('cron')?.value,
|
"enableLogToFile": this.loggingSettings.get('enabled')?.value,
|
||||||
"logFileName": this.loggingSettings.get('logName')?.value,
|
"logFileName": this.loggingSettings.get('logName')?.value,
|
||||||
"logFilePath": this.loggingSettings.get('logPath')?.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 {MatSelectModule} from "@angular/material/select";
|
||||||
import {MatInput} from "@angular/material/input";
|
import {MatInput} from "@angular/material/input";
|
||||||
import {MatTooltip} from "@angular/material/tooltip";
|
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 {MatDatepickerModule} from "@angular/material/datepicker";
|
||||||
|
import {DateOnly} from "@model/dateOnly";
|
||||||
|
import {of} from "rxjs";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-schedule-conf',
|
selector: 'app-schedule-conf',
|
||||||
@ -20,8 +20,6 @@ import {MatDatepickerModule} from "@angular/material/datepicker";
|
|||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
MatInput,
|
MatInput,
|
||||||
MatTooltip,
|
MatTooltip,
|
||||||
MatIconButton,
|
|
||||||
MatIcon,
|
|
||||||
MatDatepickerModule,
|
MatDatepickerModule,
|
||||||
MatNativeDateModule
|
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 class="setup-navigation">
|
||||||
<div>
|
<div>
|
||||||
<button mat-flat-button color="accent"
|
<button mat-flat-button color="accent"
|
||||||
[disabled]="previousButtonDisabled"
|
[hidden]="getIndex <= 2"
|
||||||
[hidden]="previousButtonRoute === ''"
|
(click)="onPreviousClick()">
|
||||||
(click)="onPreviousClick()"
|
|
||||||
[routerLink]="previousButtonRoute">
|
|
||||||
Назад
|
Назад
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (!skipButtonDisabled) {
|
||||||
|
<button mat-flat-button color="accent" (click)="onSkipClick()">Пропустить</button>
|
||||||
|
}
|
||||||
|
|
||||||
@if (loaderActive) {
|
@if (loaderActive) {
|
||||||
<app-data-spinner [scale]="40"/>
|
<app-data-spinner [scale]="40"/>
|
||||||
} @else {
|
} @else {
|
||||||
<button mat-flat-button color="accent"
|
<button mat-flat-button color="accent"
|
||||||
[disabled]="nextButtonDisabled"
|
[disabled]="nextButtonDisabled"
|
||||||
(click)="onNextClick()">
|
(click)="onNextClick()"
|
||||||
|
id="nextButtonFocus">
|
||||||
@if (getIndex === routes.length - 1) {
|
@if (getIndex === routes.length - 1) {
|
||||||
Завершить
|
Завершить
|
||||||
} @else {
|
} @else {
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
import {Component, ViewEncapsulation} from '@angular/core';
|
import {Component, ViewEncapsulation} from '@angular/core';
|
||||||
import {MatSidenavModule} from "@angular/material/sidenav";
|
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 {MatCard} from "@angular/material/card";
|
||||||
import {MatButton} from "@angular/material/button";
|
import {MatButton} from "@angular/material/button";
|
||||||
import {NavigationService} from "@service/navigation.service";
|
import {NavigationService} from "@service/navigation.service";
|
||||||
import {catchError, Observable} from "rxjs";
|
import {catchError, Observable} from "rxjs";
|
||||||
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||||
import SetupService from "@api/v1/setup.service";
|
import SetupService from "@api/v1/setup.service";
|
||||||
|
import {ToastrService} from "ngx-toastr";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-setup',
|
selector: 'app-setup',
|
||||||
@ -16,7 +17,6 @@ import SetupService from "@api/v1/setup.service";
|
|||||||
RouterOutlet,
|
RouterOutlet,
|
||||||
MatCard,
|
MatCard,
|
||||||
MatButton,
|
MatButton,
|
||||||
RouterLink,
|
|
||||||
DataSpinnerComponent
|
DataSpinnerComponent
|
||||||
],
|
],
|
||||||
templateUrl: './setup.component.html',
|
templateUrl: './setup.component.html',
|
||||||
@ -26,64 +26,71 @@ import SetupService from "@api/v1/setup.service";
|
|||||||
})
|
})
|
||||||
|
|
||||||
export class SetupComponent {
|
export class SetupComponent {
|
||||||
protected previousButtonDisabled: boolean = false;
|
|
||||||
protected previousButtonRoute: string = '';
|
|
||||||
|
|
||||||
protected nextButtonDisabled: boolean = false;
|
protected nextButtonDisabled: boolean = false;
|
||||||
protected nextButtonRoute!: string;
|
protected skipButtonDisabled: boolean = false;
|
||||||
|
|
||||||
protected loaderActive: 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;
|
private index: number = 1;
|
||||||
|
|
||||||
protected get getIndex() {
|
protected get getIndex() {
|
||||||
return this.index;
|
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 => {
|
api.isConfigured().subscribe(x => {
|
||||||
if (x)
|
if (x) this.router.navigate(['/']).then();
|
||||||
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.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.navigationService.nextButtonState$.subscribe(state => {
|
||||||
this.nextButtonDisabled = !state;
|
this.nextButtonDisabled = !state;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.navigationService.skipNavigation.subscribe(action => {
|
this.navigationService.skipButtonState$.subscribe(state => {
|
||||||
this.executeAction(action);
|
this.skipButtonDisabled = !state;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setRoutes() {
|
|
||||||
this.previousButtonRoute = this.routes[this.index - 1];
|
|
||||||
this.nextButtonRoute = this.routes[this.index + 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
private executeAction(action: () => Observable<boolean>) {
|
private executeAction(action: () => Observable<boolean>) {
|
||||||
this.loaderActive = true;
|
this.loaderActive = true;
|
||||||
action().pipe(
|
action()
|
||||||
|
.pipe(
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
this.nextButtonDisabled = true;
|
this.nextButtonDisabled = true;
|
||||||
this.loaderActive = false;
|
this.loaderActive = false;
|
||||||
throw error;
|
throw error;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.subscribe(x => {
|
.subscribe(success => {
|
||||||
this.nextButtonDisabled = x;
|
if (success) {
|
||||||
this.loaderActive = !x;
|
this.moveToNextPage();
|
||||||
if (x) {
|
} else {
|
||||||
if (this.index < this.routes.length - 1) {
|
this.notify.error('Некорректно введены данные');
|
||||||
this.router.navigate(['setup/', this.nextButtonRoute]).then();
|
this.nextButtonDisabled = true;
|
||||||
this.index++;
|
}
|
||||||
this.setRoutes();
|
|
||||||
} else
|
this.loaderActive = false;
|
||||||
this.router.navigate(['/']).then();
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onSkipClick() {
|
||||||
|
this.navigationService.skipButtonAction().subscribe(success => {
|
||||||
|
if (success) {
|
||||||
|
this.moveToNextPage();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -93,9 +100,29 @@ export class SetupComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected onPreviousClick() {
|
protected onPreviousClick() {
|
||||||
if (this.index - 1 > 0) {
|
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.index--;
|
||||||
this.setRoutes();
|
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>
|
<h4 style="margin-bottom: -5px;">Что дальше?</h4>
|
||||||
<p class="mat-body-2 secondary">
|
<p class="mat-body-2 secondary">
|
||||||
Теперь, когда основные настройки завершены, вы можете начать использовать систему с уверенностью, что все работает правильно.
|
Теперь, когда основные настройки завершены, вы можете начать использовать систему с уверенностью, что все работает
|
||||||
|
правильно.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h4 style="margin-bottom: -5px;">Изменение настроек</h4>
|
<h4 style="margin-bottom: -5px;">Изменение настроек</h4>
|
||||||
@ -17,6 +18,173 @@
|
|||||||
Для изменения настроек перейдите в раздел настроек программы, который доступен в меню пользователя.
|
Для изменения настроек перейдите в раздел настроек программы, который доступен в меню пользователя.
|
||||||
</p>
|
</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 class="mat-h3" style="color: red;font-weight: lighter;">
|
||||||
Для того, чтобы настройки были применены нажмите кнопку "Завершить" и перезагрузите приложение
|
Для того, чтобы настройки были применены нажмите кнопку "Завершить" и перезагрузите приложение
|
||||||
</p>
|
</p>
|
||||||
|
@ -1,26 +1,60 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {MatButton} from "@angular/material/button";
|
|
||||||
import {NavigationService} from "@service/navigation.service";
|
import {NavigationService} from "@service/navigation.service";
|
||||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
import {MatInput} from "@angular/material/input";
|
|
||||||
import SetupService from "@api/v1/setup.service";
|
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({
|
@Component({
|
||||||
selector: 'app-summary',
|
selector: 'app-summary',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [MatFormFieldModule, MatExpansionModule],
|
||||||
MatButton,
|
|
||||||
MatFormFieldModule,
|
|
||||||
MatInput
|
|
||||||
],
|
|
||||||
templateUrl: './summary.component.html'
|
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) {
|
constructor(private navigationService: NavigationService, private api: SetupService) {
|
||||||
this.navigationService.nextButtonAction = () => {
|
this.navigationService.nextButtonAction = () => {
|
||||||
return this.api.submit();
|
return this.api.submit();
|
||||||
};
|
};
|
||||||
this.navigationService.setNextButtonState(true);
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,22 +1,19 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {MatButton} from "@angular/material/button";
|
|
||||||
import {NavigationService} from "@service/navigation.service";
|
import {NavigationService} from "@service/navigation.service";
|
||||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
import {MatInput} from "@angular/material/input";
|
import {MatInput} from "@angular/material/input";
|
||||||
import {AsyncPipe} from "@angular/common";
|
|
||||||
import {FormControl, ReactiveFormsModule, Validators} from "@angular/forms";
|
import {FormControl, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||||
import SetupService from "@api/v1/setup.service";
|
import SetupService from "@api/v1/setup.service";
|
||||||
import {environment} from "@environment";
|
import {environment} from "@environment";
|
||||||
import {AvailableVersion} from "@api/api.service";
|
import {AvailableVersion} from "@api/api.service";
|
||||||
|
import {of} from "rxjs";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-welcome',
|
selector: 'app-welcome',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
MatButton,
|
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInput,
|
MatInput,
|
||||||
AsyncPipe,
|
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule
|
||||||
],
|
],
|
||||||
templateUrl: './welcome.component.html'
|
templateUrl: './welcome.component.html'
|
||||||
@ -33,13 +30,21 @@ export class WelcomeComponent {
|
|||||||
|
|
||||||
constructor(private navigationService: NavigationService, private api: SetupService) {
|
constructor(private navigationService: NavigationService, private api: SetupService) {
|
||||||
this.apiToGetToken += AvailableVersion[this.api.version];
|
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.tokenControl.valueChanges.subscribe(() => {
|
||||||
this.navigationService.setNextButtonState(this.tokenControl.valid);
|
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 {Injectable} from '@angular/core';
|
||||||
import {BehaviorSubject, Observable} from "rxjs";
|
import {BehaviorSubject, Observable, Subject} from "rxjs";
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class NavigationService {
|
export class NavigationService {
|
||||||
private nextButtonState = new BehaviorSubject<boolean>(false);
|
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();
|
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) {
|
setNextButtonState(state: boolean) {
|
||||||
this.nextButtonState.next(state);
|
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