Compare commits
49 Commits
1ffbfad37a
...
master
Author | SHA1 | Date | |
---|---|---|---|
52b2af097f | |||
ea5e731bd2 | |||
74a7fe7eb6 | |||
2f9d552e43 | |||
004671c006 | |||
0f6a1e7a45 | |||
437a3fcc58 | |||
0002371265 | |||
0f25d5404c | |||
e98a0db7ca | |||
324c7630ea | |||
f1f1ed16e1 | |||
6fcd68b627 | |||
d50da4db3e | |||
066b1444af | |||
df4ea723b3 | |||
434dec492d | |||
24d6b91553 | |||
2b988db70d | |||
a3a19be5a4 | |||
9f742cab78 | |||
c8bcda8da2 | |||
1bf2868d00 | |||
5b9b67d50c | |||
061307447e | |||
cf09738447 | |||
79a992dc69 | |||
612da04cbb | |||
3d38b49839 | |||
fcd179166e | |||
224d7a3443 | |||
2370a2051b | |||
1d691ccc09 | |||
a7542eaf32 | |||
a8b1485b0e | |||
90fca336f5 | |||
a7b8c15e3a | |||
135570d384 | |||
7830c5f21d | |||
6e914caabc | |||
f26d74aae5 | |||
3aefee124a | |||
eda6ca4b1a | |||
10bf53adec | |||
e10075dfed | |||
2b482d2b2d | |||
9017e87175 | |||
16e25905dc | |||
8138a63324 |
@ -1,6 +1,6 @@
|
|||||||
# MIREA schedule by Winsomnia
|
# MIREA schedule by Winsomnia
|
||||||
|
|
||||||
[](https://github.com/angular/angular-cli)
|
[](https://github.com/angular/angular-cli)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
This project provides a Web interface for working with the MIREA schedule.
|
This project provides a Web interface for working with the MIREA schedule.
|
||||||
|
4407
package-lock.json
generated
4407
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
32
package.json
32
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "frontend",
|
"name": "frontend",
|
||||||
"version": "1.0.0-b10",
|
"version": "1.0.0-rc0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
@ -10,17 +10,17 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^19.0.4",
|
"@angular/animations": "^19.1.4",
|
||||||
"@angular/cdk": "~19.0.3",
|
"@angular/cdk": "~19.1.2",
|
||||||
"@angular/cdk-experimental": "^19.0.3",
|
"@angular/cdk-experimental": "^19.1.2",
|
||||||
"@angular/common": "^19.0.4",
|
"@angular/common": "^19.1.4",
|
||||||
"@angular/compiler": "^19.0.4",
|
"@angular/compiler": "^19.1.4",
|
||||||
"@angular/core": "^19.0.4",
|
"@angular/core": "^19.1.4",
|
||||||
"@angular/forms": "^19.0.4",
|
"@angular/forms": "^19.1.4",
|
||||||
"@angular/material": "~19.0.3",
|
"@angular/material": "~19.1.2",
|
||||||
"@angular/platform-browser": "^19.0.4",
|
"@angular/platform-browser": "^19.1.4",
|
||||||
"@angular/platform-browser-dynamic": "^19.0.4",
|
"@angular/platform-browser-dynamic": "^19.1.4",
|
||||||
"@angular/router": "^19.0.4",
|
"@angular/router": "^19.1.4",
|
||||||
"@progress/kendo-date-math": "^1.5.14",
|
"@progress/kendo-date-math": "^1.5.14",
|
||||||
"ngx-toastr": "^19.0.0",
|
"ngx-toastr": "^19.0.0",
|
||||||
"rxjs": "~7.8.1",
|
"rxjs": "~7.8.1",
|
||||||
@ -28,9 +28,9 @@
|
|||||||
"zone.js": "^0.15.0"
|
"zone.js": "^0.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^19.0.5",
|
"@angular-devkit/build-angular": "^19.1.5",
|
||||||
"@angular/cli": "^19.0.5",
|
"@angular/cli": "^19.1.5",
|
||||||
"@angular/compiler-cli": "^19.0.4",
|
"@angular/compiler-cli": "^19.1.4",
|
||||||
"@types/jasmine": "~5.1.5",
|
"@types/jasmine": "~5.1.5",
|
||||||
"jasmine-core": "~5.5.0",
|
"jasmine-core": "~5.5.0",
|
||||||
"karma": "~6.4.4",
|
"karma": "~6.4.4",
|
||||||
@ -38,6 +38,6 @@
|
|||||||
"karma-coverage": "~2.2.1",
|
"karma-coverage": "~2.2.1",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "^5.6.3"
|
"typescript": "^5.7.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,57 +11,44 @@ export interface RequestData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class RequestBuilder {
|
export class RequestBuilder {
|
||||||
private endpoint: string = '';
|
private result: RequestData = Object.create({});
|
||||||
private queryParams: Record<string, string | number | boolean | Array<any> | null> | null = null;
|
|
||||||
private httpHeaders: HttpHeaders = new HttpHeaders();
|
|
||||||
private data: any = null;
|
|
||||||
private silenceMode: boolean = false;
|
|
||||||
private withCredentials: boolean = false;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public setEndpoint(endpoint: string): this {
|
public setEndpoint(endpoint: string): this {
|
||||||
this.endpoint = endpoint;
|
this.result.endpoint = endpoint;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setQueryParams(queryParams: Record<string, string | number | boolean | Array<any> | null>): RequestBuilder {
|
public setQueryParams(queryParams: Record<string, string | number | boolean | Array<any> | null>): RequestBuilder {
|
||||||
this.queryParams = queryParams;
|
this.result.queryParams = queryParams;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addHeaders(headers: Record<string, string>): RequestBuilder {
|
public addHeaders(headers: Record<string, string>): RequestBuilder {
|
||||||
Object.keys(headers).forEach(key => {
|
Object.keys(headers).forEach(key => {
|
||||||
this.httpHeaders = this.httpHeaders.set(key, headers[key]);
|
this.result.httpHeaders = this.result.httpHeaders.set(key, headers[key]);
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setData(data: any): RequestBuilder {
|
public setData(data: any): RequestBuilder {
|
||||||
this.data = data;
|
this.result.data = data;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setSilenceMode(silence: boolean = true): RequestBuilder {
|
public setSilenceMode(silence: boolean = true): RequestBuilder {
|
||||||
this.silenceMode = silence;
|
this.result.silenceMode = silence;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setWithCredentials(credentials: boolean = true): RequestBuilder {
|
public setWithCredentials(credentials: boolean = true): RequestBuilder {
|
||||||
this.withCredentials = credentials;
|
this.result.withCredentials = credentials;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get build(): RequestData {
|
public get build(): RequestData {
|
||||||
return {
|
return this.result;
|
||||||
endpoint: this.endpoint,
|
|
||||||
queryParams: this.queryParams,
|
|
||||||
httpHeaders: this.httpHeaders,
|
|
||||||
data: this.data,
|
|
||||||
silenceMode: this.silenceMode,
|
|
||||||
withCredentials: this.withCredentials,
|
|
||||||
needAuth: false
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ export default abstract class ApiService {
|
|||||||
Object.keys(queryParams).forEach(key => {
|
Object.keys(queryParams).forEach(key => {
|
||||||
const value = queryParams[key];
|
const value = queryParams[key];
|
||||||
if (value !== null && value !== undefined) {
|
if (value !== null && value !== undefined) {
|
||||||
if (typeof (value) === typeof (Array)) {
|
if (Array.isArray(value)) {
|
||||||
(value as Array<any>).forEach(x => url.searchParams.append(key, x.toString()));
|
(value as Array<any>).forEach(x => url.searchParams.append(key, x.toString()));
|
||||||
} else
|
} else
|
||||||
url.searchParams.append(key, value.toString());
|
url.searchParams.append(key, value.toString());
|
||||||
@ -52,7 +52,7 @@ export default abstract class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static combineUrls(...parts: string[]): string {
|
private static combineUrls(...parts: string[]): string {
|
||||||
return parts.map(part => part.replace(/(^\/+|\/+$)/g, '')).join('/');
|
return parts.map(part => (!part || part == '' ? '/' : part).replace(/(^\/+|\/+$)/g, '')).join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected combinedUrl(request: RequestData) {
|
protected combinedUrl(request: RequestData) {
|
||||||
@ -65,10 +65,10 @@ export default abstract class ApiService {
|
|||||||
return this.http.request<Type>(method, doneEndpoint, {
|
return this.http.request<Type>(method, doneEndpoint, {
|
||||||
withCredentials: request.withCredentials,
|
withCredentials: request.withCredentials,
|
||||||
headers: request.httpHeaders,
|
headers: request.httpHeaders,
|
||||||
body: request.data,
|
body: request.data
|
||||||
}).pipe(
|
}).pipe(
|
||||||
catchError(error => {
|
catchError(error => {
|
||||||
if (!secondTry && error.status === 401)
|
if (request.needAuth && !secondTry && error.status === 401)
|
||||||
return this.handle401Error(error).pipe(
|
return this.handle401Error(error).pipe(
|
||||||
switchMap(() => this.sendHttpRequest<Type>(method, request, true))
|
switchMap(() => this.sendHttpRequest<Type>(method, request, true))
|
||||||
);
|
);
|
||||||
@ -158,7 +158,7 @@ export default abstract class ApiService {
|
|||||||
|
|
||||||
private handleError(error: HttpErrorResponse): void {
|
private handleError(error: HttpErrorResponse): void {
|
||||||
// todo: change to Retry-After condition
|
// todo: change to Retry-After condition
|
||||||
if (error.error && error.error.toString().includes("setup")) {
|
if (error.error && error.error.detail && error.error.detail.includes("setup")) {
|
||||||
this.router.navigate(['/setup/']).then();
|
this.router.navigate(['/setup/']).then();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -167,6 +167,10 @@ export default abstract class ApiService {
|
|||||||
let message: string | undefined = undefined;
|
let message: string | undefined = undefined;
|
||||||
if (error.error instanceof ErrorEvent) {
|
if (error.error instanceof ErrorEvent) {
|
||||||
title = `Произошла ошибка: ${error.error.message}`;
|
title = `Произошла ошибка: ${error.error.message}`;
|
||||||
|
} else {
|
||||||
|
if (error.error && error.error.type && error.error.title) {
|
||||||
|
title = error.error.title || `Ошибка с кодом ${error.status}`;
|
||||||
|
message = error.error.detail || 'Неизвестная ошибка';
|
||||||
} else {
|
} else {
|
||||||
switch (error.status) {
|
switch (error.status) {
|
||||||
case 0:
|
case 0:
|
||||||
@ -195,11 +199,9 @@ export default abstract class ApiService {
|
|||||||
title = `Сервер вернул код ошибки: ${error.status}`;
|
title = `Сервер вернул код ошибки: ${error.status}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (error.error?.Error)
|
}
|
||||||
message = error.error.Error;
|
|
||||||
else if (error.error instanceof String)
|
if (!message)
|
||||||
message = error.error.toString();
|
|
||||||
else
|
|
||||||
message = error.error.statusMessage;
|
message = error.error.statusMessage;
|
||||||
}
|
}
|
||||||
this.notify.error(message == '' ? undefined : message, title);
|
this.notify.error(message == '' ? undefined : message, title);
|
||||||
|
@ -5,6 +5,9 @@ import {catchError, map, Observable, of} from "rxjs";
|
|||||||
import {AuthRoles} from "@model/authRoles";
|
import {AuthRoles} from "@model/authRoles";
|
||||||
import {AvailableOAuthProvidersResponse} from "@api/v1/availableProvidersResponse";
|
import {AvailableOAuthProvidersResponse} from "@api/v1/availableProvidersResponse";
|
||||||
import {OAuthProvider} from "@model/oAuthProvider";
|
import {OAuthProvider} from "@model/oAuthProvider";
|
||||||
|
import {TwoFactorAuthentication} from "@model/twoFactorAuthentication";
|
||||||
|
import {TwoFactorAuthRequest} from "@api/v1/twoFactorAuthRequest";
|
||||||
|
import {OAuthAction} from "@model/oAuthAction";
|
||||||
|
|
||||||
export interface OAuthProviderData extends AvailableOAuthProvidersResponse {
|
export interface OAuthProviderData extends AvailableOAuthProvidersResponse {
|
||||||
icon: string;
|
icon: string;
|
||||||
@ -22,7 +25,17 @@ export default class AuthApiService extends ApiService {
|
|||||||
.setWithCredentials()
|
.setWithCredentials()
|
||||||
.build;
|
.build;
|
||||||
|
|
||||||
return this.post<AuthRoles>(request);
|
return this.post<TwoFactorAuthentication>(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public twoFactorAuth(data: TwoFactorAuthRequest) {
|
||||||
|
let request = this.createRequestBuilder()
|
||||||
|
.setEndpoint('2FA')
|
||||||
|
.setData(data)
|
||||||
|
.setWithCredentials()
|
||||||
|
.build;
|
||||||
|
|
||||||
|
return this.post<boolean>(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public reLogin() {
|
public reLogin() {
|
||||||
@ -71,9 +84,10 @@ export default class AuthApiService extends ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public availableProviders(): Observable<OAuthProviderData[]> {
|
public availableProviders(callback: string): Observable<OAuthProviderData[]> {
|
||||||
let request = this.createRequestBuilder()
|
let request = this.createRequestBuilder()
|
||||||
.setEndpoint('AvailableProviders')
|
.setEndpoint('AvailableProviders')
|
||||||
|
.setQueryParams({callback: callback})
|
||||||
.setWithCredentials()
|
.setWithCredentials()
|
||||||
.build;
|
.build;
|
||||||
|
|
||||||
@ -85,4 +99,21 @@ export default class AuthApiService extends ApiService {
|
|||||||
}) as OAuthProviderData);
|
}) as OAuthProviderData);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleTokenRequest(token: string, action: OAuthAction) {
|
||||||
|
return this.createRequestBuilder()
|
||||||
|
.setEndpoint('HandleToken')
|
||||||
|
.setQueryParams({token: token, action: action})
|
||||||
|
.setWithCredentials()
|
||||||
|
.build;
|
||||||
|
}
|
||||||
|
|
||||||
|
public loginOAuth(token: string) {
|
||||||
|
return this.get<TwoFactorAuthentication>(this.handleTokenRequest(token, OAuthAction.Login));
|
||||||
|
}
|
||||||
|
|
||||||
|
public linkAccount(token: string): Observable<null> {
|
||||||
|
const request = this.handleTokenRequest(token, OAuthAction.Bind);
|
||||||
|
return this.addAuth(request).get(request);
|
||||||
|
}
|
||||||
}
|
}
|
89
src/api/v1/configuration/schedule.service.ts
Normal file
89
src/api/v1/configuration/schedule.service.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||||
|
import {CronUpdateScheduleResponse} from "@api/v1/configuration/cronUpdateScheduleResponse";
|
||||||
|
import {DateOnly} from "@model/dateOnly";
|
||||||
|
import {map} from "rxjs";
|
||||||
|
import CronUpdateSkip from "@model/cronUpdateSkip";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ScheduleService extends ApiService {
|
||||||
|
public readonly basePath = 'Configuration/Schedule';
|
||||||
|
public readonly version = AvailableVersion.v1;
|
||||||
|
|
||||||
|
public getCronUpdateSchedule() {
|
||||||
|
const request = this.createRequestBuilder()
|
||||||
|
.setEndpoint('CronUpdateSchedule')
|
||||||
|
.build;
|
||||||
|
|
||||||
|
return this.addAuth(request).get<CronUpdateScheduleResponse>(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public postCronUpdateSchedule(cron: string) {
|
||||||
|
const request = this.createRequestBuilder()
|
||||||
|
.setEndpoint('CronUpdateSchedule')
|
||||||
|
.setQueryParams({cron: cron})
|
||||||
|
.build;
|
||||||
|
|
||||||
|
return this.addAuth(request).post<CronUpdateScheduleResponse>(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getStartTerm() {
|
||||||
|
const request = this.createRequestBuilder()
|
||||||
|
.setEndpoint('StartTerm')
|
||||||
|
.build;
|
||||||
|
|
||||||
|
return this.addAuth(request).get<string>(request).pipe(map(date => new DateOnly(date)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public postStartTerm(startTerm: DateOnly, force: boolean) {
|
||||||
|
const request = this.createRequestBuilder()
|
||||||
|
.setEndpoint('StartTerm')
|
||||||
|
.setQueryParams({force: force, startTerm: startTerm.toString()})
|
||||||
|
.build;
|
||||||
|
|
||||||
|
return this.addAuth(request).post(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCronUpdateSkip() {
|
||||||
|
const request = this.createRequestBuilder()
|
||||||
|
.setEndpoint('CronUpdateSkip')
|
||||||
|
.build;
|
||||||
|
|
||||||
|
return this.addAuth(request).get<{ date?: string, start?: string, end?: string }[]>(request)
|
||||||
|
.pipe(
|
||||||
|
map(data => {
|
||||||
|
return data.map(x => <CronUpdateSkip>{
|
||||||
|
date: x.date ? new DateOnly(x.date) : null,
|
||||||
|
start: x.start ? new DateOnly(x.start) : null,
|
||||||
|
end: x.end ? new DateOnly(x.end) : null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public postCronUpdateSkip(data: CronUpdateSkip[]) {
|
||||||
|
const request = this.createRequestBuilder()
|
||||||
|
.setEndpoint('CronUpdateSkip')
|
||||||
|
.setData(data.map(x => <any>{
|
||||||
|
start: x.start?.toString(),
|
||||||
|
end: x.end?.toString(),
|
||||||
|
date: x.date?.toString()
|
||||||
|
}))
|
||||||
|
.build;
|
||||||
|
|
||||||
|
return this.addAuth(request).post<any>(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public uploadScheduleFile(files: File[], campus: string[], force: boolean) {
|
||||||
|
const formData = new FormData();
|
||||||
|
files.forEach(file => formData.append('files', file, file.name));
|
||||||
|
|
||||||
|
const request = this.createRequestBuilder()
|
||||||
|
.setEndpoint('Upload')
|
||||||
|
.setData(formData)
|
||||||
|
.setQueryParams({force: force, defaultCampus: campus})
|
||||||
|
.build;
|
||||||
|
|
||||||
|
return this.addAuth(request).post(request);
|
||||||
|
}
|
||||||
|
}
|
17
src/api/v1/lessonType.service.ts
Normal file
17
src/api/v1/lessonType.service.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import {Injectable} from "@angular/core";
|
||||||
|
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||||
|
import {LessonTypeResponse} from "@api/v1/lessonTypeResponse";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class LessonTypeService extends ApiService {
|
||||||
|
public readonly basePath = 'LessonType/';
|
||||||
|
public readonly version = AvailableVersion.v1;
|
||||||
|
|
||||||
|
public getLessonTypes(page: number | null = null, pageSize: number | null = null) {
|
||||||
|
let request = this.createRequestBuilder()
|
||||||
|
.setQueryParams({page: page, pageSize: pageSize})
|
||||||
|
.build;
|
||||||
|
|
||||||
|
return this.get<LessonTypeResponse[]>(request);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import {Injectable} from "@angular/core";
|
import {Injectable} from "@angular/core";
|
||||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||||
|
import {PasswordPolicy} from "@model/passwordPolicy";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export default class SecurityService extends ApiService {
|
export default class SecurityService extends ApiService {
|
||||||
@ -14,4 +15,12 @@ export default class SecurityService extends ApiService {
|
|||||||
|
|
||||||
return this.combinedUrl(request);
|
return this.combinedUrl(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public passwordPolicy() {
|
||||||
|
let request = this.createRequestBuilder()
|
||||||
|
.setEndpoint('PasswordPolicy')
|
||||||
|
.build;
|
||||||
|
|
||||||
|
return this.get<PasswordPolicy>(request);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {Injectable} from "@angular/core";
|
import {Injectable} from "@angular/core";
|
||||||
import ApiService, {AvailableVersion} from "@api/api.service";
|
import ApiService, {AvailableVersion} from "@api/api.service";
|
||||||
import {catchError, of, switchMap} from "rxjs";
|
import {catchError, of} from "rxjs";
|
||||||
import {DatabaseResponse} from "@api/v1/configuration/databaseResponse";
|
import {DatabaseResponse} from "@api/v1/configuration/databaseResponse";
|
||||||
import {DatabaseRequest} from "@api/v1/configuration/databaseRequest";
|
import {DatabaseRequest} from "@api/v1/configuration/databaseRequest";
|
||||||
import {CacheRequest} from "@api/v1/configuration/cacheRequest";
|
import {CacheRequest} from "@api/v1/configuration/cacheRequest";
|
||||||
@ -137,18 +137,21 @@ export default class SetupService extends ApiService {
|
|||||||
|
|
||||||
public adminConfiguration() {
|
public adminConfiguration() {
|
||||||
let request = this.createRequestBuilder()
|
let request = this.createRequestBuilder()
|
||||||
.setEndpoint('UpdateAdminConfiguration')
|
|
||||||
.setWithCredentials()
|
|
||||||
.build;
|
|
||||||
|
|
||||||
return this.get(request).pipe(switchMap(_ => {
|
|
||||||
request = this.createRequestBuilder()
|
|
||||||
.setEndpoint('AdminConfiguration')
|
.setEndpoint('AdminConfiguration')
|
||||||
.setWithCredentials()
|
.setWithCredentials()
|
||||||
.build;
|
.build;
|
||||||
|
|
||||||
return this.get<UserResponse>(request);
|
return this.get<UserResponse>(request);
|
||||||
}));
|
}
|
||||||
|
|
||||||
|
public registerOAuth(token: string) {
|
||||||
|
let request = this.createRequestBuilder()
|
||||||
|
.setEndpoint('HandleToken')
|
||||||
|
.setQueryParams({token: token})
|
||||||
|
.setWithCredentials()
|
||||||
|
.build;
|
||||||
|
|
||||||
|
return this.get<null>(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setLogging(data: LoggingRequest | null = null) {
|
public setLogging(data: LoggingRequest | null = null) {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import {ApplicationConfig} from '@angular/core';
|
import {ApplicationConfig, LOCALE_ID} from '@angular/core';
|
||||||
import {provideRouter} from '@angular/router';
|
import {provideRouter} from '@angular/router';
|
||||||
|
|
||||||
import {routes} from './app.routes';
|
import {routes} from './app.routes';
|
||||||
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
|
import {provideAnimationsAsync} from '@angular/platform-browser/animations/async';
|
||||||
import {provideHttpClient} from "@angular/common/http";
|
import {provideHttpClient} from "@angular/common/http";
|
||||||
import {provideToastr} from "ngx-toastr";
|
import {provideToastr} from "ngx-toastr";
|
||||||
|
import {MAT_DATE_LOCALE, provideNativeDateAdapter} from "@angular/material/core";
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
@ -22,5 +22,8 @@ export const appConfig: ApplicationConfig = {
|
|||||||
disableTimeOut: false,
|
disableTimeOut: false,
|
||||||
autoDismiss: true,
|
autoDismiss: true,
|
||||||
maxOpened: 5
|
maxOpened: 5
|
||||||
})]
|
}),
|
||||||
|
provideNativeDateAdapter(),
|
||||||
|
{ provide: LOCALE_ID, useValue: 'ru' },
|
||||||
|
{ provide: MAT_DATE_LOCALE, useValue: 'ru' }]
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,9 @@ import {SummaryComponent} from "@page/setup/summary/summary.component";
|
|||||||
import {LoginComponent} from "@page/login/login.component";
|
import {LoginComponent} from "@page/login/login.component";
|
||||||
import {PasswordPolicyComponent} from "@page/setup/password-policy/password-policy.component";
|
import {PasswordPolicyComponent} from "@page/setup/password-policy/password-policy.component";
|
||||||
import {TwoFactorComponent} from "@page/setup/two-factor/two-factor.component";
|
import {TwoFactorComponent} from "@page/setup/two-factor/two-factor.component";
|
||||||
|
import {AdminComponent} from "@page/admin/admin.component";
|
||||||
|
import {UnderConstructionComponent} from "@page/admin/under-construction/under-construction.component";
|
||||||
|
import {ScheduleConfigurationComponent} from "@page/admin/schedule-configuration/schedule-configuration.component";
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{path: '', title: 'Расписание', pathMatch: 'full', component: ScheduleComponent},
|
{path: '', title: 'Расписание', pathMatch: 'full', component: ScheduleComponent},
|
||||||
@ -29,6 +32,14 @@ export const routes: Routes = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{path: 'login', title: 'Вход', component: LoginComponent},
|
{path: 'login', title: 'Вход', component: LoginComponent},
|
||||||
/*{path: 'not-found', title: '404 страница не найдена'},
|
{
|
||||||
{path: '**', redirectTo: '/not-found'}*/
|
path: 'admin', title: 'Админ панель', component: AdminComponent, children: [
|
||||||
|
{path: 'schedule', component: ScheduleConfigurationComponent},
|
||||||
|
{path: 'institute', component: UnderConstructionComponent},
|
||||||
|
{path: 'account', component: UnderConstructionComponent},
|
||||||
|
{path: 'server', component: UnderConstructionComponent},
|
||||||
|
{path: '', redirectTo: 'schedule', pathMatch: 'full'},
|
||||||
|
{path: '**', redirectTo: 'schedule', pathMatch: 'full'}
|
||||||
|
]
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
@ -75,3 +75,11 @@
|
|||||||
.provider-item.provider-unlink:hover::after {
|
.provider-item.provider-unlink:hover::after {
|
||||||
transform: translate(-50%, -50%) scale(1.0);
|
transform: translate(-50%, -50%) scale(1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.provider-item .provider-spinner {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%) scale(1.0);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
@if (providers.length !== 0) {
|
@if (!loading && providers.length !== 0) {
|
||||||
<hr/>
|
<hr/>
|
||||||
<div>
|
<div>
|
||||||
<p class="mat-body-2 secondary">{{ message }}</p>
|
<p class="mat-body-2 secondary">{{ message }}</p>
|
||||||
@ -6,11 +6,18 @@
|
|||||||
<div class="provider-container">
|
<div class="provider-container">
|
||||||
@for (provider of providers; track $index) {
|
@for (provider of providers; track $index) {
|
||||||
<a class="provider-item" (click)="provider.disabled ? confirmDelete(provider) : openOAuth(provider)"
|
<a class="provider-item" (click)="provider.disabled ? confirmDelete(provider) : openOAuth(provider)"
|
||||||
[class.disabled]="!canUnlink && provider.disabled" [class.provider-unlink]="canUnlink && provider.disabled">
|
[class.disabled]="!canUnlink && provider.disabled || provider.active"
|
||||||
|
[class.provider-unlink]="canUnlink && provider.disabled">
|
||||||
<img [alt]="provider.providerName" [src]="provider.icon"
|
<img [alt]="provider.providerName" [src]="provider.icon"
|
||||||
class="provider-icon" draggable="false"/>
|
class="provider-icon" draggable="false"/>
|
||||||
|
@if (provider.active) {
|
||||||
|
<app-data-spinner class="provider-spinner"/>
|
||||||
|
}
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
} @else if (loading) {
|
||||||
|
<hr/>
|
||||||
|
<app-data-spinner style="display: flex; justify-content: center;"/>
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,26 @@
|
|||||||
import {Component, Inject, Input, OnInit} from '@angular/core';
|
import {Component, EventEmitter, Inject, Input, OnInit, Output} from '@angular/core';
|
||||||
import AuthApiService, {OAuthProviderData} from "@api/v1/authApiService";
|
import AuthApiService, {OAuthProviderData} from "@api/v1/authApi.service";
|
||||||
import {OAuthProvider} from "@model/oAuthProvider";
|
import {OAuthProvider} from "@model/oAuthProvider";
|
||||||
import {ToastrService} from "ngx-toastr";
|
import {ToastrService} from "ngx-toastr";
|
||||||
import {
|
import {
|
||||||
MAT_DIALOG_DATA, MatDialog,
|
MAT_DIALOG_DATA,
|
||||||
|
MatDialog,
|
||||||
MatDialogActions,
|
MatDialogActions,
|
||||||
MatDialogContent,
|
MatDialogContent,
|
||||||
MatDialogRef,
|
MatDialogRef,
|
||||||
MatDialogTitle
|
MatDialogTitle
|
||||||
} from "@angular/material/dialog";
|
} from "@angular/material/dialog";
|
||||||
import {MatButton} from "@angular/material/button";
|
import {MatButton} from "@angular/material/button";
|
||||||
|
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||||
|
import {ActivatedRoute} from "@angular/router";
|
||||||
|
import {catchError, finalize, Observable, switchMap, tap} from "rxjs";
|
||||||
|
import {TwoFactorAuthentication} from "@model/twoFactorAuthentication";
|
||||||
|
import {OAuthAction} from "@model/oAuthAction";
|
||||||
|
import SetupService from "@api/v1/setup.service";
|
||||||
|
|
||||||
interface AvailableOAuthProviders extends OAuthProviderData {
|
interface AvailableOAuthProviders extends OAuthProviderData {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
active: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -53,17 +61,25 @@ export class DeleteConfirmDialog {
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'OAuthProviders',
|
selector: 'OAuthProviders',
|
||||||
imports: [],
|
imports: [
|
||||||
|
DataSpinnerComponent
|
||||||
|
],
|
||||||
templateUrl: './OAuthProviders.html',
|
templateUrl: './OAuthProviders.html',
|
||||||
styleUrl: './OAuthProviders.css',
|
styleUrl: './OAuthProviders.css',
|
||||||
providers: [AuthApiService]
|
providers: [SetupService, AuthApiService]
|
||||||
})
|
})
|
||||||
export class OAuthProviders implements OnInit {
|
export class OAuthProviders implements OnInit {
|
||||||
protected providers: AvailableOAuthProviders[] = [];
|
protected providers: AvailableOAuthProviders[] = [];
|
||||||
protected _activeProvidersId: OAuthProvider[] = [];
|
protected _activeProvidersId: OAuthProvider[] = [];
|
||||||
|
protected _activeProviders: string[] = [];
|
||||||
|
protected loading = true;
|
||||||
|
|
||||||
@Input() message: string = 'Вы можете войти в аккаунт через';
|
@Input() message: string = 'Вы можете войти в аккаунт через';
|
||||||
@Input() activeProviders: string[] = [];
|
|
||||||
|
@Input() set activeProviders(data: string[]) {
|
||||||
|
this._activeProviders = data;
|
||||||
|
this.updateDisabledProviders();
|
||||||
|
}
|
||||||
|
|
||||||
@Input() set activeProvidersId(data: OAuthProvider[]) {
|
@Input() set activeProvidersId(data: OAuthProvider[]) {
|
||||||
this._activeProvidersId = data;
|
this._activeProvidersId = data;
|
||||||
@ -71,37 +87,89 @@ export class OAuthProviders implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Input() canUnlink: boolean = false;
|
@Input() canUnlink: boolean = false;
|
||||||
|
@Input() action: OAuthAction = OAuthAction.Login;
|
||||||
|
@Input() isSetup: boolean = false;
|
||||||
|
|
||||||
constructor(authApi: AuthApiService, private notify: ToastrService, private dialog: MatDialog) {
|
@Output() public oAuthUpdateProviders = new EventEmitter();
|
||||||
authApi.availableProviders().subscribe(providers => this.updateDisabledProviders(providers));
|
@Output() public oAuthLoginResult: EventEmitter<TwoFactorAuthentication> = new EventEmitter();
|
||||||
|
|
||||||
|
constructor(private setupApi: SetupService,
|
||||||
|
private authApi: AuthApiService,
|
||||||
|
private notify: ToastrService,
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private route: ActivatedRoute) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
const fullUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}`;
|
||||||
|
this.authApi.availableProviders(fullUrl).subscribe(providers => {
|
||||||
|
this.updateDisabledProviders(providers);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.route.queryParamMap
|
||||||
|
.pipe(
|
||||||
|
switchMap(params => {
|
||||||
|
const result = params.get('result');
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
this.loading = false; // Нет результата, завершение загрузки
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.handleOAuthResult(result); // Обрабатываем результат
|
||||||
|
}),
|
||||||
|
catchError(_ => {
|
||||||
|
this.loading = false;
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleOAuthResult(result: string): Observable<any> {
|
||||||
|
switch (this.action) {
|
||||||
|
case OAuthAction.Login:
|
||||||
|
return this.authApi.loginOAuth(result).pipe(
|
||||||
|
tap(auth => {
|
||||||
|
this.oAuthLoginResult.emit(auth);
|
||||||
|
}),
|
||||||
|
finalize(() => {
|
||||||
|
this.loading = false;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
case OAuthAction.Bind:
|
||||||
|
if (this.isSetup) {
|
||||||
|
return this.setupApi.registerOAuth(result).pipe(
|
||||||
|
tap(() => {
|
||||||
|
this.oAuthUpdateProviders.emit();
|
||||||
|
}),
|
||||||
|
finalize(() => {
|
||||||
|
this.loading = false;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else
|
||||||
|
throw new Error('Action "Bind" requires setup mode to be enabled.');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error('Unknown action type for action ' + this.action);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateDisabledProviders(data: OAuthProviderData[] | null = null) {
|
private updateDisabledProviders(data: OAuthProviderData[] | null = null) {
|
||||||
this.providers = (data ?? this.providers).map(provider => {
|
this.providers = (data ?? this.providers).map(provider => {
|
||||||
return {
|
return {
|
||||||
...provider,
|
...provider,
|
||||||
disabled: this._activeProvidersId.includes(provider.provider) || this.activeProviders.includes(provider.providerName)
|
disabled: this._activeProvidersId.includes(provider.provider) || this._activeProviders.includes(provider.providerName),
|
||||||
|
active: false
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
protected openOAuth(provider: AvailableOAuthProviders) {
|
||||||
console.log(provider.redirect);
|
|
||||||
const oauthWindow = window.open(
|
const oauthWindow = window.open(
|
||||||
provider.redirect,
|
provider.redirect,
|
||||||
'_blank',
|
'_self'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!oauthWindow) {
|
if (!oauthWindow) {
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
<mat-card style="margin: 16px; padding: 16px;">
|
||||||
|
<mat-card-header style="margin-bottom: 15px;">
|
||||||
|
<mat-card-title>{{ title }}</mat-card-title>
|
||||||
|
</mat-card-header>
|
||||||
|
<mat-card-content>
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</mat-card-content>
|
||||||
|
<mat-card-actions style="display: flex; justify-content: end; margin-top: 15px;">
|
||||||
|
@if (isLoading) {
|
||||||
|
<app-data-spinner/>
|
||||||
|
} @else {
|
||||||
|
<button mat-raised-button color="accent" [disabled]="!isSaveEnabled" (click)="onSave()">
|
||||||
|
Сохранить
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</mat-card-actions>
|
||||||
|
</mat-card>
|
@ -0,0 +1,37 @@
|
|||||||
|
import {Component, EventEmitter, Input, Output} from '@angular/core';
|
||||||
|
import {MatCardModule} from "@angular/material/card";
|
||||||
|
import {MatButton} from "@angular/material/button";
|
||||||
|
import {catchError, Observable, tap} from "rxjs";
|
||||||
|
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-configuration-card',
|
||||||
|
imports: [
|
||||||
|
MatCardModule,
|
||||||
|
MatButton,
|
||||||
|
DataSpinnerComponent
|
||||||
|
],
|
||||||
|
templateUrl: './configuration-card.component.html'
|
||||||
|
})
|
||||||
|
export class ConfigurationCardComponent {
|
||||||
|
@Input() title: string = '';
|
||||||
|
@Input() isSaveEnabled: boolean = false;
|
||||||
|
@Input() saveFunction!: () => Observable<any>;
|
||||||
|
@Output() onSaveFunction = new EventEmitter<any>();
|
||||||
|
|
||||||
|
protected isLoading: boolean = false;
|
||||||
|
|
||||||
|
onSave(): void {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
const result = this.saveFunction().pipe(catchError(err => {
|
||||||
|
this.isLoading = false;
|
||||||
|
throw err;
|
||||||
|
}),
|
||||||
|
tap(_ => {
|
||||||
|
this.isLoading = false;
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.onSaveFunction.emit(result);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
<h2 mat-dialog-title>Удаление расписания при изменении значения</h2>
|
||||||
|
<mat-dialog-content>
|
||||||
|
<p>Вы хотите удалить старое расписание?</p>
|
||||||
|
</mat-dialog-content>
|
||||||
|
<mat-dialog-actions>
|
||||||
|
<button mat-button [mat-dialog-close]="false">Нет</button>
|
||||||
|
<button mat-button [mat-dialog-close]="true" color="warn">Да, удалить</button>
|
||||||
|
</mat-dialog-actions>
|
@ -0,0 +1,22 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {
|
||||||
|
MatDialogActions,
|
||||||
|
MatDialogClose,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogTitle
|
||||||
|
} from "@angular/material/dialog";
|
||||||
|
import {MatButton} from "@angular/material/button";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-confirm-delete-schedule-dialog',
|
||||||
|
imports: [
|
||||||
|
MatDialogTitle,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogActions,
|
||||||
|
MatDialogClose,
|
||||||
|
MatButton
|
||||||
|
],
|
||||||
|
templateUrl: './confirm-delete-schedule-dialog.component.html'
|
||||||
|
})
|
||||||
|
export class ConfirmDeleteScheduleDialogComponent {
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
<app-configuration-card [title]="'Cron для обновление расписания'"
|
||||||
|
[isSaveEnabled]="cronExpression != cronExpressionBefore"
|
||||||
|
[saveFunction]="saveFunction()"
|
||||||
|
(onSaveFunction)="onSave($event)">
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>cron</mat-label>
|
||||||
|
<input matInput type="text" [(ngModel)]="cronExpression"/>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<p>Следующие запуски:</p>
|
||||||
|
<ul>
|
||||||
|
@for (date of nextRunDates; track $index) {
|
||||||
|
<li>{{ date }}</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</app-configuration-card>
|
@ -0,0 +1,48 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {ConfigurationCardComponent} from "@component/admin/configuration-card/configuration-card.component";
|
||||||
|
import {ScheduleService} from "@api/v1/configuration/schedule.service";
|
||||||
|
import {MatInputModule} from "@angular/material/input";
|
||||||
|
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||||
|
import {Observable} from "rxjs";
|
||||||
|
import {CronUpdateScheduleResponse} from "@api/v1/configuration/cronUpdateScheduleResponse";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-cron-update-schedule',
|
||||||
|
imports: [
|
||||||
|
ConfigurationCardComponent,
|
||||||
|
MatInputModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
FormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './cron-update-schedule.component.html',
|
||||||
|
providers: [ScheduleService]
|
||||||
|
})
|
||||||
|
export class CronUpdateScheduleComponent {
|
||||||
|
protected nextRunDates: string[] = [];
|
||||||
|
protected cronExpression: string = '';
|
||||||
|
protected cronExpressionBefore: string = '';
|
||||||
|
|
||||||
|
constructor(private api: ScheduleService) {
|
||||||
|
api.getCronUpdateSchedule().subscribe(data => {
|
||||||
|
this.nextRunDates = data.nextStart?.map(x => this.convertDateToString(x)) ?? [];
|
||||||
|
this.cronExpression = data.cron;
|
||||||
|
this.cronExpressionBefore = data.cron;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertDateToString(data: Date): string {
|
||||||
|
data = new Date(data);
|
||||||
|
return data.toLocaleDateString() + ' ' + data.toLocaleTimeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected saveFunction() {
|
||||||
|
return () => this.api.postCronUpdateSchedule(this.cronExpression);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onSave(data: Observable<CronUpdateScheduleResponse>): void {
|
||||||
|
data.subscribe(apiData => {
|
||||||
|
this.nextRunDates = apiData.nextStart?.map(x => this.convertDateToString(x)) ?? [];
|
||||||
|
this.cronExpressionBefore = apiData.cron;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
<app-configuration-card
|
||||||
|
[title]="'Загрузка расписания Excel'"
|
||||||
|
[isSaveEnabled]="selectedFiles.length > 0"
|
||||||
|
[saveFunction]="saveFunction()"
|
||||||
|
(onSaveFunction)="onUpload($event)">
|
||||||
|
|
||||||
|
<input type="file" #fileInput (change)="onFileSelected($event)" multiple accept=".xlsx, .xls" style="display: none;">
|
||||||
|
|
||||||
|
@if (fileLoading) {
|
||||||
|
<app-data-spinner/>
|
||||||
|
} @else {
|
||||||
|
<button mat-raised-button color="primary" (click)="onFileChooseClick()">
|
||||||
|
Выберите файлы
|
||||||
|
<mat-icon>attach_file</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (selectedFiles.length > 0) {
|
||||||
|
<div style="margin-top: 15px;">
|
||||||
|
<p>Выбранные файлы:</p>
|
||||||
|
@for (item of selectedFiles; track $index) {
|
||||||
|
<p>
|
||||||
|
{{ item.file.name }}
|
||||||
|
</p>
|
||||||
|
<mat-form-field color="accent" style="margin-bottom: 18px;">
|
||||||
|
<mat-label>Кампус по умолчанию</mat-label>
|
||||||
|
<input matInput type="text" [(ngModel)]="item.campus"
|
||||||
|
[matAutocomplete]="auto">
|
||||||
|
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="onSelectCampus($event.option.value, item)">
|
||||||
|
@for (option of onFilter(item.campus); track $index) {
|
||||||
|
<mat-option [value]="option">
|
||||||
|
{{ option }}
|
||||||
|
</mat-option>
|
||||||
|
}
|
||||||
|
</mat-autocomplete>
|
||||||
|
</mat-form-field>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</app-configuration-card>
|
@ -0,0 +1,91 @@
|
|||||||
|
import {Component, ElementRef, ViewChild} from '@angular/core';
|
||||||
|
import {ConfigurationCardComponent} from "@component/admin/configuration-card/configuration-card.component";
|
||||||
|
import {MatDialog} from "@angular/material/dialog";
|
||||||
|
import {
|
||||||
|
ConfirmDeleteScheduleDialogComponent
|
||||||
|
} from "@component/admin/confirm-delete-schedule-dialog/confirm-delete-schedule-dialog.component";
|
||||||
|
import {Observable, switchMap} from "rxjs";
|
||||||
|
import {MatButtonModule} from "@angular/material/button";
|
||||||
|
import {MatIcon} from "@angular/material/icon";
|
||||||
|
import {ScheduleService} from "@api/v1/configuration/schedule.service";
|
||||||
|
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||||
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
|
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
|
||||||
|
import {MatInput} from "@angular/material/input";
|
||||||
|
import {ToastrService} from "ngx-toastr";
|
||||||
|
import {MatAutocomplete, MatAutocompleteTrigger, MatOption} from "@angular/material/autocomplete";
|
||||||
|
import {CampusService} from "@api/v1/campus.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-schedule-file-upload',
|
||||||
|
imports: [
|
||||||
|
ConfigurationCardComponent,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIcon,
|
||||||
|
DataSpinnerComponent,
|
||||||
|
MatFormFieldModule,
|
||||||
|
FormsModule,
|
||||||
|
MatInput,
|
||||||
|
MatAutocomplete,
|
||||||
|
MatAutocompleteTrigger,
|
||||||
|
MatOption,
|
||||||
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
|
templateUrl: './schedule-file-upload.component.html',
|
||||||
|
providers: [ScheduleService, CampusService]
|
||||||
|
})
|
||||||
|
export class ScheduleFileUploadComponent {
|
||||||
|
protected selectedFiles: { file: File, campus: string }[] = [];
|
||||||
|
protected fileLoading: boolean = false;
|
||||||
|
protected campuses: string[] = [];
|
||||||
|
@ViewChild('fileInput') input!: ElementRef;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private dialog: MatDialog,
|
||||||
|
private api: ScheduleService,
|
||||||
|
private notify: ToastrService,
|
||||||
|
campus: CampusService) {
|
||||||
|
campus.getCampus().subscribe(data => {
|
||||||
|
this.campuses = data.map(x => x.codeName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onSelectCampus(value: string, item: { file: File, campus: string }) {
|
||||||
|
item.campus = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected saveFunction() {
|
||||||
|
return () => {
|
||||||
|
const dialogRef = this.dialog.open(ConfirmDeleteScheduleDialogComponent);
|
||||||
|
|
||||||
|
return dialogRef.afterClosed().pipe(switchMap(result => {
|
||||||
|
return this.api.uploadScheduleFile(
|
||||||
|
this.selectedFiles.map(x => x.file),
|
||||||
|
this.selectedFiles.map(x => x.campus),
|
||||||
|
result);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onFilter(value: string): string[] {
|
||||||
|
const filterValue = value?.toLowerCase() || '';
|
||||||
|
return this.campuses.filter(campus => campus.toLowerCase().includes(filterValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onFileChooseClick() {
|
||||||
|
this.fileLoading = true;
|
||||||
|
this.input.nativeElement.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onFileSelected(event: any): void {
|
||||||
|
this.fileLoading = false;
|
||||||
|
this.selectedFiles = Array.from(event.target.files).map(file => ({file: <File>file, campus: ''}));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onUpload(data: Observable<any>): void {
|
||||||
|
data.subscribe(_ => {
|
||||||
|
this.notify.info(`Файлы в размере ${this.selectedFiles.length} успешно загружены. Задача поставлена в очередь`);
|
||||||
|
this.selectedFiles = [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
.date-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-form-field {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[mat-icon-button] {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
<app-configuration-card [title]="'Список пропуска обновления расписания'"
|
||||||
|
[isSaveEnabled]="validateSaveButton() && !isDisableAddNewItem()"
|
||||||
|
[saveFunction]="saveFunction()"
|
||||||
|
(onSaveFunction)="onSave($event)">
|
||||||
|
|
||||||
|
<div class="date-list">
|
||||||
|
@for (dateItem of dateItems; track $index) {
|
||||||
|
<div class="date-item">
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Диапазон дат</mat-label>
|
||||||
|
<mat-date-range-input [rangePicker]="rangePicker"
|
||||||
|
[disabled]="dateItems[$index].date">
|
||||||
|
<input matStartDate [(ngModel)]="dateItem.start"
|
||||||
|
placeholder="Начало"
|
||||||
|
(dateChange)="validateDate($index)"
|
||||||
|
[min]="CurrentDate">
|
||||||
|
<input matEndDate [(ngModel)]="dateItem.end"
|
||||||
|
placeholder="Конец"
|
||||||
|
(dateChange)="validateDate($index)"
|
||||||
|
[min]="CurrentDate">
|
||||||
|
</mat-date-range-input>
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="rangePicker"></mat-datepicker-toggle>
|
||||||
|
<mat-date-range-picker #rangePicker></mat-date-range-picker>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Конкретная дата</mat-label>
|
||||||
|
<input matInput [matDatepicker]="specificDatePicker"
|
||||||
|
[(ngModel)]="dateItem.date"
|
||||||
|
(dateChange)="validateDate($index)"
|
||||||
|
[min]="CurrentDate"
|
||||||
|
[disabled]="dateItems[$index].start != null || dateItems[$index].end != null">
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="specificDatePicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #specificDatePicker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<button mat-icon-button color="warn" (click)="removeDate($index)" style="height: 100%;">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button mat-raised-button color="accent"
|
||||||
|
[disabled]="isDisableAddNewItem()"
|
||||||
|
(click)="addDate()">
|
||||||
|
<mat-icon>add</mat-icon>
|
||||||
|
Добавить строку
|
||||||
|
</button>
|
||||||
|
</app-configuration-card>
|
@ -0,0 +1,93 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
|
import {MatDatepickerModule} from "@angular/material/datepicker";
|
||||||
|
import {MatInput} from "@angular/material/input";
|
||||||
|
import {FormsModule} from "@angular/forms";
|
||||||
|
import {ConfigurationCardComponent} from "@component/admin/configuration-card/configuration-card.component";
|
||||||
|
import {MatButtonModule} from "@angular/material/button";
|
||||||
|
import {MatIcon} from "@angular/material/icon";
|
||||||
|
import {ScheduleService} from "@api/v1/configuration/schedule.service";
|
||||||
|
import CronUpdateSkip from "@model/cronUpdateSkip";
|
||||||
|
import {DateOnly} from "@model/dateOnly";
|
||||||
|
import {addDays} from "@progress/kendo-date-math";
|
||||||
|
import {Observable} from "rxjs";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-skip-update-schedule',
|
||||||
|
imports: [
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatDatepickerModule,
|
||||||
|
MatInput,
|
||||||
|
FormsModule,
|
||||||
|
ConfigurationCardComponent,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIcon
|
||||||
|
],
|
||||||
|
templateUrl: './skip-update-schedule.component.html',
|
||||||
|
styleUrl: './skip-update-schedule.component.css',
|
||||||
|
providers: [ScheduleService]
|
||||||
|
})
|
||||||
|
export class SkipUpdateScheduleComponent {
|
||||||
|
dateItems: { start?: Date, end?: Date, date?: Date }[] = [];
|
||||||
|
dateItemsBefore: { start?: Date, end?: Date, date?: Date }[] = [];
|
||||||
|
|
||||||
|
constructor(private api: ScheduleService) {
|
||||||
|
api.getCronUpdateSkip().subscribe(data => {
|
||||||
|
this.dateItems = data.map(x => <{ start?: Date, end?: Date, date?: Date }>{
|
||||||
|
start: x.start?.date,
|
||||||
|
end: x.end?.date,
|
||||||
|
date: x.date?.date
|
||||||
|
});
|
||||||
|
if (this.dateItems.length == 0)
|
||||||
|
this.addDate();
|
||||||
|
|
||||||
|
this.dateItemsBefore = JSON.parse(JSON.stringify(this.dateItems));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addDate(): void {
|
||||||
|
this.dateItems.push({start: undefined, end: undefined, date: undefined});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeDate(index: number): void {
|
||||||
|
this.dateItems.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateDate(index: number): void {
|
||||||
|
const item = this.dateItems[index];
|
||||||
|
|
||||||
|
if (item.start && item.start < this.CurrentDate)
|
||||||
|
item.start = undefined;
|
||||||
|
if (item.end && item.end < this.CurrentDate)
|
||||||
|
item.end = undefined;
|
||||||
|
if (item.date && item.date < this.CurrentDate)
|
||||||
|
item.date = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDisableAddNewItem() {
|
||||||
|
return this.dateItems.some(item => (!item.start || !item.end) && !item.date);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateSaveButton(): boolean {
|
||||||
|
return (this.dateItems.length == 0 || this.dateItems.some(item =>
|
||||||
|
(item.start && item.end) || item.date
|
||||||
|
)) && JSON.stringify(this.dateItems) != JSON.stringify(this.dateItemsBefore);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveFunction() {
|
||||||
|
return () => this.api.postCronUpdateSkip(this.dateItems.map(x =>
|
||||||
|
<CronUpdateSkip>{
|
||||||
|
start: x.start ? new DateOnly(x.start) : undefined,
|
||||||
|
end: x.end ? new DateOnly(x.end) : undefined,
|
||||||
|
date: x.date ? new DateOnly(x.date) : undefined
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
onSave(event: Observable<any>): void {
|
||||||
|
event.subscribe(_ => {
|
||||||
|
this.dateItemsBefore = JSON.parse(JSON.stringify(this.dateItems));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected CurrentDate: Date = addDays(new Date(), -1);
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
<app-configuration-card
|
||||||
|
[title]="'Дата начала семестра'"
|
||||||
|
[isSaveEnabled]="startDate != startDateBefore"
|
||||||
|
[saveFunction]="saveFunction()"
|
||||||
|
(onSaveFunction)="onSave($event)">
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Дата начала семестра</mat-label>
|
||||||
|
<input matInput [matDatepicker]="datePicker" [(ngModel)]="startDate" [min]="ValidMinDate">
|
||||||
|
<mat-datepicker-toggle matSuffix [for]="datePicker"></mat-datepicker-toggle>
|
||||||
|
<mat-datepicker #datePicker></mat-datepicker>
|
||||||
|
</mat-form-field>
|
||||||
|
</app-configuration-card>
|
@ -0,0 +1,61 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
|
import {MatDatepicker, MatDatepickerInput, MatDatepickerToggle} from "@angular/material/datepicker";
|
||||||
|
import {FormsModule} from "@angular/forms";
|
||||||
|
import {ConfigurationCardComponent} from "@component/admin/configuration-card/configuration-card.component";
|
||||||
|
import {MatInput} from "@angular/material/input";
|
||||||
|
import {addDays} from "@progress/kendo-date-math";
|
||||||
|
import {ScheduleService} from "@api/v1/configuration/schedule.service";
|
||||||
|
import {DateOnly} from "@model/dateOnly";
|
||||||
|
import {MatDialog} from "@angular/material/dialog";
|
||||||
|
import {
|
||||||
|
ConfirmDeleteScheduleDialogComponent
|
||||||
|
} from "@component/admin/confirm-delete-schedule-dialog/confirm-delete-schedule-dialog.component";
|
||||||
|
import {Observable, switchMap} from "rxjs";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-term-start-date',
|
||||||
|
imports: [
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatDatepickerToggle,
|
||||||
|
FormsModule,
|
||||||
|
MatDatepickerInput,
|
||||||
|
MatDatepicker,
|
||||||
|
ConfigurationCardComponent,
|
||||||
|
MatInput
|
||||||
|
],
|
||||||
|
templateUrl: './term-start-date.component.html',
|
||||||
|
providers: [ScheduleService]
|
||||||
|
})
|
||||||
|
export class TermStartDateComponent {
|
||||||
|
protected startDate: Date = new Date();
|
||||||
|
protected startDateBefore: Date = new Date();
|
||||||
|
|
||||||
|
constructor(private api: ScheduleService, private dialog: MatDialog) {
|
||||||
|
this.api.getStartTerm().subscribe(data => {
|
||||||
|
this.startDate = data.date;
|
||||||
|
this.startDateBefore = this.startDate;
|
||||||
|
console.log(this.startDate == this.startDateBefore);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected saveFunction() {
|
||||||
|
return () => {
|
||||||
|
const dialogRef = this.dialog.open(ConfirmDeleteScheduleDialogComponent, {
|
||||||
|
data: {startDate: this.startDate}
|
||||||
|
});
|
||||||
|
|
||||||
|
return dialogRef.afterClosed().pipe(switchMap(result => {
|
||||||
|
return this.api.postStartTerm(new DateOnly(this.startDate), result);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onSave(data: Observable<any>): void {
|
||||||
|
data.subscribe(_ => {
|
||||||
|
this.startDateBefore = this.startDate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ValidMinDate = addDays(new Date(), -180);
|
||||||
|
}
|
@ -1,10 +0,0 @@
|
|||||||
.notification-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-button {
|
|
||||||
margin-left: 8px;
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
<div class="notification-content" [class]="data.className">
|
|
||||||
<span>
|
|
||||||
{{ data.message }}
|
|
||||||
</span>
|
|
||||||
<button mat-icon-button class="close-button" (click)="dismiss()">
|
|
||||||
<mat-icon>close</mat-icon>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
@if (showProgressBar) {
|
|
||||||
<mat-progress-bar mode="determinate" [value]="progress" [color]="color"/>
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
import {Component, Inject} from '@angular/core';
|
|
||||||
import {MatIcon} from "@angular/material/icon";
|
|
||||||
import {MatProgressBar} from "@angular/material/progress-bar";
|
|
||||||
import {MAT_SNACK_BAR_DATA, MatSnackBarRef} from "@angular/material/snack-bar";
|
|
||||||
import {MatIconButton} from "@angular/material/button";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-notification',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
MatIconButton,
|
|
||||||
MatIcon,
|
|
||||||
MatProgressBar
|
|
||||||
],
|
|
||||||
templateUrl: './notification.component.html',
|
|
||||||
styleUrl: './notification.component.css'
|
|
||||||
})
|
|
||||||
|
|
||||||
export class NotificationComponent {
|
|
||||||
showProgressBar: boolean = false;
|
|
||||||
progress: number = 100;
|
|
||||||
color: string = "primary";
|
|
||||||
|
|
||||||
constructor(@Inject(MAT_SNACK_BAR_DATA) public data: any, private snackBarRef: MatSnackBarRef<NotificationComponent>) {
|
|
||||||
if (data.duration) {
|
|
||||||
this.startProgress(data.duration);
|
|
||||||
this.showProgressBar = true;
|
|
||||||
}
|
|
||||||
if (data.color) {
|
|
||||||
this.color = data.color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dismiss(): void {
|
|
||||||
this.snackBarRef.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
private startProgress(duration: number): void {
|
|
||||||
const interval: number = duration / 100;
|
|
||||||
const progressInterval = setInterval(async () => {
|
|
||||||
this.progress--;
|
|
||||||
if (this.progress === 0) {
|
|
||||||
clearInterval(progressInterval);
|
|
||||||
setTimeout(() => {
|
|
||||||
this.dismiss();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
}, interval);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,45 @@
|
|||||||
|
<div [formGroup]="formGroup" style="display: flex; flex-direction: column; align-items: stretch;">
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Пароль</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите пароль"
|
||||||
|
formControlName="password"
|
||||||
|
required
|
||||||
|
[type]="hidePass ? 'password' : 'text'"
|
||||||
|
id="passwordNextFocus"
|
||||||
|
focusNext="focusNext">
|
||||||
|
|
||||||
|
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
|
||||||
|
[attr.aria-pressed]="hidePass">
|
||||||
|
<mat-icon>{{ hidePass ? 'visibility_off' : 'visibility' }}</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (formGroup.get('password')?.hasError('required')) {
|
||||||
|
<mat-error>
|
||||||
|
Пароль является <i>обязательным</i>
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (formGroup.get('password')?.hasError('minlength')) {
|
||||||
|
<mat-error>
|
||||||
|
Пароль должен быть не менее {{ policy.minimumLength }} символов
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (formGroup.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>
|
||||||
|
</div>
|
@ -0,0 +1,76 @@
|
|||||||
|
import {Component, Input} from '@angular/core';
|
||||||
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
|
import {MatInput} from "@angular/material/input";
|
||||||
|
import {MatIconButton} from "@angular/material/button";
|
||||||
|
import {FormGroup, ReactiveFormsModule, ValidatorFn, Validators} from "@angular/forms";
|
||||||
|
import {MatSelectModule} from "@angular/material/select";
|
||||||
|
import {MatTooltip} from "@angular/material/tooltip";
|
||||||
|
import {MatIcon} from "@angular/material/icon";
|
||||||
|
import {PasswordPolicy} from "@model/passwordPolicy";
|
||||||
|
import SecurityService from "@api/v1/securityService";
|
||||||
|
import {FocusNextDirective} from "@/directives/focus-next.directive";
|
||||||
|
import SetupService from "@api/v1/setup.service";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'password-input',
|
||||||
|
imports: [
|
||||||
|
ReactiveFormsModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatInput,
|
||||||
|
MatTooltip,
|
||||||
|
MatIconButton,
|
||||||
|
MatIcon,
|
||||||
|
FocusNextDirective
|
||||||
|
],
|
||||||
|
templateUrl: './password-input.component.html',
|
||||||
|
providers: [SecurityService, SetupService]
|
||||||
|
})
|
||||||
|
export class PasswordInputComponent {
|
||||||
|
protected hidePass = true;
|
||||||
|
protected policy!: PasswordPolicy;
|
||||||
|
@Input() formGroup!: FormGroup;
|
||||||
|
@Input() focusNext: string | undefined;
|
||||||
|
@Input() isSetupMode: boolean = false;
|
||||||
|
|
||||||
|
constructor(securityApi: SecurityService, setupApi: SetupService) {
|
||||||
|
if (this.isSetupMode)
|
||||||
|
setupApi.passwordPolicyConfiguration().subscribe(policy => {
|
||||||
|
this.policy = policy;
|
||||||
|
this.updateValueAndValidity(policy);
|
||||||
|
});
|
||||||
|
else
|
||||||
|
securityApi.passwordPolicy().subscribe(policy => {
|
||||||
|
this.policy = policy;
|
||||||
|
this.updateValueAndValidity(policy);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateValueAndValidity(policy: PasswordPolicy): void {
|
||||||
|
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(/[!@#$%^&*(),.?":{}|<>]/));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.formGroup.get('password')?.setValidators(validators);
|
||||||
|
this.formGroup.get('password')?.updateValueAndValidity();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected togglePassword(event: MouseEvent) {
|
||||||
|
this.hidePass = !this.hidePass;
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,7 @@
|
|||||||
<app-other idButton="lecture-button" textButton="Кабинеты" #lecture (retryLoadData)="loadLectureHalls()"/>
|
<app-other idButton="lecture-button" textButton="Кабинеты" #lecture (retryLoadData)="loadLectureHalls()"/>
|
||||||
<app-other idButton="group-button" textButton="Группы" #group (retryLoadData)="loadGroups()"/>
|
<app-other idButton="group-button" textButton="Группы" #group (retryLoadData)="loadGroups()"/>
|
||||||
<app-other idButton="professor-button" textButton="Профессоры" #professor (retryLoadData)="loadProfessors()"/>
|
<app-other idButton="professor-button" textButton="Профессоры" #professor (retryLoadData)="loadProfessors()"/>
|
||||||
|
<app-other idButton="lesson-type-button" textButton="Тип занятия" #lesson_type (retryLoadData)="loadLessonType()"/>
|
||||||
<section>
|
<section>
|
||||||
<button mat-flat-button (click)="otherFilter()">Отфильтровать</button>
|
<button mat-flat-button (click)="otherFilter()">Отфильтровать</button>
|
||||||
</section>
|
</section>
|
||||||
|
@ -18,6 +18,8 @@ import {AuthRoles} from "@model/authRoles";
|
|||||||
import {HasRoleDirective} from "@/directives/has-role.directive";
|
import {HasRoleDirective} from "@/directives/has-role.directive";
|
||||||
import {TabSelectType, TabStorageService} from "@service/tab-storage.service";
|
import {TabSelectType, TabStorageService} from "@service/tab-storage.service";
|
||||||
import {ScheduleRequest} from "@api/v1/scheduleRequest";
|
import {ScheduleRequest} from "@api/v1/scheduleRequest";
|
||||||
|
import {CampusService} from "@api/v1/campus.service";
|
||||||
|
import {LessonTypeService} from "@api/v1/lessonType.service";
|
||||||
|
|
||||||
export enum TabsSelect {
|
export enum TabsSelect {
|
||||||
Group,
|
Group,
|
||||||
@ -43,7 +45,15 @@ export enum TabsSelect {
|
|||||||
],
|
],
|
||||||
templateUrl: './tabs.component.html',
|
templateUrl: './tabs.component.html',
|
||||||
styleUrl: './tabs.component.css',
|
styleUrl: './tabs.component.css',
|
||||||
providers: [ScheduleService, DisciplineService, LectureHallService, GroupService, ProfessorService, TabStorageService]
|
providers: [
|
||||||
|
ScheduleService,
|
||||||
|
DisciplineService,
|
||||||
|
LectureHallService,
|
||||||
|
GroupService,
|
||||||
|
ProfessorService,
|
||||||
|
TabStorageService,
|
||||||
|
CampusService,
|
||||||
|
LessonTypeService]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class TabsComponent implements AfterViewInit {
|
export class TabsComponent implements AfterViewInit {
|
||||||
@ -55,7 +65,9 @@ export class TabsComponent implements AfterViewInit {
|
|||||||
private lectureApi: LectureHallService,
|
private lectureApi: LectureHallService,
|
||||||
private groupApi: GroupService,
|
private groupApi: GroupService,
|
||||||
private professorApi: ProfessorService,
|
private professorApi: ProfessorService,
|
||||||
private tabStorage: TabStorageService) {
|
private tabStorage: TabStorageService,
|
||||||
|
private campusApi: CampusService,
|
||||||
|
private lessonTypeApi: LessonTypeService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
@ -142,6 +154,7 @@ export class TabsComponent implements AfterViewInit {
|
|||||||
await this.loadLectureHalls();
|
await this.loadLectureHalls();
|
||||||
await this.loadGroups();
|
await this.loadGroups();
|
||||||
await this.loadProfessors();
|
await this.loadProfessors();
|
||||||
|
await this.loadLessonType();
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
await this.chooseTabs(0);
|
await this.chooseTabs(0);
|
||||||
@ -159,12 +172,14 @@ export class TabsComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async loadLectureHalls() {
|
protected async loadLectureHalls() {
|
||||||
|
this.campusApi.getCampus().subscribe(campus => {
|
||||||
this.lectureApi.getLectureHalls().subscribe(data => {
|
this.lectureApi.getLectureHalls().subscribe(data => {
|
||||||
this.lectureHallEx.Data = data.map(x => ({
|
this.lectureHallEx.Data = data.map(x => ({
|
||||||
id: x.id,
|
id: x.id,
|
||||||
name: x.name
|
name: x.name + ` (${campus.find(c => c.id == x.campusId)?.codeName})`
|
||||||
}) as SelectData);
|
}) as SelectData);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async loadGroups() {
|
protected async loadGroups() {
|
||||||
@ -185,6 +200,15 @@ export class TabsComponent implements AfterViewInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async loadLessonType() {
|
||||||
|
this.lessonTypeApi.getLessonTypes().subscribe(data => {
|
||||||
|
this.lessonTypeEx.Data = data.map(x => ({
|
||||||
|
id: x.id,
|
||||||
|
name: x.name
|
||||||
|
}) as SelectData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@ViewChild('groupTab') groupTab!: IScheduleTab;
|
@ViewChild('groupTab') groupTab!: IScheduleTab;
|
||||||
@ViewChild('professorTab') professorTab!: IScheduleTab;
|
@ViewChild('professorTab') professorTab!: IScheduleTab;
|
||||||
@ViewChild('lectureHallTab') lectureHallTab!: IScheduleTab;
|
@ViewChild('lectureHallTab') lectureHallTab!: IScheduleTab;
|
||||||
@ -193,6 +217,7 @@ export class TabsComponent implements AfterViewInit {
|
|||||||
@ViewChild('lecture') lectureHallEx!: OtherComponent;
|
@ViewChild('lecture') lectureHallEx!: OtherComponent;
|
||||||
@ViewChild('group') groupEx!: OtherComponent;
|
@ViewChild('group') groupEx!: OtherComponent;
|
||||||
@ViewChild('professor') professorEx!: OtherComponent;
|
@ViewChild('professor') professorEx!: OtherComponent;
|
||||||
|
@ViewChild('lesson_type') lessonTypeEx!: OtherComponent;
|
||||||
|
|
||||||
@ViewChild('tabGroup') tabs!: MatTabGroup;
|
@ViewChild('tabGroup') tabs!: MatTabGroup;
|
||||||
protected readonly AuthRoles = AuthRoles;
|
protected readonly AuthRoles = AuthRoles;
|
||||||
@ -202,7 +227,8 @@ export class TabsComponent implements AfterViewInit {
|
|||||||
groups: this.groupEx.selectedIds,
|
groups: this.groupEx.selectedIds,
|
||||||
disciplines: this.disciplineEx.selectedIds,
|
disciplines: this.disciplineEx.selectedIds,
|
||||||
professors: this.professorEx.selectedIds,
|
professors: this.professorEx.selectedIds,
|
||||||
lectureHalls: this.lectureHallEx.selectedIds
|
lectureHalls: this.lectureHallEx.selectedIds,
|
||||||
|
lessonType: this.lessonTypeEx.selectedIds
|
||||||
});
|
});
|
||||||
|
|
||||||
this.eventResult.emit(
|
this.eventResult.emit(
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core';
|
import {Directive, Input, TemplateRef, ViewContainerRef} from '@angular/core';
|
||||||
import AuthApiService from "@api/v1/authApiService";
|
import AuthApiService from "@api/v1/authApi.service";
|
||||||
import {AuthRoles} from "@model/authRoles";
|
import {AuthRoles} from "@model/authRoles";
|
||||||
import {catchError, of} from "rxjs";
|
import {catchError, of} from "rxjs";
|
||||||
|
|
||||||
|
28
src/pages/admin/admin.component.css
Normal file
28
src/pages/admin/admin.component.css
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
mat-sidenav-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-sidenav {
|
||||||
|
width: auto;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 20vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-nav-list a {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
|
mat-icon {
|
||||||
|
margin-right: 16px;
|
||||||
|
font-size: 24px;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-link {
|
||||||
|
backdrop-filter: contrast(75%);
|
||||||
|
}
|
21
src/pages/admin/admin.component.html
Normal file
21
src/pages/admin/admin.component.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<mat-card *appHasRole="AuthRoles.Admin">
|
||||||
|
<mat-sidenav-container>
|
||||||
|
<mat-sidenav mode="side" opened>
|
||||||
|
<mat-nav-list>
|
||||||
|
@for (link of navLinks; track $index) {
|
||||||
|
<a
|
||||||
|
mat-list-item
|
||||||
|
[class.active-link]="isActive(link.route)"
|
||||||
|
(click)="navigate(link.route)"
|
||||||
|
[disabled]="isActive(link.route)">
|
||||||
|
<mat-icon>{{ link.icon }}</mat-icon>
|
||||||
|
<span>{{ link.label }}</span>
|
||||||
|
</a>
|
||||||
|
}
|
||||||
|
</mat-nav-list>
|
||||||
|
</mat-sidenav>
|
||||||
|
<mat-sidenav-content>
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
</mat-sidenav-content>
|
||||||
|
</mat-sidenav-container>
|
||||||
|
</mat-card>
|
59
src/pages/admin/admin.component.ts
Normal file
59
src/pages/admin/admin.component.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {MatCard} from "@angular/material/card";
|
||||||
|
import {MatSidenavModule} from "@angular/material/sidenav";
|
||||||
|
import {HasRoleDirective} from "@/directives/has-role.directive";
|
||||||
|
import {Router, RouterOutlet} from "@angular/router";
|
||||||
|
import AuthApiService from "@api/v1/authApi.service";
|
||||||
|
import {MatListItem, MatNavList} from "@angular/material/list";
|
||||||
|
import {AuthRoles} from "@model/authRoles";
|
||||||
|
import {MatIcon} from "@angular/material/icon";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-admin',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
MatCard,
|
||||||
|
HasRoleDirective,
|
||||||
|
MatNavList,
|
||||||
|
MatSidenavModule,
|
||||||
|
RouterOutlet,
|
||||||
|
MatListItem,
|
||||||
|
MatIcon,
|
||||||
|
],
|
||||||
|
templateUrl: './admin.component.html',
|
||||||
|
styleUrl: './admin.component.css',
|
||||||
|
providers: [AuthApiService]
|
||||||
|
})
|
||||||
|
export class AdminComponent {
|
||||||
|
navLinks = [
|
||||||
|
{label: 'Расписание', route: 'schedule', icon: 'calendar_month'},
|
||||||
|
{label: 'Институт', route: 'institute', icon: 'school'},
|
||||||
|
{label: 'Аккаунт', route: 'account', icon: 'person'},
|
||||||
|
{label: 'Сервер', route: 'server', icon: 'settings'},
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(private auth: AuthApiService, private router: Router) {
|
||||||
|
this.auth.getRole()
|
||||||
|
.subscribe(data => {
|
||||||
|
if (data === null)
|
||||||
|
router.navigate(['login']).then();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive(route: string): boolean {
|
||||||
|
return this.router.isActive(`/admin/${route}`, {
|
||||||
|
paths: 'exact',
|
||||||
|
queryParams: 'ignored',
|
||||||
|
fragment: 'ignored',
|
||||||
|
matrixParams: 'ignored',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(route: string): void {
|
||||||
|
if (!this.isActive(route)) {
|
||||||
|
this.router.navigate([`/admin/${route}`]).then();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly AuthRoles = AuthRoles;
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
/* Основной контейнер */
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > * {
|
||||||
|
flex: 1 1 calc(50% - 16px);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > :first-child:nth-last-child(1),
|
||||||
|
.container > :first-child:nth-last-child(1) ~ * {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1500px) {
|
||||||
|
.container > * {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
<div>
|
||||||
|
<h2 style="margin: 15px;">Конфигурация расписания</h2>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<app-term-start-date></app-term-start-date>
|
||||||
|
<app-schedule-file-upload></app-schedule-file-upload>
|
||||||
|
<app-cron-update-schedule></app-cron-update-schedule>
|
||||||
|
<app-skip-update-schedule></app-skip-update-schedule>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,20 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import {CronUpdateScheduleComponent} from "@component/admin/cron-update-schedule/cron-update-schedule.component";
|
||||||
|
import {SkipUpdateScheduleComponent} from "@component/admin/skip-update-schedule/skip-update-schedule.component";
|
||||||
|
import {TermStartDateComponent} from "@component/admin/term-start-date/term-start-date.component";
|
||||||
|
import {ScheduleFileUploadComponent} from "@component/admin/schedule-file-upload/schedule-file-upload.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-schedule-configuration',
|
||||||
|
imports: [
|
||||||
|
CronUpdateScheduleComponent,
|
||||||
|
SkipUpdateScheduleComponent,
|
||||||
|
TermStartDateComponent,
|
||||||
|
ScheduleFileUploadComponent
|
||||||
|
],
|
||||||
|
templateUrl: './schedule-configuration.component.html',
|
||||||
|
styleUrl: './schedule-configuration.component.css'
|
||||||
|
})
|
||||||
|
export class ScheduleConfigurationComponent {
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
.under-construction {
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.under-construction h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.under-construction p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
@ -0,0 +1,4 @@
|
|||||||
|
<div class="under-construction">
|
||||||
|
<h1>Страница находится в разработке</h1>
|
||||||
|
<p>Пожалуйста, зайдите позже.</p>
|
||||||
|
</div>
|
@ -0,0 +1,11 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-under-construction',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './under-construction.component.html',
|
||||||
|
styleUrl: './under-construction.component.css'
|
||||||
|
})
|
||||||
|
export class UnderConstructionComponent {
|
||||||
|
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
<mat-sidenav-container class="formLogin">
|
<mat-sidenav-container class="formLogin">
|
||||||
|
|
||||||
<mat-card>
|
<mat-card>
|
||||||
<p class="mat-h3">
|
<p class="mat-h3">
|
||||||
Вход в систему
|
Вход в систему
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form [formGroup]="loginForm">
|
<form [formGroup]="loginForm">
|
||||||
|
@if (!requiresTwoFactorAuth) {
|
||||||
<mat-form-field color="accent">
|
<mat-form-field color="accent">
|
||||||
<mat-label>Имя пользователя/email</mat-label>
|
<mat-label>Имя пользователя/email</mat-label>
|
||||||
<input matInput
|
<input matInput
|
||||||
@ -27,35 +27,28 @@
|
|||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field color="accent" style="margin-bottom: 20px">
|
<password-input [focusNext]="'loginNextFocus'" [formGroup]="loginForm"/>
|
||||||
<mat-label>Пароль</mat-label>
|
} @else {
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Код 2FA</mat-label>
|
||||||
<input matInput
|
<input matInput
|
||||||
matTooltip="Укажите пароль"
|
formControlName="twoFactorCode"
|
||||||
formControlName="password"
|
matTooltip="Введите код из приложения"
|
||||||
required
|
required
|
||||||
[type]="hidePass ? 'password' : 'text'"
|
|
||||||
id="passwordNextFocus"
|
|
||||||
focusNext="loginNextFocus">
|
focusNext="loginNextFocus">
|
||||||
|
@if (loginForm.get('twoFactorCode')?.hasError('required')) {
|
||||||
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
|
|
||||||
[attr.aria-pressed]="hidePass">
|
|
||||||
<mat-icon>{{ hidePass ? 'visibility_off' : 'visibility' }}</mat-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if (loginForm.get('password')?.hasError('required')) {
|
|
||||||
<mat-error>
|
<mat-error>
|
||||||
Пароль является <i>обязательным</i>
|
Код 2FA обязателен.
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (loginForm.get('password')?.hasError('minlength')) {
|
|
||||||
<mat-error>
|
|
||||||
Пароль должен быть не менее 8 символов
|
|
||||||
</mat-error>
|
</mat-error>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
@if (!requiresTwoFactorAuth) {
|
||||||
|
<OAuthProviders (oAuthLoginResult)="loginOAuth($event)"/>
|
||||||
|
}
|
||||||
|
|
||||||
<mat-error>
|
<mat-error>
|
||||||
{{ errorText }}
|
{{ errorText }}
|
||||||
</mat-error>
|
</mat-error>
|
||||||
@ -66,7 +59,7 @@
|
|||||||
} @else {
|
} @else {
|
||||||
<button mat-flat-button color="accent"
|
<button mat-flat-button color="accent"
|
||||||
[disabled]="loginButtonIsDisable"
|
[disabled]="loginButtonIsDisable"
|
||||||
(click)="login()"
|
(click)="requiresTwoFactorAuth ? login2Fa() : login()"
|
||||||
id="loginNextFocus">
|
id="loginNextFocus">
|
||||||
Войти
|
Войти
|
||||||
</button>
|
</button>
|
||||||
|
@ -3,15 +3,17 @@ import {MatSidenavContainer} from "@angular/material/sidenav";
|
|||||||
import {MatFormFieldModule} from "@angular/material/form-field";
|
import {MatFormFieldModule} from "@angular/material/form-field";
|
||||||
import {MatInput} from "@angular/material/input";
|
import {MatInput} from "@angular/material/input";
|
||||||
import {MatTooltip} from "@angular/material/tooltip";
|
import {MatTooltip} from "@angular/material/tooltip";
|
||||||
import {MatIcon} from "@angular/material/icon";
|
import {MatButton} from "@angular/material/button";
|
||||||
import {MatButton, MatIconButton} from "@angular/material/button";
|
|
||||||
import {MatCard} from "@angular/material/card";
|
import {MatCard} from "@angular/material/card";
|
||||||
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
import {FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||||
import {FocusNextDirective} from "@/directives/focus-next.directive";
|
import {FocusNextDirective} from "@/directives/focus-next.directive";
|
||||||
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
|
||||||
import AuthApiService from "@api/v1/authApiService";
|
import AuthApiService from "@api/v1/authApi.service";
|
||||||
import {Router} from "@angular/router";
|
import {Router} from "@angular/router";
|
||||||
import {catchError} from "rxjs";
|
import {catchError} from "rxjs";
|
||||||
|
import {TwoFactorAuthentication} from "@model/twoFactorAuthentication";
|
||||||
|
import {PasswordInputComponent} from "@component/common/password-input/password-input.component";
|
||||||
|
import {OAuthProviders} from "@component/OAuthProviders/OAuthProviders";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-login',
|
selector: 'app-login',
|
||||||
@ -21,13 +23,13 @@ import {catchError} from "rxjs";
|
|||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInput,
|
MatInput,
|
||||||
MatTooltip,
|
MatTooltip,
|
||||||
MatIcon,
|
|
||||||
MatIconButton,
|
|
||||||
MatButton,
|
MatButton,
|
||||||
MatCard,
|
MatCard,
|
||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
FocusNextDirective,
|
FocusNextDirective,
|
||||||
DataSpinnerComponent
|
DataSpinnerComponent,
|
||||||
|
PasswordInputComponent,
|
||||||
|
OAuthProviders
|
||||||
],
|
],
|
||||||
templateUrl: './login.component.html',
|
templateUrl: './login.component.html',
|
||||||
styleUrl: './login.component.css',
|
styleUrl: './login.component.css',
|
||||||
@ -35,10 +37,10 @@ import {catchError} from "rxjs";
|
|||||||
})
|
})
|
||||||
export class LoginComponent {
|
export class LoginComponent {
|
||||||
protected loginForm!: FormGroup;
|
protected loginForm!: FormGroup;
|
||||||
protected hidePass: boolean = true;
|
|
||||||
protected loaderActive: boolean = false;
|
protected loaderActive: boolean = false;
|
||||||
protected loginButtonIsDisable: boolean = true;
|
protected loginButtonIsDisable: boolean = true;
|
||||||
protected errorText: string = '';
|
protected errorText: string = '';
|
||||||
|
protected requiresTwoFactorAuth: boolean = false;
|
||||||
|
|
||||||
constructor(private formBuilder: FormBuilder, private auth: AuthApiService, private router: Router) {
|
constructor(private formBuilder: FormBuilder, private auth: AuthApiService, private router: Router) {
|
||||||
this.auth.getRole()
|
this.auth.getRole()
|
||||||
@ -68,9 +70,20 @@ export class LoginComponent {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected togglePassword(event: MouseEvent) {
|
private updateTwoFactorValidation(data: TwoFactorAuthentication) {
|
||||||
this.hidePass = !this.hidePass;
|
if (data === TwoFactorAuthentication.None) {
|
||||||
event.stopPropagation();
|
this.router.navigate(['admin']).then();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.requiresTwoFactorAuth = true;
|
||||||
|
this.loginForm.addControl(
|
||||||
|
'twoFactorCode',
|
||||||
|
new FormControl('', Validators.required)
|
||||||
|
);
|
||||||
|
this.loginForm.removeControl('user');
|
||||||
|
this.loginForm.removeControl('password');
|
||||||
|
this.loginButtonIsDisable = !this.loginForm.valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected login() {
|
protected login() {
|
||||||
@ -82,7 +95,27 @@ export class LoginComponent {
|
|||||||
})
|
})
|
||||||
.pipe(catchError(error => {
|
.pipe(catchError(error => {
|
||||||
this.loaderActive = false;
|
this.loaderActive = false;
|
||||||
this.errorText = error.error instanceof String ? error.error : error.statusText;
|
this.errorText = error.error.detail;
|
||||||
|
this.loginButtonIsDisable = true;
|
||||||
|
throw error;
|
||||||
|
}))
|
||||||
|
.subscribe(x => {
|
||||||
|
this.loaderActive = false;
|
||||||
|
this.errorText = '';
|
||||||
|
this.updateTwoFactorValidation(x);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected login2Fa() {
|
||||||
|
this.loaderActive = true;
|
||||||
|
|
||||||
|
this.auth.twoFactorAuth({
|
||||||
|
code: this.loginForm.get('twoFactorCode')?.value,
|
||||||
|
method: TwoFactorAuthentication.TotpRequired
|
||||||
|
})
|
||||||
|
.pipe(catchError(error => {
|
||||||
|
this.loaderActive = false;
|
||||||
|
this.errorText = error.error.detail;
|
||||||
this.loginButtonIsDisable = true;
|
this.loginButtonIsDisable = true;
|
||||||
throw error;
|
throw error;
|
||||||
}))
|
}))
|
||||||
@ -92,4 +125,8 @@ export class LoginComponent {
|
|||||||
this.router.navigate(['admin']).then();
|
this.router.navigate(['admin']).then();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected loginOAuth(result: TwoFactorAuthentication) {
|
||||||
|
this.updateTwoFactorValidation(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {Component, LOCALE_ID, ViewChild} from '@angular/core';
|
import {Component, ViewChild} from '@angular/core';
|
||||||
import {AdditionalText, TableHeaderComponent} from "@component/schedule/table-header/table-header.component";
|
import {AdditionalText, TableHeaderComponent} from "@component/schedule/table-header/table-header.component";
|
||||||
import {addDays, weekInYear} from "@progress/kendo-date-math";
|
import {addDays, weekInYear} from "@progress/kendo-date-math";
|
||||||
import {TabsComponent, TabsSelect} from "@component/schedule/tabs/tabs.component";
|
import {TabsComponent, TabsSelect} from "@component/schedule/tabs/tabs.component";
|
||||||
@ -38,8 +38,7 @@ import {HasRoleDirective} from "@/directives/has-role.directive";
|
|||||||
styleUrl: './schedule.component.css',
|
styleUrl: './schedule.component.css',
|
||||||
providers: [
|
providers: [
|
||||||
ScheduleService,
|
ScheduleService,
|
||||||
ImportService,
|
ImportService
|
||||||
{provide: LOCALE_ID, useValue: 'ru-RU'}
|
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -56,7 +55,11 @@ export class ScheduleComponent {
|
|||||||
|
|
||||||
@ViewChild('tableHeader') childComponent!: TableHeaderComponent;
|
@ViewChild('tableHeader') childComponent!: TableHeaderComponent;
|
||||||
|
|
||||||
constructor(api: ScheduleService, route: ActivatedRoute, private importApi: ImportService, private notify: ToastrService, public dialog: MatDialog) {
|
constructor(api: ScheduleService,
|
||||||
|
route: ActivatedRoute,
|
||||||
|
private importApi: ImportService,
|
||||||
|
private notify: ToastrService,
|
||||||
|
public dialog: MatDialog) {
|
||||||
route.queryParams.subscribe(params => {
|
route.queryParams.subscribe(params => {
|
||||||
TabStorageService.selectDataFromQuery(params);
|
TabStorageService.selectDataFromQuery(params);
|
||||||
});
|
});
|
||||||
@ -130,10 +133,10 @@ export class ScheduleComponent {
|
|||||||
this.startWeek = this.startTerm;
|
this.startWeek = this.startTerm;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleWeekEvent(eventData: boolean | null) {
|
protected handleWeekEvent(forward: boolean | null) {
|
||||||
if (eventData === null) {
|
if (forward === null) {
|
||||||
this.calculateCurrentWeek();
|
this.calculateCurrentWeek();
|
||||||
} else if (eventData) {
|
} else if (forward) {
|
||||||
this.startWeek = addDays(this.startWeek, 7);
|
this.startWeek = addDays(this.startWeek, 7);
|
||||||
} else {
|
} else {
|
||||||
this.startWeek = addDays(this.startWeek, -7);
|
this.startWeek = addDays(this.startWeek, -7);
|
||||||
@ -141,7 +144,16 @@ export class ScheduleComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get currentWeek(): number {
|
get currentWeek(): number {
|
||||||
let result = (weekInYear(this.startWeek) - weekInYear(this.startTerm)) + 1;
|
const startTermWeek = weekInYear(this.startTerm);
|
||||||
|
let startWeekNumber;
|
||||||
|
const startWeek = addDays(this.startWeek, 6);
|
||||||
|
|
||||||
|
if (startWeek.getFullYear() > this.startTerm.getFullYear())
|
||||||
|
startWeekNumber = weekInYear(new Date(this.startTerm.getFullYear(), 11, 29)) + weekInYear(startWeek);
|
||||||
|
else
|
||||||
|
startWeekNumber = weekInYear(startWeek);
|
||||||
|
|
||||||
|
let result = startWeekNumber - startTermWeek + 1;
|
||||||
|
|
||||||
if (result <= 0)
|
if (result <= 0)
|
||||||
result = 1;
|
result = 1;
|
||||||
@ -156,7 +168,7 @@ export class ScheduleComponent {
|
|||||||
|
|
||||||
protected openDialog() {
|
protected openDialog() {
|
||||||
if (this.lastRequest == null) {
|
if (this.lastRequest == null) {
|
||||||
this.notify.error("It is not possible to make an import request because the table data has not been selected", "Import error");
|
this.notify.error("Запрос на импорт невозможен, поскольку данные таблицы не были выбраны", "Ошибка импорта");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const dialogRef = this.dialog.open(ConfirmDialogComponent);
|
const dialogRef = this.dialog.open(ConfirmDialogComponent);
|
||||||
@ -176,7 +188,7 @@ export class ScheduleComponent {
|
|||||||
},
|
},
|
||||||
error: _ => {
|
error: _ => {
|
||||||
this.excelImportLoader = false;
|
this.excelImportLoader = false;
|
||||||
this.notify.error("Failed to import Excel file");
|
this.notify.error("Не удалось импортировать файл Excel");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -53,47 +53,7 @@
|
|||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field color="accent" style="margin-bottom: 20px">
|
<password-input [formGroup]="createAdminForm" [isSetupMode]="true"/>
|
||||||
<mat-label>Пароль</mat-label>
|
|
||||||
<input matInput
|
|
||||||
matTooltip="Укажите пароль"
|
|
||||||
formControlName="password"
|
|
||||||
required
|
|
||||||
[type]="hidePass ? 'password' : 'text'">
|
|
||||||
|
|
||||||
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
|
|
||||||
[attr.aria-pressed]="hidePass">
|
|
||||||
<mat-icon>{{ hidePass ? 'visibility_off' : 'visibility' }}</mat-icon>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if (createAdminForm.get('password')?.hasError('required')) {
|
|
||||||
<mat-error>
|
|
||||||
Пароль является <i>обязательным</i>
|
|
||||||
</mat-error>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (createAdminForm.get('password')?.hasError('minlength')) {
|
|
||||||
<mat-error>
|
|
||||||
Пароль должен быть не менее {{ 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>
|
|
||||||
|
|
||||||
<mat-form-field color="accent">
|
<mat-form-field color="accent">
|
||||||
<mat-label>Повторите пароль</mat-label>
|
<mat-label>Повторите пароль</mat-label>
|
||||||
@ -117,6 +77,8 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<OAuthProviders [canUnlink]="true" [activeProvidersId]="activatedProviders"
|
<OAuthProviders [canUnlink]="true" [activeProvidersId]="activatedProviders"
|
||||||
[message]="'Или можете получить часть данных от сторонних сервисов'"/>
|
(oAuthUpdateProviders)="updateProviders()"
|
||||||
|
[message]="'Или можете получить часть данных от сторонних сервисов'"
|
||||||
|
[action]="OAuthAction.Bind" [isSetup]="true"/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {FormBuilder, FormGroup, ReactiveFormsModule, ValidatorFn, Validators} from "@angular/forms";
|
import {Location} from '@angular/common';
|
||||||
|
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
|
||||||
import {NavigationService} from "@service/navigation.service";
|
import {NavigationService} from "@service/navigation.service";
|
||||||
import {passwordMatchValidator} from '@service/password-match.validator';
|
import {passwordMatchValidator} from '@service/password-match.validator';
|
||||||
import SetupService from "@api/v1/setup.service";
|
import SetupService from "@api/v1/setup.service";
|
||||||
@ -9,10 +10,12 @@ import {MatInput} from "@angular/material/input";
|
|||||||
import {MatTooltip} from "@angular/material/tooltip";
|
import {MatTooltip} from "@angular/material/tooltip";
|
||||||
import {MatIconButton} from "@angular/material/button";
|
import {MatIconButton} from "@angular/material/button";
|
||||||
import {MatIcon} from "@angular/material/icon";
|
import {MatIcon} from "@angular/material/icon";
|
||||||
import AuthApiService from "@api/v1/authApiService";
|
import AuthApiService from "@api/v1/authApi.service";
|
||||||
import {PasswordPolicy} from "@model/passwordPolicy";
|
|
||||||
import {OAuthProviders} from "@component/OAuthProviders/OAuthProviders";
|
import {OAuthProviders} from "@component/OAuthProviders/OAuthProviders";
|
||||||
import {OAuthProvider} from "@model/oAuthProvider";
|
import {OAuthProvider} from "@model/oAuthProvider";
|
||||||
|
import {PasswordInputComponent} from "@component/common/password-input/password-input.component";
|
||||||
|
import {OAuthAction} from "@model/oAuthAction";
|
||||||
|
import {Router} from "@angular/router";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-create-admin',
|
selector: 'app-create-admin',
|
||||||
@ -25,20 +28,20 @@ import {OAuthProvider} from "@model/oAuthProvider";
|
|||||||
MatTooltip,
|
MatTooltip,
|
||||||
MatIconButton,
|
MatIconButton,
|
||||||
MatIcon,
|
MatIcon,
|
||||||
OAuthProviders
|
OAuthProviders,
|
||||||
|
PasswordInputComponent
|
||||||
],
|
],
|
||||||
templateUrl: './create-admin.component.html',
|
templateUrl: './create-admin.component.html',
|
||||||
providers: [AuthApiService]
|
providers: [AuthApiService, Location]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class CreateAdminComponent {
|
export class CreateAdminComponent {
|
||||||
protected createAdminForm!: FormGroup;
|
protected createAdminForm!: FormGroup;
|
||||||
protected hidePass = true;
|
|
||||||
protected hideRetypePass = true;
|
protected hideRetypePass = true;
|
||||||
protected policy!: PasswordPolicy;
|
|
||||||
protected activatedProviders: OAuthProvider[] = [];
|
protected activatedProviders: OAuthProvider[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(private router: Router,
|
||||||
|
private location: Location,
|
||||||
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
|
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
|
||||||
this.createAdminForm = this.formBuilder.group({
|
this.createAdminForm = this.formBuilder.group({
|
||||||
user: ['', Validators.pattern(/^([A-Za-z0-9]){4,}$/)],
|
user: ['', Validators.pattern(/^([A-Za-z0-9]){4,}$/)],
|
||||||
@ -63,52 +66,34 @@ export class CreateAdminComponent {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.api.passwordPolicyConfiguration().subscribe(policy => {
|
this.updateAdminData();
|
||||||
this.policy = policy;
|
}
|
||||||
const passwordValidators = this.createPasswordValidators(policy);
|
|
||||||
this.createAdminForm.get('password')?.setValidators(passwordValidators);
|
|
||||||
this.createAdminForm.get('password')?.updateValueAndValidity();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
private updateAdminData() {
|
||||||
this.api.adminConfiguration().subscribe(configuration => {
|
this.api.adminConfiguration().subscribe(configuration => {
|
||||||
if (configuration) {
|
if (configuration) {
|
||||||
|
if (this.createAdminForm.get('email')?.value == 0)
|
||||||
this.createAdminForm.get('email')?.setValue(configuration.email);
|
this.createAdminForm.get('email')?.setValue(configuration.email);
|
||||||
|
|
||||||
|
if (this.createAdminForm.get('user')?.value == 0)
|
||||||
this.createAdminForm.get('user')?.setValue(configuration.username);
|
this.createAdminForm.get('user')?.setValue(configuration.username);
|
||||||
|
|
||||||
this.activatedProviders = configuration.usedOAuthProviders;
|
this.activatedProviders = configuration.usedOAuthProviders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const currentPath = this.router.url.split('?')[0];
|
||||||
|
this.location.replaceState(currentPath);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
|
||||||
this.hidePass = !this.hidePass;
|
|
||||||
event.stopPropagation();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected toggleRetypePassword(event: MouseEvent) {
|
protected toggleRetypePassword(event: MouseEvent) {
|
||||||
this.hideRetypePass = !this.hideRetypePass;
|
this.hideRetypePass = !this.hideRetypePass;
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected updateProviders() {
|
||||||
|
this.updateAdminData();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly OAuthAction = OAuthAction;
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,11 @@
|
|||||||
Настройте систему логирования как будет удобно для отображения.
|
Настройте систему логирования как будет удобно для отображения.
|
||||||
Можно настроить путь к файлу, имена файлов или вовсе отключить логирование в файл.
|
Можно настроить путь к файлу, имена файлов или вовсе отключить логирование в файл.
|
||||||
</p>
|
</p>
|
||||||
|
<p class="mat-body-2 secondary">
|
||||||
|
Также вы можете настроить интеграцию с Seq.
|
||||||
|
Введите необходимые данные и мы отправим тестовый лог на сервер Seq. Его уровень будет Warning.
|
||||||
|
Если тестовый лог не появился вернитесь на данный шаг и перепроверьте данные.
|
||||||
|
</p>
|
||||||
|
|
||||||
<form [formGroup]="loggingSettings">
|
<form [formGroup]="loggingSettings">
|
||||||
<p>
|
<p>
|
||||||
@ -31,5 +36,18 @@
|
|||||||
matTooltip="Укажите название файла, в который будут записаны логи"
|
matTooltip="Укажите название файла, в который будут записаны логи"
|
||||||
formControlName="logName">
|
formControlName="logName">
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Сервер Seq</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите сервер Seq вначале указав схему (http/https)"
|
||||||
|
formControlName="seqServer">
|
||||||
|
</mat-form-field>
|
||||||
|
<mat-form-field color="accent">
|
||||||
|
<mat-label>Api ключ Seq</mat-label>
|
||||||
|
<input matInput
|
||||||
|
matTooltip="Укажите ключ API, который вы создали в Seq"
|
||||||
|
formControlName="seqKey">
|
||||||
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -45,7 +45,9 @@ export class LoggingComponent {
|
|||||||
this.loggingSettings = this.formBuilder.group({
|
this.loggingSettings = this.formBuilder.group({
|
||||||
enabled: [true, Validators.required],
|
enabled: [true, Validators.required],
|
||||||
logPath: [''],
|
logPath: [''],
|
||||||
logName: ['']
|
logName: [''],
|
||||||
|
seqServer: [''],
|
||||||
|
seqKey: ['']
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -56,9 +58,11 @@ export class LoggingComponent {
|
|||||||
|
|
||||||
this.navigationService.nextButtonAction = () => {
|
this.navigationService.nextButtonAction = () => {
|
||||||
return this.api.setLogging({
|
return this.api.setLogging({
|
||||||
"enableLogToFile": this.loggingSettings.get('enabled')?.value,
|
enableLogToFile: this.loggingSettings.get('enabled')?.value,
|
||||||
"logFileName": this.loggingSettings.get('logName')?.value,
|
logFileName: this.loggingSettings.get('logName')?.value,
|
||||||
"logFilePath": this.loggingSettings.get('logPath')?.value
|
logFilePath: this.loggingSettings.get('logPath')?.value,
|
||||||
|
apiServerSeq: this.loggingSettings.get('seqServer')?.value,
|
||||||
|
apiKeySeq: this.loggingSettings.get('seqKey')?.value
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -73,6 +77,8 @@ export class LoggingComponent {
|
|||||||
this.loggingSettings.get('enabled')?.setValue(x.enableLogToFile);
|
this.loggingSettings.get('enabled')?.setValue(x.enableLogToFile);
|
||||||
this.loggingSettings.get('logName')?.setValue(x.logFileName);
|
this.loggingSettings.get('logName')?.setValue(x.logFileName);
|
||||||
this.loggingSettings.get('logPath')?.setValue(x.logFilePath);
|
this.loggingSettings.get('logPath')?.setValue(x.logFilePath);
|
||||||
|
this.loggingSettings.get('seqServer')?.setValue(x.apiServerSeq);
|
||||||
|
this.loggingSettings.get('seqKey')?.setValue(x.apiKeySeq);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import {Component, ViewEncapsulation} from '@angular/core';
|
import {Component, ViewEncapsulation} from '@angular/core';
|
||||||
import {MatSidenavModule} from "@angular/material/sidenav";
|
import {MatSidenavModule} from "@angular/material/sidenav";
|
||||||
import {Router, RouterOutlet} from "@angular/router";
|
import {ActivatedRoute, Router, RouterOutlet} from "@angular/router";
|
||||||
import {MatCard} from "@angular/material/card";
|
import {MatCard} from "@angular/material/card";
|
||||||
import {MatButton} from "@angular/material/button";
|
import {MatButton} from "@angular/material/button";
|
||||||
import {NavigationService} from "@service/navigation.service";
|
import {NavigationService} from "@service/navigation.service";
|
||||||
@ -30,20 +30,29 @@ export class SetupComponent {
|
|||||||
protected skipButtonDisabled: boolean = false;
|
protected skipButtonDisabled: boolean = false;
|
||||||
protected loaderActive: boolean = false;
|
protected loaderActive: boolean = false;
|
||||||
|
|
||||||
protected routes: Array<string> = ['', 'welcome', 'database', 'cache', 'password-policy', 'schedule', 'logging', 'create-admin', 'two-factor', 'summary'];
|
protected routes: Array<string> = ['', 'welcome', 'create-admin', 'database', 'cache', 'password-policy', 'schedule', 'logging', 'two-factor', 'summary'];
|
||||||
private index: number = 1;
|
private index: number = 1;
|
||||||
|
|
||||||
protected get getIndex() {
|
protected get getIndex() {
|
||||||
return this.index;
|
return this.index;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(private router: Router, private navigationService: NavigationService, api: SetupService, private notify: ToastrService) {
|
constructor(private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private navigationService: NavigationService,
|
||||||
|
api: SetupService,
|
||||||
|
private notify: ToastrService) {
|
||||||
|
|
||||||
api.isConfigured().subscribe(x => {
|
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();
|
const currentQueryParams = this.route.snapshot.queryParams;
|
||||||
|
this.router.navigate(
|
||||||
|
['setup/', this.routes[this.index]],
|
||||||
|
{queryParams: currentQueryParams}
|
||||||
|
).then();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.initializeButtonSubscriptions();
|
this.initializeButtonSubscriptions();
|
||||||
@ -104,13 +113,25 @@ export class SetupComponent {
|
|||||||
this.moveToPreviousPage();
|
this.moveToPreviousPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
private moveToNextPage() {
|
private moveToNextPage(): void {
|
||||||
if (this.index < this.routes.length - 1) {
|
if (this.index < this.routes.length - 1) {
|
||||||
this.index++;
|
this.index++;
|
||||||
this.router.navigate(['setup/', this.routes[this.index]]).then();
|
|
||||||
|
const currentQueryParams = this.route.snapshot.queryParams;
|
||||||
|
|
||||||
|
this.router.navigate(
|
||||||
|
['setup/', this.routes[this.index]],
|
||||||
|
{queryParams: currentQueryParams}
|
||||||
|
).then(() => {
|
||||||
this.initializePage();
|
this.initializePage();
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
this.router.navigate(['/']).then();
|
const currentQueryParams = this.route.snapshot.queryParams;
|
||||||
|
|
||||||
|
this.router.navigate(
|
||||||
|
['/'],
|
||||||
|
{queryParams: currentQueryParams}
|
||||||
|
).then();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -163,6 +163,16 @@
|
|||||||
Путь к файлу журнала: {{ loggingConfig.logFilePath }}
|
Путь к файлу журнала: {{ loggingConfig.logFilePath }}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@if (loggingConfig.apiServerSeq) {
|
||||||
|
<div>
|
||||||
|
Сервер Seq: {{ loggingConfig.apiServerSeq }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@if (loggingConfig.apiKeySeq) {
|
||||||
|
<div>
|
||||||
|
Ключ Seq: {{ loggingConfig.apiKeySeq }}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
<h3>Ваш код: <i><strong>{{ secret }}</strong></i></h3>
|
<h3>Ваш код: <i><strong>{{ secret }}</strong></i></h3>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<img [src]="totpImage" alt="totp-qr-code"/>
|
<img [src]="totpImage" alt="totp-qr-code" style="max-height: 60vh;"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form [formGroup]="twoFactorForm">
|
<form [formGroup]="twoFactorForm">
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
export interface CreateUserRequest {
|
|
||||||
email: string;
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export interface LoginRequest {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
export interface ScheduleRequest {
|
|
||||||
groups?: Array<number>;
|
|
||||||
isEven?: boolean;
|
|
||||||
disciplines?: Array<number>;
|
|
||||||
professors?: Array<number>;
|
|
||||||
lectureHalls?: Array<number>;
|
|
||||||
}
|
|
@ -2,4 +2,6 @@ export interface LoggingRequest {
|
|||||||
enableLogToFile: boolean;
|
enableLogToFile: boolean;
|
||||||
logFileName?: string;
|
logFileName?: string;
|
||||||
logFilePath?: string;
|
logFilePath?: string;
|
||||||
|
apiServerSeq?: string;
|
||||||
|
apiKeySeq?: string;
|
||||||
}
|
}
|
||||||
|
@ -4,4 +4,5 @@ export interface ScheduleRequest {
|
|||||||
disciplines?: Array<number>;
|
disciplines?: Array<number>;
|
||||||
professors?: Array<number>;
|
professors?: Array<number>;
|
||||||
lectureHalls?: Array<number>;
|
lectureHalls?: Array<number>;
|
||||||
|
lessonType?: Array<number>;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
export interface CampusBasicInfoResponse {
|
|
||||||
id: number;
|
|
||||||
codeName: string;
|
|
||||||
fullName?: string;
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
export interface CampusDetailsResponse {
|
|
||||||
id: number;
|
|
||||||
codeName: string;
|
|
||||||
fullName?: string;
|
|
||||||
address?: string;
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export interface DisciplineResponse {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export interface ErrorResponse {
|
|
||||||
error: string;
|
|
||||||
code: number;
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export interface FacultyResponse {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
export interface GroupDetailsResponse {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
courseNumber: number;
|
|
||||||
facultyId?: number;
|
|
||||||
facultyName?: string;
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
export interface GroupResponse {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
courseNumber: number;
|
|
||||||
facultyId?: number;
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
export interface LectureHallDetailsResponse {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
campusId: number;
|
|
||||||
campusName?: string;
|
|
||||||
campusCode?: string;
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export interface LectureHallResponse {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
campusId: number;
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export interface ProfessorResponse {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
altName?: string;
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
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>;
|
|
||||||
}
|
|
@ -0,0 +1,4 @@
|
|||||||
|
export interface CronUpdateScheduleResponse {
|
||||||
|
cron: string;
|
||||||
|
nextStart?: Date[];
|
||||||
|
}
|
4
src/shared/responses/v1/lessonTypeResponse.ts
Normal file
4
src/shared/responses/v1/lessonTypeResponse.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export interface LessonTypeResponse {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
export enum DayOfWeek {
|
|
||||||
Sunday,
|
|
||||||
Monday,
|
|
||||||
Tuesday,
|
|
||||||
Wednesday,
|
|
||||||
Thursday,
|
|
||||||
Friday,
|
|
||||||
Saturday
|
|
||||||
}
|
|
@ -1,4 +0,0 @@
|
|||||||
export interface PairPeriodTime {
|
|
||||||
start: string;
|
|
||||||
end: string;
|
|
||||||
}
|
|
7
src/shared/structs/cronUpdateSkip.ts
Normal file
7
src/shared/structs/cronUpdateSkip.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import {DateOnly} from "@model/dateOnly";
|
||||||
|
|
||||||
|
export default interface CronUpdateSkip {
|
||||||
|
start?: DateOnly;
|
||||||
|
end?: DateOnly;
|
||||||
|
date?: DateOnly;
|
||||||
|
}
|
4
src/shared/structs/oAuthAction.ts
Normal file
4
src/shared/structs/oAuthAction.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum OAuthAction {
|
||||||
|
Login,
|
||||||
|
Bind
|
||||||
|
}
|
Reference in New Issue
Block a user