Compare commits
7 Commits
99958a2383
...
1ffbfad37a
Author | SHA1 | Date | |
---|---|---|---|
1ffbfad37a | |||
c04c457211 | |||
fba28b6bbe | |||
86e6f59567 | |||
a2d4151cc3 | |||
3af8c43cd9 | |||
21f89132ff |
@ -1,6 +1,6 @@
|
||||
# MIREA schedule by Winsomnia
|
||||
|
||||
[](https://github.com/angular/angular-cli)
|
||||
[](https://github.com/angular/angular-cli)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
This project provides a Web interface for working with the MIREA schedule.
|
||||
|
5994
package-lock.json
generated
5994
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
41
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.0.0-b9",
|
||||
"version": "1.0.0-b10",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
@ -10,35 +10,34 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^18.2.9",
|
||||
"@angular/cdk": "~18.2.10",
|
||||
"@angular/cdk-experimental": "^18.2.10",
|
||||
"@angular/common": "^18.2.9",
|
||||
"@angular/compiler": "^18.2.9",
|
||||
"@angular/core": "^18.2.9",
|
||||
"@angular/forms": "^18.2.9",
|
||||
"@angular/material": "~18.2.10",
|
||||
"@angular/platform-browser": "^18.2.9",
|
||||
"@angular/platform-browser-dynamic": "^18.2.9",
|
||||
"@angular/router": "^18.2.9",
|
||||
"@dhutaryan/ngx-mat-timepicker": "^18.0.2",
|
||||
"@angular/animations": "^19.0.4",
|
||||
"@angular/cdk": "~19.0.3",
|
||||
"@angular/cdk-experimental": "^19.0.3",
|
||||
"@angular/common": "^19.0.4",
|
||||
"@angular/compiler": "^19.0.4",
|
||||
"@angular/core": "^19.0.4",
|
||||
"@angular/forms": "^19.0.4",
|
||||
"@angular/material": "~19.0.3",
|
||||
"@angular/platform-browser": "^19.0.4",
|
||||
"@angular/platform-browser-dynamic": "^19.0.4",
|
||||
"@angular/router": "^19.0.4",
|
||||
"@progress/kendo-date-math": "^1.5.14",
|
||||
"ngx-toastr": "^19.0.0",
|
||||
"rxjs": "~7.8.1",
|
||||
"tslib": "^2.8.0",
|
||||
"zone.js": "^0.14.10"
|
||||
"tslib": "^2.8.1",
|
||||
"zone.js": "^0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^18.2.10",
|
||||
"@angular/cli": "^18.2.10",
|
||||
"@angular/compiler-cli": "^18.2.9",
|
||||
"@types/jasmine": "~5.1.4",
|
||||
"jasmine-core": "~5.4.0",
|
||||
"@angular-devkit/build-angular": "^19.0.5",
|
||||
"@angular/cli": "^19.0.5",
|
||||
"@angular/compiler-cli": "^19.0.4",
|
||||
"@types/jasmine": "~5.1.5",
|
||||
"jasmine-core": "~5.5.0",
|
||||
"karma": "~6.4.4",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.1",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "^5.5.4"
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import {Router} from "@angular/router";
|
||||
import {Injectable} from "@angular/core";
|
||||
import {RequestBuilder, RequestData} from "@api/RequestBuilder";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {AuthRoles} from "@model/AuthRoles";
|
||||
import {AuthRoles} from "@model/authRoles";
|
||||
|
||||
export enum AvailableVersion {
|
||||
v1
|
||||
@ -69,7 +69,7 @@ export default abstract class ApiService {
|
||||
}).pipe(
|
||||
catchError(error => {
|
||||
if (!secondTry && error.status === 401)
|
||||
return this.handle401Error().pipe(
|
||||
return this.handle401Error(error).pipe(
|
||||
switchMap(() => this.sendHttpRequest<Type>(method, request, true))
|
||||
);
|
||||
else {
|
||||
@ -88,7 +88,7 @@ export default abstract class ApiService {
|
||||
});
|
||||
}
|
||||
|
||||
private handle401Error(): Observable<any> {
|
||||
private handle401Error(error: any): Observable<any> {
|
||||
if (ApiService.isRefreshingToken.value)
|
||||
return ApiService.refreshTokenSubject.asObservable();
|
||||
|
||||
@ -103,7 +103,7 @@ export default abstract class ApiService {
|
||||
ApiService.isRefreshingToken.next(false);
|
||||
ApiService.refreshTokenSubject.error(err);
|
||||
ApiService.refreshTokenSubject = new ReplaySubject(1);
|
||||
throw err;
|
||||
throw error;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,14 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||
import {LoginRequest} from "@api/v1/loginRequest";
|
||||
import {catchError, of} from "rxjs";
|
||||
import {AuthRoles} from "@model/AuthRoles";
|
||||
import {catchError, map, Observable, of} from "rxjs";
|
||||
import {AuthRoles} from "@model/authRoles";
|
||||
import {AvailableOAuthProvidersResponse} from "@api/v1/availableProvidersResponse";
|
||||
import {OAuthProvider} from "@model/oAuthProvider";
|
||||
|
||||
export interface OAuthProviderData extends AvailableOAuthProvidersResponse {
|
||||
icon: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export default class AuthApiService extends ApiService {
|
||||
@ -19,7 +25,7 @@ export default class AuthApiService extends ApiService {
|
||||
return this.post<AuthRoles>(request);
|
||||
}
|
||||
|
||||
public reLogin(){
|
||||
public reLogin() {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('ReLogin')
|
||||
.setWithCredentials()
|
||||
@ -51,4 +57,32 @@ export default class AuthApiService extends ApiService {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private getProviderIcon(provider: OAuthProvider): string {
|
||||
switch (provider) {
|
||||
case OAuthProvider.Google:
|
||||
return 'assets/icons/google.svg';
|
||||
case OAuthProvider.Yandex:
|
||||
return 'assets/icons/yandex.svg';
|
||||
case OAuthProvider.MailRu:
|
||||
return 'assets/icons/mailru.svg';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public availableProviders(): Observable<OAuthProviderData[]> {
|
||||
let request = this.createRequestBuilder()
|
||||
.setEndpoint('AvailableProviders')
|
||||
.setWithCredentials()
|
||||
.build;
|
||||
|
||||
return this.get<Array<AvailableOAuthProvidersResponse>>(request).pipe(
|
||||
map(data => {
|
||||
return data.map((provider) => ({
|
||||
...provider,
|
||||
icon: this.getProviderIcon(provider.provider),
|
||||
}) as OAuthProviderData);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import {Injectable} from "@angular/core";
|
||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||
import {DateOnly} from "@model/DateOnly";
|
||||
import {PeriodTimes} from "@model/pairPeriodTime";
|
||||
import {DateOnly} from "@model/dateOnly";
|
||||
import {PairPeriodTime} from "@model/pairPeriodTime";
|
||||
import {ScheduleRequest} from "@api/v1/scheduleRequest";
|
||||
import {ScheduleResponse} from "@api/v1/scheduleResponse";
|
||||
import {map} from "rxjs";
|
||||
@ -16,7 +16,7 @@ export class ScheduleService extends ApiService {
|
||||
}
|
||||
|
||||
public pairPeriod() {
|
||||
return this.get<PeriodTimes>('PairPeriod');
|
||||
return this.get<PairPeriodTime>('PairPeriod');
|
||||
}
|
||||
|
||||
public postSchedule(data: ScheduleRequest) {
|
||||
|
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 |
77
src/components/OAuthProviders/OAuthProviders.css
Normal file
77
src/components/OAuthProviders/OAuthProviders.css
Normal file
@ -0,0 +1,77 @@
|
||||
.provider-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.provider-container a {
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
transition: transform 0.3s ease-in-out, filter 0.3s ease;
|
||||
border-radius: 50%;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.provider-icon {
|
||||
object-fit: contain;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease, filter 0.3s ease;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.provider-container a:hover .provider-icon {
|
||||
transform: scale(1.1); /* Slight zoom-in effect on hover */
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); /* Adding shadow for effect */
|
||||
}
|
||||
|
||||
.provider-container .provider-item.disabled {
|
||||
pointer-events: none; /* Disables click */
|
||||
opacity: 0.5; /* Dims the icon to indicate it is disabled */
|
||||
}
|
||||
|
||||
.provider-container .provider-item.disabled .provider-icon {
|
||||
filter: grayscale(100%) contrast(100%); /* Desaturates image if disabled */
|
||||
}
|
||||
|
||||
.provider-item {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.provider-item.provider-unlink {
|
||||
filter: grayscale(50%) contrast(60%);
|
||||
transition: filter 0.3s ease;
|
||||
}
|
||||
|
||||
.provider-item.provider-unlink:hover {
|
||||
filter: grayscale(0%) contrast(100%);
|
||||
}
|
||||
|
||||
.provider-item.provider-unlink::after {
|
||||
content: '×';
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
left: 90%;
|
||||
transform: translate(-100%, 0%) scale(0.0);
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
background-color: red;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
|
||||
.provider-item.provider-unlink:hover::after {
|
||||
transform: translate(-50%, -50%) scale(1.0);
|
||||
}
|
16
src/components/OAuthProviders/OAuthProviders.html
Normal file
16
src/components/OAuthProviders/OAuthProviders.html
Normal file
@ -0,0 +1,16 @@
|
||||
@if (providers.length !== 0) {
|
||||
<hr/>
|
||||
<div>
|
||||
<p class="mat-body-2 secondary">{{ message }}</p>
|
||||
|
||||
<div class="provider-container">
|
||||
@for (provider of providers; track $index) {
|
||||
<a class="provider-item" (click)="provider.disabled ? confirmDelete(provider) : openOAuth(provider)"
|
||||
[class.disabled]="!canUnlink && provider.disabled" [class.provider-unlink]="canUnlink && provider.disabled">
|
||||
<img [alt]="provider.providerName" [src]="provider.icon"
|
||||
class="provider-icon" draggable="false"/>
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
126
src/components/OAuthProviders/OAuthProviders.ts
Normal file
126
src/components/OAuthProviders/OAuthProviders.ts
Normal file
@ -0,0 +1,126 @@
|
||||
import {Component, Inject, Input, OnInit} from '@angular/core';
|
||||
import AuthApiService, {OAuthProviderData} from "@api/v1/authApiService";
|
||||
import {OAuthProvider} from "@model/oAuthProvider";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {
|
||||
MAT_DIALOG_DATA, MatDialog,
|
||||
MatDialogActions,
|
||||
MatDialogContent,
|
||||
MatDialogRef,
|
||||
MatDialogTitle
|
||||
} from "@angular/material/dialog";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
|
||||
interface AvailableOAuthProviders extends OAuthProviderData {
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-delete-confirm-dialog',
|
||||
template: `
|
||||
<h1 mat-dialog-title>Удалить провайдера?</h1>
|
||||
<mat-dialog-content>
|
||||
<p>Вы уверены, что хотите удалить провайдера {{ data.provider.name }}?</p>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions style="display: flex; justify-content: flex-end;">
|
||||
<button mat-button (click)="onCancel()">Отмена</button>
|
||||
<button mat-raised-button color="warn" (click)="onConfirm()">Удалить</button>
|
||||
</mat-dialog-actions>
|
||||
`,
|
||||
imports: [
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatButton
|
||||
]
|
||||
})
|
||||
export class DeleteConfirmDialog {
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<DeleteConfirmDialog>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
}
|
||||
|
||||
onConfirm(): void {
|
||||
this.dialogRef.close(true);
|
||||
}
|
||||
|
||||
onCancel(): void {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'OAuthProviders',
|
||||
imports: [],
|
||||
templateUrl: './OAuthProviders.html',
|
||||
styleUrl: './OAuthProviders.css',
|
||||
providers: [AuthApiService]
|
||||
})
|
||||
export class OAuthProviders implements OnInit {
|
||||
protected providers: AvailableOAuthProviders[] = [];
|
||||
protected _activeProvidersId: OAuthProvider[] = [];
|
||||
|
||||
@Input() message: string = 'Вы можете войти в аккаунт через';
|
||||
@Input() activeProviders: string[] = [];
|
||||
|
||||
@Input() set activeProvidersId(data: OAuthProvider[]) {
|
||||
this._activeProvidersId = data;
|
||||
this.updateDisabledProviders();
|
||||
}
|
||||
|
||||
@Input() canUnlink: boolean = false;
|
||||
|
||||
constructor(authApi: AuthApiService, private notify: ToastrService, private dialog: MatDialog) {
|
||||
authApi.availableProviders().subscribe(providers => this.updateDisabledProviders(providers));
|
||||
}
|
||||
|
||||
private updateDisabledProviders(data: OAuthProviderData[] | null = null) {
|
||||
this.providers = (data ?? this.providers).map(provider => {
|
||||
return {
|
||||
...provider,
|
||||
disabled: this._activeProvidersId.includes(provider.provider) || this.activeProviders.includes(provider.providerName)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event.data && event.data.success === false) {
|
||||
console.error(event.data.message);
|
||||
this.notify.error(event.data.message, 'OAuth ошибка');
|
||||
} else {
|
||||
this.activeProvidersId.push(event.data.provider);
|
||||
this.updateDisabledProviders();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected openOAuth(provider: AvailableOAuthProviders) {
|
||||
console.log(provider.redirect);
|
||||
const oauthWindow = window.open(
|
||||
provider.redirect,
|
||||
'_blank',
|
||||
);
|
||||
|
||||
if (!oauthWindow) {
|
||||
this.notify.error('Не удалось открыть OAuth окно');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected confirmDelete(provider: AvailableOAuthProviders) {
|
||||
const dialogRef = this.dialog.open(DeleteConfirmDialog, {data: {provider}});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.deleteProvider(provider);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected deleteProvider(provider: AvailableOAuthProviders) {
|
||||
// todo: remove provider
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
<mat-progress-spinner [color]="color" mode="indeterminate" [diameter]="scale" />
|
||||
<mat-progress-spinner [color]="color" mode="indeterminate" [diameter]="scale"/>
|
||||
|
@ -1,13 +1,11 @@
|
||||
import {Component, Input} from '@angular/core';
|
||||
import {MatProgressSpinner} from "@angular/material/progress-spinner";
|
||||
import {NgStyle} from "@angular/common";
|
||||
|
||||
@Component({
|
||||
selector: 'app-data-spinner',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatProgressSpinner,
|
||||
NgStyle
|
||||
],
|
||||
templateUrl: './data-spinner.component.html'
|
||||
})
|
||||
|
@ -22,8 +22,8 @@
|
||||
<hr/>
|
||||
<div class="app-footer-copyright">
|
||||
<span>Powered by <a href="https://winsomnia.net">Winsomnia</a> ©{{ currentYear }}.</span>
|
||||
<a href="https://opensource.org/license/mit/">Code licensed under an MIT-style License.</a>
|
||||
<span>Current Version: {{ version }}</span>
|
||||
<a href="https://opensource.org/license/mit/">Code licensed under an MIT-style License.</a>
|
||||
<span>Current Version: {{ version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -1,16 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {Component} from '@angular/core';
|
||||
import {MatToolbar} from "@angular/material/toolbar";
|
||||
import {MatAnchor, MatButton} from "@angular/material/button";
|
||||
import {HasRoleDirective} from "@/directives/has-role.directive";
|
||||
import {AuthRoles} from "@model/AuthRoles";
|
||||
import {AuthRoles} from "@model/authRoles";
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatToolbar,
|
||||
MatButton,
|
||||
MatAnchor,
|
||||
HasRoleDirective
|
||||
],
|
||||
templateUrl: './header.component.html',
|
||||
|
@ -1,14 +1,13 @@
|
||||
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import {MatButton, MatFabButton} from "@angular/material/button";
|
||||
import {MatFabButton} from "@angular/material/button";
|
||||
|
||||
@Component({
|
||||
selector: 'app-loading-indicator',
|
||||
standalone: true,
|
||||
imports: [
|
||||
DataSpinnerComponent,
|
||||
MatButton,
|
||||
MatIcon,
|
||||
MatFabButton
|
||||
],
|
||||
|
@ -3,7 +3,6 @@ import {MatTableDataSource, MatTableModule} from "@angular/material/table";
|
||||
import {MatIcon} from "@angular/material/icon";
|
||||
import {DatePipe} from "@angular/common";
|
||||
import {addDays} from "@progress/kendo-date-math";
|
||||
import {MatDivider} from "@angular/material/divider";
|
||||
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||
import {ScheduleResponse} from "@api/v1/scheduleResponse";
|
||||
|
||||
@ -23,7 +22,6 @@ interface Dictionary {
|
||||
MatTableModule,
|
||||
MatIcon,
|
||||
DatePipe,
|
||||
MatDivider,
|
||||
DataSpinnerComponent
|
||||
],
|
||||
templateUrl: './table.component.html',
|
||||
|
@ -90,7 +90,7 @@
|
||||
}
|
||||
|
||||
@if (((filteredGroupsSpecialist && filteredGroupsSpecialist.length > 0 && filteredGroupsBehaviour && filteredGroupsBehaviour.length > 0) ||
|
||||
((!filteredGroupsSpecialist || filteredGroupsSpecialist.length === 0) && filteredGroupsBehaviour && filteredGroupsBehaviour.length > 0)) &&
|
||||
((!filteredGroupsSpecialist || filteredGroupsSpecialist.length === 0) && filteredGroupsBehaviour && filteredGroupsBehaviour.length > 0)) &&
|
||||
filteredGroupsMagistracy && filteredGroupsMagistracy.length > 0) {
|
||||
<div class="div-wrapper">
|
||||
<hr/>
|
||||
|
@ -23,7 +23,8 @@
|
||||
Кабинет
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<mat-chip-listbox hideSingleSelectionIndicator (change)="onLectureHallSelected($event.value)" [formControl]="formLectureHalls" #lectureChip>
|
||||
<mat-chip-listbox hideSingleSelectionIndicator (change)="onLectureHallSelected($event.value)"
|
||||
[formControl]="formLectureHalls" #lectureChip>
|
||||
@for (lectureHall of lectureHallsFiltered; track $index) {
|
||||
<mat-chip-option [value]="lectureHall.id" color="accent">
|
||||
{{ lectureHall.name }}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import {Component, EventEmitter, ViewChild} from '@angular/core';
|
||||
import {AsyncPipe} from "@angular/common";
|
||||
import {MatAccordion, MatExpansionModule, MatExpansionPanel} from "@angular/material/expansion";
|
||||
import {MatChipListbox, MatChipsModule} from "@angular/material/chips";
|
||||
import {catchError} from "rxjs";
|
||||
@ -22,7 +21,6 @@ enum Enclosure {
|
||||
imports: [
|
||||
MatChipsModule,
|
||||
MatExpansionModule,
|
||||
AsyncPipe,
|
||||
ReactiveFormsModule,
|
||||
MatAccordion,
|
||||
LoadingIndicatorComponent
|
||||
|
@ -1,11 +1,14 @@
|
||||
<!--suppress CssInvalidPropertyValue -->
|
||||
<button mat-button [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger" [id]="idButton" style="margin-bottom: 10px;">{{ textButton }}</button>
|
||||
<button mat-button [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger" [id]="idButton"
|
||||
style="margin-bottom: 10px;">{{ textButton }}
|
||||
</button>
|
||||
|
||||
<mat-menu #menu="matMenu" [hasBackdrop]="false" class="menu-options">
|
||||
<div (click)="$event.stopPropagation()" (keydown)="$event.stopPropagation()" style="padding: 0 15px 15px">
|
||||
<div class="header-menu">
|
||||
<mat-form-field appearance="outline" color="accent" style="display:flex;">
|
||||
<input matInput placeholder="Поиск..." [(ngModel)]="searchQuery" [disabled]="data === null || data.length === 0">
|
||||
<input matInput placeholder="Поиск..." [(ngModel)]="searchQuery"
|
||||
[disabled]="data === null || data.length === 0">
|
||||
<button mat-icon-button matSuffix (click)="clearSearchQuery()" [disabled]="data === null || data.length === 0">
|
||||
<mat-icon style="color: var(--mdc-filled-button-label-text-color);">close</mat-icon>
|
||||
</button>
|
||||
|
@ -4,7 +4,6 @@ import {MatTab, MatTabGroup} from "@angular/material/tabs";
|
||||
import {Observable} from "rxjs";
|
||||
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||
import {MatButton} from "@angular/material/button";
|
||||
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||
import {GroupComponent} from "@component/schedule/tabs/group/group.component";
|
||||
import {ProfessorComponent} from "@component/schedule/tabs/professor/professor.component";
|
||||
import {LectureHallComponent} from "@component/schedule/tabs/lecture-hall/lecture-hall.component";
|
||||
@ -15,7 +14,7 @@ import {DisciplineService} from "@api/v1/discipline.service";
|
||||
import {LectureHallService} from "@api/v1/lectureHall.service";
|
||||
import {GroupService} from "@api/v1/group.service";
|
||||
import {ProfessorService} from "@api/v1/professor.service";
|
||||
import {AuthRoles} from "@model/AuthRoles";
|
||||
import {AuthRoles} from "@model/authRoles";
|
||||
import {HasRoleDirective} from "@/directives/has-role.directive";
|
||||
import {TabSelectType, TabStorageService} from "@service/tab-storage.service";
|
||||
import {ScheduleRequest} from "@api/v1/scheduleRequest";
|
||||
@ -36,7 +35,6 @@ export enum TabsSelect {
|
||||
MatTab,
|
||||
ReactiveFormsModule,
|
||||
MatButton,
|
||||
DataSpinnerComponent,
|
||||
GroupComponent,
|
||||
ProfessorComponent,
|
||||
LectureHallComponent,
|
||||
|
@ -1,6 +1,6 @@
|
||||
import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core';
|
||||
import AuthApiService from "@api/v1/authApiService";
|
||||
import {AuthRoles} from "@model/AuthRoles";
|
||||
import {AuthRoles} from "@model/authRoles";
|
||||
import {catchError, of} from "rxjs";
|
||||
|
||||
@Directive({
|
||||
@ -13,7 +13,8 @@ export class HasRoleDirective {
|
||||
private templateRef: TemplateRef<any>,
|
||||
private viewContainer: ViewContainerRef,
|
||||
private authService: AuthApiService
|
||||
) {}
|
||||
) {
|
||||
}
|
||||
|
||||
@Input() set appHasRole(role: AuthRoles) {
|
||||
this.viewContainer.clear();
|
||||
@ -29,6 +30,6 @@ export class HasRoleDirective {
|
||||
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||
else
|
||||
this.viewContainer.clear();
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -8,10 +8,12 @@
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet">
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
|
||||
rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body class="mat-typography">
|
||||
<app-root></app-root>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import {bootstrapApplication} from '@angular/platform-browser';
|
||||
import {appConfig} from './app/app.config';
|
||||
import {AppComponent} from './app/app.component';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
|
@ -17,7 +17,7 @@
|
||||
|
||||
.formLogin form {
|
||||
display: flex;
|
||||
flex-direction:column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.formLoginButton {
|
||||
|
@ -57,7 +57,7 @@
|
||||
</form>
|
||||
|
||||
<mat-error>
|
||||
{{errorText}}
|
||||
{{ errorText }}
|
||||
</mat-error>
|
||||
|
||||
<div class="formLoginButton">
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component } from '@angular/core';
|
||||
import {Component} from '@angular/core';
|
||||
import {MatDialogActions, MatDialogContent, MatDialogRef, MatDialogTitle} from '@angular/material/dialog';
|
||||
import {MatButton} from "@angular/material/button";
|
||||
|
||||
@ -15,7 +15,8 @@ import {MatButton} from "@angular/material/button";
|
||||
})
|
||||
export class ConfirmDialogComponent {
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<ConfirmDialogComponent>) { }
|
||||
constructor(public dialogRef: MatDialogRef<ConfirmDialogComponent>) {
|
||||
}
|
||||
|
||||
protected onConfirm(): void {
|
||||
this.dialogRef.close(true);
|
||||
|
@ -9,13 +9,16 @@
|
||||
</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) {
|
||||
<app-data-spinner/>
|
||||
<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,76 +26,103 @@ import SetupService from "@api/v1/setup.service";
|
||||
})
|
||||
|
||||
export class SetupComponent {
|
||||
protected previousButtonDisabled: boolean = false;
|
||||
protected previousButtonRoute: string = '';
|
||||
|
||||
protected nextButtonDisabled: boolean = false;
|
||||
protected nextButtonRoute!: string;
|
||||
|
||||
protected skipButtonDisabled: boolean = false;
|
||||
protected loaderActive: boolean = false;
|
||||
|
||||
protected routes: Array<string> = ['', 'welcome', 'database', 'cache', 'create-admin', 'schedule', 'logging', 'summary'];
|
||||
protected routes: Array<string> = ['', 'welcome', 'database', 'cache', 'password-policy', 'schedule', 'logging', 'create-admin', 'two-factor', 'summary'];
|
||||
private index: number = 1;
|
||||
|
||||
protected get getIndex() {
|
||||
return this.index;
|
||||
}
|
||||
|
||||
constructor(private router: Router, private navigationService: NavigationService, api: SetupService) {
|
||||
constructor(private router: Router, private navigationService: NavigationService, api: SetupService, private notify: ToastrService) {
|
||||
api.isConfigured().subscribe(x => {
|
||||
if (x)
|
||||
this.router.navigate(['/']).then();
|
||||
if (x) this.router.navigate(['/']).then();
|
||||
});
|
||||
|
||||
if (!this.router.url.includes(this.routes[this.index]))
|
||||
if (!this.router.url.includes(this.routes[this.index])) {
|
||||
this.router.navigate(['setup/', this.routes[this.index]]).then();
|
||||
}
|
||||
|
||||
this.setRoutes();
|
||||
this.initializeButtonSubscriptions();
|
||||
|
||||
this.navigationService.autoSkipNavigation$.subscribe(action => {
|
||||
if (!this.navigationService.isNavigationUserInitiated) {
|
||||
this.executeAction(action);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private initializeButtonSubscriptions() {
|
||||
this.navigationService.nextButtonState$.subscribe(state => {
|
||||
this.nextButtonDisabled = !state;
|
||||
});
|
||||
|
||||
this.navigationService.skipNavigation.subscribe(action => {
|
||||
this.executeAction(action);
|
||||
this.navigationService.skipButtonState$.subscribe(state => {
|
||||
this.skipButtonDisabled = !state;
|
||||
});
|
||||
}
|
||||
|
||||
private setRoutes() {
|
||||
this.previousButtonRoute = this.routes[this.index - 1];
|
||||
this.nextButtonRoute = this.routes[this.index + 1];
|
||||
}
|
||||
|
||||
private executeAction(action: () => Observable<boolean>) {
|
||||
this.loaderActive = true;
|
||||
action().pipe(
|
||||
catchError(error => {
|
||||
this.nextButtonDisabled = true;
|
||||
this.loaderActive = false;
|
||||
throw error;
|
||||
})
|
||||
)
|
||||
.subscribe(x => {
|
||||
this.nextButtonDisabled = x;
|
||||
this.loaderActive = !x;
|
||||
if (x) {
|
||||
if (this.index < this.routes.length - 1) {
|
||||
this.router.navigate(['setup/', this.nextButtonRoute]).then();
|
||||
this.index++;
|
||||
this.setRoutes();
|
||||
} else
|
||||
this.router.navigate(['/']).then();
|
||||
action()
|
||||
.pipe(
|
||||
catchError(error => {
|
||||
this.nextButtonDisabled = true;
|
||||
this.loaderActive = false;
|
||||
throw error;
|
||||
})
|
||||
)
|
||||
.subscribe(success => {
|
||||
if (success) {
|
||||
this.moveToNextPage();
|
||||
} else {
|
||||
this.notify.error('Некорректно введены данные');
|
||||
this.nextButtonDisabled = true;
|
||||
}
|
||||
|
||||
this.loaderActive = false;
|
||||
});
|
||||
}
|
||||
|
||||
protected onSkipClick() {
|
||||
this.navigationService.skipButtonAction().subscribe(success => {
|
||||
if (success) {
|
||||
this.moveToNextPage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected onNextClick() {
|
||||
this.executeAction(this.navigationService.nextButtonAction);
|
||||
}
|
||||
|
||||
protected onPreviousClick() {
|
||||
if (this.index - 1 > 0) {
|
||||
this.index--;
|
||||
this.setRoutes();
|
||||
this.navigationService.setUserInitiatedNavigation(true);
|
||||
this.moveToPreviousPage();
|
||||
}
|
||||
|
||||
private moveToNextPage() {
|
||||
if (this.index < this.routes.length - 1) {
|
||||
this.index++;
|
||||
this.router.navigate(['setup/', this.routes[this.index]]).then();
|
||||
this.initializePage();
|
||||
} else {
|
||||
this.router.navigate(['/']).then();
|
||||
}
|
||||
}
|
||||
|
||||
private moveToPreviousPage() {
|
||||
if (this.index > 0) {
|
||||
this.index--;
|
||||
this.router.navigate(['setup/', this.routes[this.index]]).then();
|
||||
this.initializePage();
|
||||
}
|
||||
}
|
||||
|
||||
private initializePage() {
|
||||
this.navigationService.resetButtonStates();
|
||||
}
|
||||
}
|
||||
|
@ -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" alt="totp-qr-code"/>
|
||||
</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);
|
||||
}
|
||||
}
|
||||
|
5
src/shared/requests/v1/CreateUserRequest.ts
Normal file
5
src/shared/requests/v1/CreateUserRequest.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface CreateUserRequest {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
4
src/shared/requests/v1/LoginRequest.ts
Normal file
4
src/shared/requests/v1/LoginRequest.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface LoginRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
7
src/shared/requests/v1/ScheduleRequest.ts
Normal file
7
src/shared/requests/v1/ScheduleRequest.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface ScheduleRequest {
|
||||
groups?: Array<number>;
|
||||
isEven?: boolean;
|
||||
disciplines?: Array<number>;
|
||||
professors?: Array<number>;
|
||||
lectureHalls?: Array<number>;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a request to configure cache settings.
|
||||
*/
|
||||
export interface CacheRequest {
|
||||
/**
|
||||
* Gets or sets the server address.
|
||||
*/
|
||||
server: string;
|
||||
/**
|
||||
* Gets or sets the port number.
|
||||
*/
|
||||
port: number;
|
||||
/**
|
||||
* Gets or sets the password.
|
||||
*/
|
||||
password?: string;
|
||||
}
|
5
src/shared/requests/v1/configuration/cacheRequest.ts
Normal file
5
src/shared/requests/v1/configuration/cacheRequest.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface CacheRequest {
|
||||
server: string;
|
||||
port: number;
|
||||
password?: string;
|
||||
}
|
8
src/shared/requests/v1/configuration/databaseRequest.ts
Normal file
8
src/shared/requests/v1/configuration/databaseRequest.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface DatabaseRequest {
|
||||
server: string;
|
||||
port: number;
|
||||
database: string;
|
||||
user: string;
|
||||
ssl: boolean;
|
||||
password?: string;
|
||||
}
|
8
src/shared/requests/v1/configuration/emailRequest.ts
Normal file
8
src/shared/requests/v1/configuration/emailRequest.ts
Normal file
@ -0,0 +1,8 @@
|
||||
export interface EmailRequest {
|
||||
server: string;
|
||||
from: string;
|
||||
password: string;
|
||||
port: number;
|
||||
ssl: boolean;
|
||||
user: string;
|
||||
}
|
5
src/shared/requests/v1/configuration/loggingRequest.ts
Normal file
5
src/shared/requests/v1/configuration/loggingRequest.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface LoggingRequest {
|
||||
enableLogToFile: boolean;
|
||||
logFileName?: string;
|
||||
logFilePath?: string;
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export interface ScheduleConfigurationRequest {
|
||||
cronUpdateSchedule?: string;
|
||||
startTerm: string;
|
||||
}
|
@ -1,29 +1,5 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Request model for creating a user.
|
||||
*/
|
||||
export interface CreateUserRequest {
|
||||
/**
|
||||
* Gets or sets the email address of the user.
|
||||
*/
|
||||
email: string;
|
||||
/**
|
||||
* Gets or sets the username of the user.
|
||||
*/
|
||||
username: string;
|
||||
/**
|
||||
* Gets or sets the password of the user.
|
||||
*/
|
||||
password: string;
|
||||
}
|
||||
export interface CreateUserRequest {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a request to configure the database connection settings.
|
||||
*/
|
||||
export interface DatabaseRequest {
|
||||
/**
|
||||
* Gets or sets the server address.
|
||||
*/
|
||||
server: string;
|
||||
/**
|
||||
* Gets or sets the port number.
|
||||
*/
|
||||
port: number;
|
||||
/**
|
||||
* Gets or sets the database name.
|
||||
*/
|
||||
database: string;
|
||||
/**
|
||||
* Gets or sets the username.
|
||||
*/
|
||||
user: string;
|
||||
/**
|
||||
* Gets or sets a value indicating whether SSL is enabled.
|
||||
*/
|
||||
ssl: boolean;
|
||||
/**
|
||||
* Gets or sets the password.
|
||||
*/
|
||||
password?: string;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a request to configure logging settings.
|
||||
*/
|
||||
export interface LoggingRequest {
|
||||
/**
|
||||
* Gets or sets a value indicating whether logging to file is enabled.
|
||||
*/
|
||||
enableLogToFile: boolean;
|
||||
/**
|
||||
* Gets or sets the log file name.
|
||||
*/
|
||||
logFileName?: string;
|
||||
/**
|
||||
* Gets or sets the log file path.
|
||||
*/
|
||||
logFilePath?: string;
|
||||
}
|
@ -1,25 +1,4 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface LoginRequest {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
username: string;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
password: number;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
@ -1,24 +0,0 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
/**
|
||||
* Represents a request to configure the schedule settings.
|
||||
*/
|
||||
export interface ScheduleConfigurationRequest {
|
||||
/**
|
||||
* Gets or sets the cron expression for updating the schedule.
|
||||
*/
|
||||
cronUpdateSchedule?: string;
|
||||
/**
|
||||
* Gets or sets the start date of the term.
|
||||
*/
|
||||
startTerm: string;
|
||||
}
|
@ -1,37 +1,7 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a request object for retrieving schedules based on various filters.
|
||||
*/
|
||||
export interface ScheduleRequest {
|
||||
/**
|
||||
* Gets or sets an array of group IDs.
|
||||
*/
|
||||
groups?: Array<number>;
|
||||
/**
|
||||
* Gets or sets a value indicating whether to retrieve schedules for even weeks.
|
||||
*/
|
||||
isEven?: boolean;
|
||||
/**
|
||||
* Gets or sets an array of discipline IDs.
|
||||
*/
|
||||
disciplines?: Array<number>;
|
||||
/**
|
||||
* Gets or sets an array of professor IDs.
|
||||
*/
|
||||
professors?: Array<number>;
|
||||
/**
|
||||
* Gets or sets an array of lecture hall IDs.
|
||||
*/
|
||||
lectureHalls?: Array<number>;
|
||||
}
|
||||
|
6
src/shared/requests/v1/twoFactorAuthRequest.ts
Normal file
6
src/shared/requests/v1/twoFactorAuthRequest.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import {TwoFactorAuthentication} from "@model/twoFactorAuthentication";
|
||||
|
||||
export interface TwoFactorAuthRequest {
|
||||
code: string;
|
||||
method: TwoFactorAuthentication;
|
||||
}
|
5
src/shared/responses/v1/CampusBasicInfoResponse.ts
Normal file
5
src/shared/responses/v1/CampusBasicInfoResponse.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface CampusBasicInfoResponse {
|
||||
id: number;
|
||||
codeName: string;
|
||||
fullName?: string;
|
||||
}
|
6
src/shared/responses/v1/CampusDetailsResponse.ts
Normal file
6
src/shared/responses/v1/CampusDetailsResponse.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface CampusDetailsResponse {
|
||||
id: number;
|
||||
codeName: string;
|
||||
fullName?: string;
|
||||
address?: string;
|
||||
}
|
4
src/shared/responses/v1/DisciplineResponse.ts
Normal file
4
src/shared/responses/v1/DisciplineResponse.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface DisciplineResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
4
src/shared/responses/v1/ErrorResponse.ts
Normal file
4
src/shared/responses/v1/ErrorResponse.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
code: number;
|
||||
}
|
4
src/shared/responses/v1/FacultyResponse.ts
Normal file
4
src/shared/responses/v1/FacultyResponse.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface FacultyResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
7
src/shared/responses/v1/GroupDetailsResponse.ts
Normal file
7
src/shared/responses/v1/GroupDetailsResponse.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface GroupDetailsResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
courseNumber: number;
|
||||
facultyId?: number;
|
||||
facultyName?: string;
|
||||
}
|
6
src/shared/responses/v1/GroupResponse.ts
Normal file
6
src/shared/responses/v1/GroupResponse.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export interface GroupResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
courseNumber: number;
|
||||
facultyId?: number;
|
||||
}
|
7
src/shared/responses/v1/LectureHallDetailsResponse.ts
Normal file
7
src/shared/responses/v1/LectureHallDetailsResponse.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export interface LectureHallDetailsResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
campusId: number;
|
||||
campusName?: string;
|
||||
campusCode?: string;
|
||||
}
|
5
src/shared/responses/v1/LectureHallResponse.ts
Normal file
5
src/shared/responses/v1/LectureHallResponse.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface LectureHallResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
campusId: number;
|
||||
}
|
5
src/shared/responses/v1/ProfessorResponse.ts
Normal file
5
src/shared/responses/v1/ProfessorResponse.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface ProfessorResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
altName?: string;
|
||||
}
|
21
src/shared/responses/v1/ScheduleResponse.ts
Normal file
21
src/shared/responses/v1/ScheduleResponse.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import {DayOfWeek} from "@model/dayOfWeek";
|
||||
|
||||
export interface ScheduleResponse {
|
||||
dayOfWeek: DayOfWeek;
|
||||
pairNumber: number;
|
||||
isEven: boolean;
|
||||
discipline: string;
|
||||
disciplineId: number;
|
||||
isExcludedWeeks?: boolean;
|
||||
weeks?: Array<number>;
|
||||
typeOfOccupations: Array<string>;
|
||||
group: string;
|
||||
groupId: number;
|
||||
lectureHalls: Array<string | null>;
|
||||
lectureHallsId: Array<number | null>;
|
||||
professors: Array<string | null>;
|
||||
professorsId: Array<number | null>;
|
||||
campus: Array<string | null>;
|
||||
campusId: Array<number | null>;
|
||||
linkToMeet: Array<string | null>;
|
||||
}
|
7
src/shared/responses/v1/availableProvidersResponse.ts
Normal file
7
src/shared/responses/v1/availableProvidersResponse.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import {OAuthProvider} from "@model/oAuthProvider";
|
||||
|
||||
export interface AvailableOAuthProvidersResponse {
|
||||
providerName: string;
|
||||
provider: OAuthProvider;
|
||||
redirect: string;
|
||||
}
|
@ -1,29 +1,5 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents basic information about a campus.
|
||||
*/
|
||||
export interface CampusBasicInfoResponse {
|
||||
/**
|
||||
* Gets or sets the unique identifier of the campus.
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Gets or sets the code name of the campus.
|
||||
*/
|
||||
codeName: string;
|
||||
/**
|
||||
* Gets or sets the full name of the campus (optional).
|
||||
*/
|
||||
fullName?: string;
|
||||
}
|
||||
export interface CampusBasicInfoResponse {
|
||||
id: number;
|
||||
codeName: string;
|
||||
fullName?: string;
|
||||
}
|
||||
|
@ -1,33 +1,6 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents detailed information about a campus.
|
||||
*/
|
||||
export interface CampusDetailsResponse {
|
||||
/**
|
||||
* Gets or sets the unique identifier of the campus.
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Gets or sets the code name of the campus.
|
||||
*/
|
||||
codeName: string;
|
||||
/**
|
||||
* Gets or sets the full name of the campus (optional).
|
||||
*/
|
||||
fullName?: string;
|
||||
/**
|
||||
* Gets or sets the address of the campus (optional).
|
||||
*/
|
||||
address?: string;
|
||||
id: number;
|
||||
codeName: string;
|
||||
fullName?: string;
|
||||
address?: string;
|
||||
}
|
||||
|
8
src/shared/responses/v1/configuration/cacheResponse.ts
Normal file
8
src/shared/responses/v1/configuration/cacheResponse.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import {CacheType} from "@model/cacheType";
|
||||
|
||||
export interface CacheResponse {
|
||||
type: CacheType;
|
||||
server?: string;
|
||||
port: number;
|
||||
password?: string;
|
||||
}
|
12
src/shared/responses/v1/configuration/databaseResponse.ts
Normal file
12
src/shared/responses/v1/configuration/databaseResponse.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import {DatabaseType} from "@model/databaseType";
|
||||
|
||||
export interface DatabaseResponse {
|
||||
type: DatabaseType;
|
||||
server?: string;
|
||||
port: number;
|
||||
database?: string;
|
||||
user?: string;
|
||||
ssl: boolean;
|
||||
password?: string;
|
||||
pathToDatabase?: string;
|
||||
}
|
@ -1,25 +1,4 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents information about a discipline.
|
||||
*/
|
||||
export interface DisciplineResponse {
|
||||
/**
|
||||
* Gets or sets the unique identifier of the discipline.
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Gets or sets the name of the discipline.
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
export interface DisciplineResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
@ -1,41 +0,0 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a request to configure email settings.
|
||||
*/
|
||||
export interface EmailRequest {
|
||||
/**
|
||||
* Gets or sets the server address.
|
||||
*/
|
||||
server: string;
|
||||
/**
|
||||
* Gets or sets the email address from which emails will be sent.
|
||||
*/
|
||||
from: string;
|
||||
/**
|
||||
* Gets or sets the password for the email account.
|
||||
*/
|
||||
password: string;
|
||||
/**
|
||||
* Gets or sets the port number.
|
||||
*/
|
||||
port: number;
|
||||
/**
|
||||
* Gets or sets a value indicating whether SSL is enabled.
|
||||
*/
|
||||
ssl: boolean;
|
||||
/**
|
||||
* Gets or sets the username.
|
||||
*/
|
||||
user: string;
|
||||
}
|
@ -1,25 +1,4 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A class for providing information about an error
|
||||
*/
|
||||
export interface ErrorResponse {
|
||||
/**
|
||||
* The text or translation code of the error. This field may not contain information in specific scenarios. For example, it might be empty for HTTP 204 responses where no content is returned or if the validation texts have not been configured.
|
||||
*/
|
||||
error: string;
|
||||
/**
|
||||
* In addition to returning the response code in the header, it is also duplicated in this field. Represents the HTTP response code.
|
||||
*/
|
||||
code: number;
|
||||
}
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
code: number;
|
||||
}
|
||||
|
@ -1,25 +1,4 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents basic information about a faculty.
|
||||
*/
|
||||
export interface FacultyResponse {
|
||||
/**
|
||||
* Gets or sets the unique identifier of the faculty.
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Gets or sets the name of the faculty.
|
||||
*/
|
||||
name: string;
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
@ -1,37 +1,7 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents detailed information about a group.
|
||||
*/
|
||||
export interface GroupDetailsResponse {
|
||||
/**
|
||||
* Gets or sets the unique identifier of the group.
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Gets or sets the name of the group.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Gets or sets the course number of the group.
|
||||
*/
|
||||
courseNumber: number;
|
||||
/**
|
||||
* Gets or sets the unique identifier of the faculty to which the group belongs (optional).
|
||||
*/
|
||||
facultyId?: number;
|
||||
/**
|
||||
* Gets or sets the name of the faculty to which the group belongs (optional).
|
||||
*/
|
||||
facultyName?: string;
|
||||
}
|
||||
export interface GroupDetailsResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
courseNumber: number;
|
||||
facultyId?: number;
|
||||
facultyName?: string;
|
||||
}
|
||||
|
@ -1,33 +1,6 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents basic information about a group.
|
||||
*/
|
||||
export interface GroupResponse {
|
||||
/**
|
||||
* Gets or sets the unique identifier of the group.
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Gets or sets the name of the group.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Gets or sets the course number of the group.
|
||||
*/
|
||||
courseNumber: number;
|
||||
/**
|
||||
* Gets or sets the unique identifier of the faculty to which the group belongs (optional).
|
||||
*/
|
||||
facultyId?: number;
|
||||
}
|
||||
export interface GroupResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
courseNumber: number;
|
||||
facultyId?: number;
|
||||
}
|
||||
|
@ -1,37 +1,7 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents the detailed response model for a lecture hall.
|
||||
*/
|
||||
export interface LectureHallDetailsResponse {
|
||||
/**
|
||||
* Gets or sets the ID of the lecture hall.
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Gets or sets the name of the lecture hall.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Gets or sets the ID of the campus to which the lecture hall belongs.
|
||||
*/
|
||||
campusId: number;
|
||||
/**
|
||||
* Gets or sets the name of the campus.
|
||||
*/
|
||||
campusName?: string;
|
||||
/**
|
||||
* Gets or sets the code of the campus.
|
||||
*/
|
||||
campusCode?: string;
|
||||
}
|
||||
export interface LectureHallDetailsResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
campusId: number;
|
||||
campusName?: string;
|
||||
campusCode?: string;
|
||||
}
|
||||
|
@ -1,29 +1,5 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents the response model for a lecture hall.
|
||||
*/
|
||||
export interface LectureHallResponse {
|
||||
/**
|
||||
* Gets or sets the ID of the lecture hall.
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Gets or sets the name of the lecture hall.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Gets or sets the ID of the campus to which the lecture hall belongs.
|
||||
*/
|
||||
campusId: number;
|
||||
}
|
||||
export interface LectureHallResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
campusId: number;
|
||||
}
|
||||
|
@ -1,29 +1,5 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents information about a professor.
|
||||
*/
|
||||
export interface ProfessorResponse {
|
||||
/**
|
||||
* Gets or sets the unique identifier of the professor.
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Gets or sets the name of the professor.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Gets or sets the alternate name of the professor (optional).
|
||||
*/
|
||||
altName?: string;
|
||||
}
|
||||
export interface ProfessorResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
altName?: string;
|
||||
}
|
||||
|
@ -1,84 +1,21 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
import {DayOfWeek} from "@model/dayOfWeek";
|
||||
|
||||
/**
|
||||
* Represents a response object containing schedule information.
|
||||
*/
|
||||
export interface ScheduleResponse {
|
||||
dayOfWeek: DayOfWeek;
|
||||
/**
|
||||
* Gets or sets the pair number for the schedule entry.
|
||||
*/
|
||||
pairNumber: number;
|
||||
/**
|
||||
* Gets or sets a value indicating whether the pair is on an even week.
|
||||
*/
|
||||
isEven: boolean;
|
||||
/**
|
||||
* Gets or sets the name of the discipline for the schedule entry.
|
||||
*/
|
||||
discipline: string;
|
||||
/**
|
||||
* Gets or sets the ID of the discipline for the schedule entry.
|
||||
*/
|
||||
disciplineId: number;
|
||||
/**
|
||||
* Gets or sets exclude or include weeks for a specific discipline.
|
||||
*/
|
||||
isExcludedWeeks?: boolean;
|
||||
/**
|
||||
* The week numbers required for the correct display of the schedule. Whether there will be Mirea.Api.Dto.Responses.Schedule.ScheduleResponse.Discipline during the week or not depends on the Mirea.Api.Dto.Responses.Schedule.ScheduleResponse.IsExcludedWeeks property.
|
||||
*/
|
||||
weeks?: Array<number>;
|
||||
/**
|
||||
* Gets or sets the type of occupation for the schedule entry.
|
||||
*/
|
||||
typeOfOccupations: Array<string>;
|
||||
/**
|
||||
* Gets or sets the name of the group for the schedule entry.
|
||||
*/
|
||||
group: string;
|
||||
/**
|
||||
* Gets or sets the ID of the group for the schedule entry.
|
||||
*/
|
||||
groupId: number;
|
||||
/**
|
||||
* Gets or sets the names of the lecture halls for the schedule entry.
|
||||
*/
|
||||
lectureHalls: Array<string>;
|
||||
/**
|
||||
* Gets or sets the IDs of the lecture halls for the schedule entry.
|
||||
*/
|
||||
lectureHallsId: Array<number>;
|
||||
/**
|
||||
* Gets or sets the names of the professors for the schedule entry.
|
||||
*/
|
||||
professors: Array<string>;
|
||||
/**
|
||||
* Gets or sets the IDs of the professors for the schedule entry.
|
||||
*/
|
||||
professorsId: Array<number>;
|
||||
/**
|
||||
* Gets or sets the names of the campuses for the schedule entry.
|
||||
*/
|
||||
campus: Array<string>;
|
||||
/**
|
||||
* Gets or sets the IDs of the campuses for the schedule entry.
|
||||
*/
|
||||
campusId: Array<number>;
|
||||
/**
|
||||
* Gets or sets the links to online meetings for the schedule entry.
|
||||
*/
|
||||
linkToMeet: Array<string>;
|
||||
dayOfWeek: DayOfWeek;
|
||||
pairNumber: number;
|
||||
isEven: boolean;
|
||||
discipline: string;
|
||||
disciplineId: number;
|
||||
isExcludedWeeks?: boolean;
|
||||
weeks?: Array<number>;
|
||||
typeOfOccupations: Array<string>;
|
||||
group: string;
|
||||
groupId: number;
|
||||
lectureHalls: Array<string | null>;
|
||||
lectureHallsId: Array<number | null>;
|
||||
professors: Array<string | null>;
|
||||
professorsId: Array<number | null>;
|
||||
campus: Array<string | null>;
|
||||
campusId: Array<number | null>;
|
||||
linkToMeet: Array<string | null>;
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
/**
|
||||
* MIREA Schedule Web API
|
||||
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
|
||||
*
|
||||
* OpenAPI spec version: 1.0
|
||||
* Contact: support@winsomnia.net
|
||||
*
|
||||
* NOTE: This class is auto generated by the swagger code generator program.
|
||||
* https://github.com/swagger-api/swagger-codegen.git
|
||||
* Do not edit the class manually.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface TokenResponse {
|
||||
/**
|
||||
*
|
||||
*/
|
||||
accessToken : string;
|
||||
/**
|
||||
*
|
||||
*/
|
||||
expiresIn: Date;
|
||||
}
|
8
src/shared/responses/v1/userResponse.ts
Normal file
8
src/shared/responses/v1/userResponse.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import {OAuthProvider} from "@model/oAuthProvider";
|
||||
|
||||
export interface UserResponse {
|
||||
email: string;
|
||||
username: string;
|
||||
twoFactorAuthenticatorEnabled: boolean;
|
||||
usedOAuthProviders: OAuthProvider[];
|
||||
}
|
4
src/shared/structs/CacheType.ts
Normal file
4
src/shared/structs/CacheType.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum CacheType {
|
||||
Memcached,
|
||||
Redis
|
||||
}
|
5
src/shared/structs/DatabaseType.ts
Normal file
5
src/shared/structs/DatabaseType.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum DatabaseType {
|
||||
Mysql,
|
||||
Sqlite,
|
||||
PostgresSql
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user