diff --git a/src/api/v1/securityService.ts b/src/api/v1/securityService.ts
new file mode 100644
index 0000000..ef1a2f6
--- /dev/null
+++ b/src/api/v1/securityService.ts
@@ -0,0 +1,17 @@
+import {Injectable} from "@angular/core";
+import ApiService, {AvailableVersion} from "@api/api.service";
+
+@Injectable()
+export default class SecurityService extends ApiService {
+ public readonly basePath = 'Security/';
+ public readonly version = AvailableVersion.v1;
+
+ public generateTotpQrCode(totpKey: string, username: string) {
+ let request = this.createRequestBuilder()
+ .setEndpoint('GenerateTotpQrCode')
+ .setQueryParams({totpKey: totpKey, label: username})
+ .build;
+
+ return this.combinedUrl(request);
+ }
+}
diff --git a/src/api/v1/setup.service.ts b/src/api/v1/setup.service.ts
index d09c1ba..1a314d9 100644
--- a/src/api/v1/setup.service.ts
+++ b/src/api/v1/setup.service.ts
@@ -1,12 +1,17 @@
import {Injectable} from "@angular/core";
import ApiService, {AvailableVersion} from "@api/api.service";
-import {DatabaseRequest} from "@api/v1/databaseRequest";
-import {CacheRequest} from "@api/v1/cacheRequest";
+import {catchError, of, switchMap} from "rxjs";
+import {DatabaseResponse} from "@api/v1/configuration/databaseResponse";
+import {DatabaseRequest} from "@api/v1/configuration/databaseRequest";
+import {CacheRequest} from "@api/v1/configuration/cacheRequest";
import {CreateUserRequest} from "@api/v1/createUserRequest";
-import {LoggingRequest} from "@api/v1/loggingRequest";
-import {EmailRequest} from "@api/v1/emailRequest";
-import {ScheduleConfigurationRequest} from "@api/v1/scheduleConfigurationRequest";
-import {DateOnly} from "@model/DateOnly";
+import {LoggingRequest} from "@api/v1/configuration/loggingRequest";
+import {ScheduleConfigurationRequest} from "@api/v1/configuration/scheduleConfigurationRequest";
+import {EmailRequest} from "@api/v1/configuration/emailRequest";
+import {DateOnly} from "@model/dateOnly";
+import {CacheResponse} from "@api/v1/configuration/cacheResponse";
+import {PasswordPolicy} from "@model/passwordPolicy";
+import {UserResponse} from "@api/v1/userResponse";
@Injectable()
export default class SetupService extends ApiService {
@@ -23,6 +28,17 @@ export default class SetupService extends ApiService {
return this.get(request);
}
+ public isConfiguredToken() {
+ let request = this.createRequestBuilder()
+ .setEndpoint('IsConfiguredToken')
+ .setWithCredentials()
+ .build;
+
+ return this.get(request).pipe(catchError(_ => {
+ return of(false);
+ }));
+ }
+
public setPsql(data: DatabaseRequest) {
let request = this.createRequestBuilder()
.setEndpoint('SetPsql')
@@ -53,6 +69,15 @@ export default class SetupService extends ApiService {
return this.post(request);
}
+ public databaseConfiguration() {
+ let request = this.createRequestBuilder()
+ .setEndpoint('DatabaseConfiguration')
+ .setWithCredentials()
+ .build;
+
+ return this.get(request);
+ }
+
public setRedis(data: CacheRequest) {
let request = this.createRequestBuilder()
.setEndpoint('SetRedis')
@@ -72,6 +97,34 @@ export default class SetupService extends ApiService {
return this.post(request);
}
+ public cacheConfiguration() {
+ let request = this.createRequestBuilder()
+ .setEndpoint('CacheConfiguration')
+ .setWithCredentials()
+ .build;
+
+ return this.get(request);
+ }
+
+ public setPasswordPolicy(data: PasswordPolicy | null) {
+ let request = this.createRequestBuilder()
+ .setEndpoint('SetPasswordPolicy')
+ .setData(data)
+ .setWithCredentials()
+ .build;
+
+ return this.post(request);
+ }
+
+ public passwordPolicyConfiguration() {
+ let request = this.createRequestBuilder()
+ .setEndpoint('PasswordPolicyConfiguration')
+ .setWithCredentials()
+ .build;
+
+ return this.get(request);
+ }
+
public createAdmin(data: CreateUserRequest) {
let request = this.createRequestBuilder()
.setEndpoint('CreateAdmin')
@@ -82,6 +135,22 @@ export default class SetupService extends ApiService {
return this.post(request);
}
+ public adminConfiguration() {
+ let request = this.createRequestBuilder()
+ .setEndpoint('UpdateAdminConfiguration')
+ .setWithCredentials()
+ .build;
+
+ return this.get(request).pipe(switchMap(_ => {
+ request = this.createRequestBuilder()
+ .setEndpoint('AdminConfiguration')
+ .setWithCredentials()
+ .build;
+
+ return this.get(request);
+ }));
+ }
+
public setLogging(data: LoggingRequest | null = null) {
let request = this.createRequestBuilder()
.setEndpoint('SetLogging')
@@ -92,6 +161,15 @@ export default class SetupService extends ApiService {
return this.post(request);
}
+ public loggingConfiguration() {
+ let request = this.createRequestBuilder()
+ .setEndpoint('LoggingConfiguration')
+ .setWithCredentials()
+ .build;
+
+ return this.get(request);
+ }
+
public setEmail(data: EmailRequest | null = null) {
let request = this.createRequestBuilder()
.setEndpoint('SetEmail')
@@ -114,6 +192,34 @@ export default class SetupService extends ApiService {
return this.post(request);
}
+ public generateTotpKey() {
+ let request = this.createRequestBuilder()
+ .setEndpoint('GenerateTotpKey')
+ .setWithCredentials()
+ .build;
+
+ return this.get(request);
+ }
+
+ public verifyTotp(code: string) {
+ let request = this.createRequestBuilder()
+ .setEndpoint('VerifyTotp')
+ .setWithCredentials()
+ .setQueryParams({code: code})
+ .build;
+
+ return this.get(request);
+ }
+
+ public scheduleConfiguration() {
+ let request = this.createRequestBuilder()
+ .setEndpoint('ScheduleConfiguration')
+ .setWithCredentials()
+ .build;
+
+ return this.get(request);
+ }
+
public submit() {
let request = this.createRequestBuilder()
.setEndpoint('Submit')
diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts
index c9106cb..7e601f3 100644
--- a/src/app/app.routes.ts
+++ b/src/app/app.routes.ts
@@ -9,6 +9,8 @@ import {SetupComponent} from "@page/setup/setup.component";
import {CreateAdminComponent} from "@page/setup/create-admin/create-admin.component";
import {SummaryComponent} from "@page/setup/summary/summary.component";
import {LoginComponent} from "@page/login/login.component";
+import {PasswordPolicyComponent} from "@page/setup/password-policy/password-policy.component";
+import {TwoFactorComponent} from "@page/setup/two-factor/two-factor.component";
export const routes: Routes = [
{path: '', title: 'Расписание', pathMatch: 'full', component: ScheduleComponent},
@@ -21,6 +23,8 @@ export const routes: Routes = [
{path: 'schedule', component: SetupScheduleComponent},
{path: 'logging', component: LoggingComponent},
{path: 'summary', component: SummaryComponent},
+ {path: 'password-policy', component: PasswordPolicyComponent},
+ {path: 'two-factor', component: TwoFactorComponent},
{path: '', redirectTo: 'welcome', pathMatch: 'full'}
]
},
diff --git a/src/assets/icons/google.svg b/src/assets/icons/google.svg
new file mode 100644
index 0000000..90d50c3
--- /dev/null
+++ b/src/assets/icons/google.svg
@@ -0,0 +1,47 @@
+
+
diff --git a/src/assets/icons/mailru.svg b/src/assets/icons/mailru.svg
new file mode 100644
index 0000000..c05d921
--- /dev/null
+++ b/src/assets/icons/mailru.svg
@@ -0,0 +1,22 @@
+
+
diff --git a/src/assets/icons/yandex.svg b/src/assets/icons/yandex.svg
new file mode 100644
index 0000000..7a65280
--- /dev/null
+++ b/src/assets/icons/yandex.svg
@@ -0,0 +1,17 @@
+
+
diff --git a/src/pages/schedule/schedule.component.html b/src/pages/schedule/schedule.component.html
index dc38806..2ab65f9 100644
--- a/src/pages/schedule/schedule.component.html
+++ b/src/pages/schedule/schedule.component.html
@@ -9,13 +9,16 @@
-
+
- Показать недели в дисциплине
- @if(excelImportLoader) {
-
+ Показать недели в
+ дисциплине
+
+ @if (excelImportLoader) {
+
} @else {
}
diff --git a/src/pages/schedule/schedule.component.ts b/src/pages/schedule/schedule.component.ts
index e7003fc..8d25875 100644
--- a/src/pages/schedule/schedule.component.ts
+++ b/src/pages/schedule/schedule.component.ts
@@ -5,12 +5,12 @@ import {TabsComponent, TabsSelect} from "@component/schedule/tabs/tabs.component
import {catchError, Observable} from "rxjs";
import {ScheduleService} from "@api/v1/schedule.service";
import {ScheduleResponse} from "@api/v1/scheduleResponse";
-import {PeriodTimes} from "@model/pairPeriodTime";
+import {PairPeriodTime} from "@model/pairPeriodTime";
import {ActivatedRoute} from "@angular/router";
import {TabStorageService} from "@service/tab-storage.service";
import {MatDialog} from "@angular/material/dialog";
import {ConfirmDialogComponent} from "@page/schedule/confirm-dialog.component";
-import {AuthRoles} from "@model/AuthRoles";
+import {AuthRoles} from "@model/authRoles";
import {ImportService} from "@api/v1/import.service";
import {ScheduleRequest} from "@api/v1/scheduleRequest";
import {ToastrService} from "ngx-toastr";
@@ -50,7 +50,7 @@ export class ScheduleComponent {
protected data: ScheduleResponse[] = [];
protected startTerm: Date;
protected isLoadTable: boolean = false;
- protected pairPeriods: PeriodTimes = {};
+ protected pairPeriods: PairPeriodTime | null = null;
protected disciplineWithWeeks: boolean = false;
protected excelImportLoader: boolean = false;
diff --git a/src/pages/setup/cache/cache.component.html b/src/pages/setup/cache/cache.component.html
index 6723dd1..65747c2 100644
--- a/src/pages/setup/cache/cache.component.html
+++ b/src/pages/setup/cache/cache.component.html
@@ -15,7 +15,7 @@
База данных
-
+
Redis
Memcached
@@ -29,7 +29,8 @@
+ formControlName="server"
+ focusNext="serverNextFocus">
@if (databaseForm.get('server')?.hasError('required')) {
@@ -49,7 +50,9 @@
+ formControlName="port"
+ id="serverNextFocus"
+ focusNext="passwordNextFocus">
@if (databaseForm.get('port')?.hasError('required')) {
@@ -69,7 +72,9 @@
+ [type]="hidePass ? 'password' : 'text'"
+ id="passwordNextFocus"
+ focusNext="nextButtonFocus">
@@ -74,13 +74,23 @@
@if (createAdminForm.get('password')?.hasError('minlength')) {
- Пароль должен быть не менее 8 символов
+ Пароль должен быть не менее {{ policy.minimumLength }} символов
}
@if (createAdminForm.get('password')?.hasError('pattern')) {
- Пароль должен содержать хотя бы один латинский символ верхнего регистра и специальный символ (!@#$%^&*)
+ Пароль должен содержать:
+ @if (policy.requireLettersDifferentCase) {
+ * Латинские символы разных регистров
+ } @else if (policy.requireLetter) {
+ * Один латинский символ
+ } @else if (policy.requireDigit) {
+ * Одну цифру
+ }
+ @if (policy.requireSpecialCharacter) {
+ * специальный символ
+ }
}
@@ -105,5 +115,8 @@
}
+
+
diff --git a/src/pages/setup/create-admin/create-admin.component.ts b/src/pages/setup/create-admin/create-admin.component.ts
index 040d019..4dddd93 100644
--- a/src/pages/setup/create-admin/create-admin.component.ts
+++ b/src/pages/setup/create-admin/create-admin.component.ts
@@ -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) {
diff --git a/src/pages/setup/database/database.component.html b/src/pages/setup/database/database.component.html
index ebc306a..3e7c8a5 100644
--- a/src/pages/setup/database/database.component.html
+++ b/src/pages/setup/database/database.component.html
@@ -17,9 +17,9 @@
База данных
-
- MySQL
- PostgreSQL
+
+ MySQL
+ PostgreSQL
Sqlite
@@ -57,7 +57,8 @@
+ formControlName="server"
+ focusNext="portNextFocus">
@if (databaseForm.get('server')?.hasError('required')) {
@@ -77,7 +78,9 @@
+ formControlName="port"
+ id="portNextFocus"
+ focusNext="databaseNextFocus">
@if (databaseForm.get('port')?.hasError('required')) {
@@ -97,7 +100,9 @@
+ formControlName="database_name"
+ id="databaseNextFocus"
+ focusNext="userNextFocus">
@if (databaseForm.get('database_name')?.hasError('required')) {
@@ -117,7 +122,9 @@
+ formControlName="user"
+ id="userNextFocus"
+ focusNext="passwordNextFocus">
@if (databaseForm.get('user')?.hasError('required')) {
@@ -137,7 +144,9 @@
+ [type]="hidePass ? 'password' : 'text'"
+ id="passwordNextFocus"
+ focusNext="nextButtonFocus">