Compare commits

..

7 Commits

Author SHA1 Message Date
1ffbfad37a build: update ref 2024-12-18 07:11:15 +03:00
c04c457211 fix: add alt 2024-12-18 07:10:51 +03:00
fba28b6bbe feat: rewrite setup wizard 2024-12-18 07:09:29 +03:00
86e6f59567 feat: add providers OAuth 2024-12-18 07:02:08 +03:00
a2d4151cc3 refactor: clean code 2024-12-18 06:57:27 +03:00
3af8c43cd9 refactor: adapt models to the new api 2024-12-18 06:50:41 +03:00
21f89132ff refactor: adapt models to the new api 2024-12-18 06:48:51 +03:00
109 changed files with 4914 additions and 3605 deletions
README.mdpackage-lock.jsonpackage.json
src
api
app
assets
components
directives
index.htmlmain.ts
pages
services
shared
styles.css

@ -1,6 +1,6 @@
# MIREA schedule by Winsomnia # MIREA schedule by Winsomnia
[![Angular Release](https://img.shields.io/badge/v18.2-8?style=flat-square&label=Angular&labelColor=512BD4&color=606060)](https://github.com/angular/angular-cli) [![Angular Release](https://img.shields.io/badge/v19.0-8?style=flat-square&label=Angular&labelColor=512BD4&color=606060)](https://github.com/angular/angular-cli)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](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.

5994
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "frontend", "name": "frontend",
"version": "1.0.0-b9", "version": "1.0.0-b10",
"scripts": { "scripts": {
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
@ -10,35 +10,34 @@
}, },
"private": true, "private": true,
"dependencies": { "dependencies": {
"@angular/animations": "^18.2.9", "@angular/animations": "^19.0.4",
"@angular/cdk": "~18.2.10", "@angular/cdk": "~19.0.3",
"@angular/cdk-experimental": "^18.2.10", "@angular/cdk-experimental": "^19.0.3",
"@angular/common": "^18.2.9", "@angular/common": "^19.0.4",
"@angular/compiler": "^18.2.9", "@angular/compiler": "^19.0.4",
"@angular/core": "^18.2.9", "@angular/core": "^19.0.4",
"@angular/forms": "^18.2.9", "@angular/forms": "^19.0.4",
"@angular/material": "~18.2.10", "@angular/material": "~19.0.3",
"@angular/platform-browser": "^18.2.9", "@angular/platform-browser": "^19.0.4",
"@angular/platform-browser-dynamic": "^18.2.9", "@angular/platform-browser-dynamic": "^19.0.4",
"@angular/router": "^18.2.9", "@angular/router": "^19.0.4",
"@dhutaryan/ngx-mat-timepicker": "^18.0.2",
"@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",
"tslib": "^2.8.0", "tslib": "^2.8.1",
"zone.js": "^0.14.10" "zone.js": "^0.15.0"
}, },
"devDependencies": { "devDependencies": {
"@angular-devkit/build-angular": "^18.2.10", "@angular-devkit/build-angular": "^19.0.5",
"@angular/cli": "^18.2.10", "@angular/cli": "^19.0.5",
"@angular/compiler-cli": "^18.2.9", "@angular/compiler-cli": "^19.0.4",
"@types/jasmine": "~5.1.4", "@types/jasmine": "~5.1.5",
"jasmine-core": "~5.4.0", "jasmine-core": "~5.5.0",
"karma": "~6.4.4", "karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0", "karma-chrome-launcher": "~3.2.0",
"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.5.4" "typescript": "^5.6.3"
} }
} }

@ -15,7 +15,7 @@ import {Router} from "@angular/router";
import {Injectable} from "@angular/core"; import {Injectable} from "@angular/core";
import {RequestBuilder, RequestData} from "@api/RequestBuilder"; import {RequestBuilder, RequestData} from "@api/RequestBuilder";
import {ToastrService} from "ngx-toastr"; import {ToastrService} from "ngx-toastr";
import {AuthRoles} from "@model/AuthRoles"; import {AuthRoles} from "@model/authRoles";
export enum AvailableVersion { export enum AvailableVersion {
v1 v1
@ -69,7 +69,7 @@ export default abstract class ApiService {
}).pipe( }).pipe(
catchError(error => { catchError(error => {
if (!secondTry && error.status === 401) if (!secondTry && error.status === 401)
return this.handle401Error().pipe( return this.handle401Error(error).pipe(
switchMap(() => this.sendHttpRequest<Type>(method, request, true)) switchMap(() => this.sendHttpRequest<Type>(method, request, true))
); );
else { else {
@ -88,7 +88,7 @@ export default abstract class ApiService {
}); });
} }
private handle401Error(): Observable<any> { private handle401Error(error: any): Observable<any> {
if (ApiService.isRefreshingToken.value) if (ApiService.isRefreshingToken.value)
return ApiService.refreshTokenSubject.asObservable(); return ApiService.refreshTokenSubject.asObservable();
@ -103,7 +103,7 @@ export default abstract class ApiService {
ApiService.isRefreshingToken.next(false); ApiService.isRefreshingToken.next(false);
ApiService.refreshTokenSubject.error(err); ApiService.refreshTokenSubject.error(err);
ApiService.refreshTokenSubject = new ReplaySubject(1); ApiService.refreshTokenSubject = new ReplaySubject(1);
throw err; throw error;
}) })
); );
} }

@ -1,8 +1,14 @@
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 {LoginRequest} from "@api/v1/loginRequest"; import {LoginRequest} from "@api/v1/loginRequest";
import {catchError, of} from "rxjs"; import {catchError, map, Observable, of} from "rxjs";
import {AuthRoles} from "@model/AuthRoles"; import {AuthRoles} from "@model/authRoles";
import {AvailableOAuthProvidersResponse} from "@api/v1/availableProvidersResponse";
import {OAuthProvider} from "@model/oAuthProvider";
export interface OAuthProviderData extends AvailableOAuthProvidersResponse {
icon: string;
}
@Injectable() @Injectable()
export default class AuthApiService extends ApiService { export default class AuthApiService extends ApiService {
@ -51,4 +57,32 @@ export default class AuthApiService extends ApiService {
}) })
); );
} }
private getProviderIcon(provider: OAuthProvider): string {
switch (provider) {
case OAuthProvider.Google:
return 'assets/icons/google.svg';
case OAuthProvider.Yandex:
return 'assets/icons/yandex.svg';
case OAuthProvider.MailRu:
return 'assets/icons/mailru.svg';
default:
return '';
}
}
public availableProviders(): Observable<OAuthProviderData[]> {
let request = this.createRequestBuilder()
.setEndpoint('AvailableProviders')
.setWithCredentials()
.build;
return this.get<Array<AvailableOAuthProvidersResponse>>(request).pipe(
map(data => {
return data.map((provider) => ({
...provider,
icon: this.getProviderIcon(provider.provider),
}) as OAuthProviderData);
}));
}
} }

@ -1,7 +1,7 @@
import {Injectable} from "@angular/core"; import {Injectable} from "@angular/core";
import ApiService, {AvailableVersion} from "@api/api.service"; import ApiService, {AvailableVersion} from "@api/api.service";
import {DateOnly} from "@model/DateOnly"; import {DateOnly} from "@model/dateOnly";
import {PeriodTimes} from "@model/pairPeriodTime"; import {PairPeriodTime} from "@model/pairPeriodTime";
import {ScheduleRequest} from "@api/v1/scheduleRequest"; import {ScheduleRequest} from "@api/v1/scheduleRequest";
import {ScheduleResponse} from "@api/v1/scheduleResponse"; import {ScheduleResponse} from "@api/v1/scheduleResponse";
import {map} from "rxjs"; import {map} from "rxjs";
@ -16,7 +16,7 @@ export class ScheduleService extends ApiService {
} }
public pairPeriod() { public pairPeriod() {
return this.get<PeriodTimes>('PairPeriod'); return this.get<PairPeriodTime>('PairPeriod');
} }
public postSchedule(data: ScheduleRequest) { public postSchedule(data: ScheduleRequest) {

@ -0,0 +1,17 @@
import {Injectable} from "@angular/core";
import ApiService, {AvailableVersion} from "@api/api.service";
@Injectable()
export default class SecurityService extends ApiService {
public readonly basePath = 'Security/';
public readonly version = AvailableVersion.v1;
public generateTotpQrCode(totpKey: string, username: string) {
let request = this.createRequestBuilder()
.setEndpoint('GenerateTotpQrCode')
.setQueryParams({totpKey: totpKey, label: username})
.build;
return this.combinedUrl(request);
}
}

@ -1,12 +1,17 @@
import {Injectable} from "@angular/core"; import {Injectable} from "@angular/core";
import ApiService, {AvailableVersion} from "@api/api.service"; import ApiService, {AvailableVersion} from "@api/api.service";
import {DatabaseRequest} from "@api/v1/databaseRequest"; import {catchError, of, switchMap} from "rxjs";
import {CacheRequest} from "@api/v1/cacheRequest"; import {DatabaseResponse} from "@api/v1/configuration/databaseResponse";
import {DatabaseRequest} from "@api/v1/configuration/databaseRequest";
import {CacheRequest} from "@api/v1/configuration/cacheRequest";
import {CreateUserRequest} from "@api/v1/createUserRequest"; import {CreateUserRequest} from "@api/v1/createUserRequest";
import {LoggingRequest} from "@api/v1/loggingRequest"; import {LoggingRequest} from "@api/v1/configuration/loggingRequest";
import {EmailRequest} from "@api/v1/emailRequest"; import {ScheduleConfigurationRequest} from "@api/v1/configuration/scheduleConfigurationRequest";
import {ScheduleConfigurationRequest} from "@api/v1/scheduleConfigurationRequest"; import {EmailRequest} from "@api/v1/configuration/emailRequest";
import {DateOnly} from "@model/DateOnly"; import {DateOnly} from "@model/dateOnly";
import {CacheResponse} from "@api/v1/configuration/cacheResponse";
import {PasswordPolicy} from "@model/passwordPolicy";
import {UserResponse} from "@api/v1/userResponse";
@Injectable() @Injectable()
export default class SetupService extends ApiService { export default class SetupService extends ApiService {
@ -23,6 +28,17 @@ export default class SetupService extends ApiService {
return this.get<boolean>(request); return this.get<boolean>(request);
} }
public isConfiguredToken() {
let request = this.createRequestBuilder()
.setEndpoint('IsConfiguredToken')
.setWithCredentials()
.build;
return this.get<boolean>(request).pipe(catchError(_ => {
return of(false);
}));
}
public setPsql(data: DatabaseRequest) { public setPsql(data: DatabaseRequest) {
let request = this.createRequestBuilder() let request = this.createRequestBuilder()
.setEndpoint('SetPsql') .setEndpoint('SetPsql')
@ -53,6 +69,15 @@ export default class SetupService extends ApiService {
return this.post<boolean>(request); return this.post<boolean>(request);
} }
public databaseConfiguration() {
let request = this.createRequestBuilder()
.setEndpoint('DatabaseConfiguration')
.setWithCredentials()
.build;
return this.get<DatabaseResponse>(request);
}
public setRedis(data: CacheRequest) { public setRedis(data: CacheRequest) {
let request = this.createRequestBuilder() let request = this.createRequestBuilder()
.setEndpoint('SetRedis') .setEndpoint('SetRedis')
@ -72,6 +97,34 @@ export default class SetupService extends ApiService {
return this.post<boolean>(request); return this.post<boolean>(request);
} }
public cacheConfiguration() {
let request = this.createRequestBuilder()
.setEndpoint('CacheConfiguration')
.setWithCredentials()
.build;
return this.get<CacheResponse>(request);
}
public setPasswordPolicy(data: PasswordPolicy | null) {
let request = this.createRequestBuilder()
.setEndpoint('SetPasswordPolicy')
.setData(data)
.setWithCredentials()
.build;
return this.post<boolean>(request);
}
public passwordPolicyConfiguration() {
let request = this.createRequestBuilder()
.setEndpoint('PasswordPolicyConfiguration')
.setWithCredentials()
.build;
return this.get<PasswordPolicy>(request);
}
public createAdmin(data: CreateUserRequest) { public createAdmin(data: CreateUserRequest) {
let request = this.createRequestBuilder() let request = this.createRequestBuilder()
.setEndpoint('CreateAdmin') .setEndpoint('CreateAdmin')
@ -82,6 +135,22 @@ export default class SetupService extends ApiService {
return this.post<boolean>(request); return this.post<boolean>(request);
} }
public adminConfiguration() {
let request = this.createRequestBuilder()
.setEndpoint('UpdateAdminConfiguration')
.setWithCredentials()
.build;
return this.get(request).pipe(switchMap(_ => {
request = this.createRequestBuilder()
.setEndpoint('AdminConfiguration')
.setWithCredentials()
.build;
return this.get<UserResponse>(request);
}));
}
public setLogging(data: LoggingRequest | null = null) { public setLogging(data: LoggingRequest | null = null) {
let request = this.createRequestBuilder() let request = this.createRequestBuilder()
.setEndpoint('SetLogging') .setEndpoint('SetLogging')
@ -92,6 +161,15 @@ export default class SetupService extends ApiService {
return this.post<boolean>(request); return this.post<boolean>(request);
} }
public loggingConfiguration() {
let request = this.createRequestBuilder()
.setEndpoint('LoggingConfiguration')
.setWithCredentials()
.build;
return this.get<LoggingRequest>(request);
}
public setEmail(data: EmailRequest | null = null) { public setEmail(data: EmailRequest | null = null) {
let request = this.createRequestBuilder() let request = this.createRequestBuilder()
.setEndpoint('SetEmail') .setEndpoint('SetEmail')
@ -114,6 +192,34 @@ export default class SetupService extends ApiService {
return this.post<boolean>(request); return this.post<boolean>(request);
} }
public generateTotpKey() {
let request = this.createRequestBuilder()
.setEndpoint('GenerateTotpKey')
.setWithCredentials()
.build;
return this.get<string>(request);
}
public verifyTotp(code: string) {
let request = this.createRequestBuilder()
.setEndpoint('VerifyTotp')
.setWithCredentials()
.setQueryParams({code: code})
.build;
return this.get<boolean>(request);
}
public scheduleConfiguration() {
let request = this.createRequestBuilder()
.setEndpoint('ScheduleConfiguration')
.setWithCredentials()
.build;
return this.get<ScheduleConfigurationRequest>(request);
}
public submit() { public submit() {
let request = this.createRequestBuilder() let request = this.createRequestBuilder()
.setEndpoint('Submit') .setEndpoint('Submit')

@ -9,6 +9,8 @@ import {SetupComponent} from "@page/setup/setup.component";
import {CreateAdminComponent} from "@page/setup/create-admin/create-admin.component"; import {CreateAdminComponent} from "@page/setup/create-admin/create-admin.component";
import {SummaryComponent} from "@page/setup/summary/summary.component"; import {SummaryComponent} from "@page/setup/summary/summary.component";
import {LoginComponent} from "@page/login/login.component"; import {LoginComponent} from "@page/login/login.component";
import {PasswordPolicyComponent} from "@page/setup/password-policy/password-policy.component";
import {TwoFactorComponent} from "@page/setup/two-factor/two-factor.component";
export const routes: Routes = [ export const routes: Routes = [
{path: '', title: 'Расписание', pathMatch: 'full', component: ScheduleComponent}, {path: '', title: 'Расписание', pathMatch: 'full', component: ScheduleComponent},
@ -21,6 +23,8 @@ export const routes: Routes = [
{path: 'schedule', component: SetupScheduleComponent}, {path: 'schedule', component: SetupScheduleComponent},
{path: 'logging', component: LoggingComponent}, {path: 'logging', component: LoggingComponent},
{path: 'summary', component: SummaryComponent}, {path: 'summary', component: SummaryComponent},
{path: 'password-policy', component: PasswordPolicyComponent},
{path: 'two-factor', component: TwoFactorComponent},
{path: '', redirectTo: 'welcome', pathMatch: 'full'} {path: '', redirectTo: 'welcome', pathMatch: 'full'}
] ]
}, },

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Social_Icons" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
<defs>
<style>
.cls-1 {
fill: #ea4335;
}
.cls-1, .cls-2, .cls-3, .cls-4 {
fill-rule: evenodd;
}
.cls-5 {
fill: none;
}
.cls-2 {
fill: #4285f4;
}
.cls-3 {
fill: #fbbc05;
}
.cls-4 {
fill: #34a853;
}
</style>
</defs>
<g id="_x31__stroke">
<g id="Google">
<rect width="128" height="128" rx="96" ry="96" fill="#fff"/>
<rect class="cls-5" width="128" height="128"/>
<g transform="scale(0.67, 0.67)" transform-origin="center">
<path class="cls-3"
d="M27.58,64c0-4.16.69-8.14,1.92-11.88L7.94,35.65c-4.2,8.53-6.57,18.15-6.57,28.35s2.37,19.8,6.56,28.33l21.56-16.5c-1.22-3.72-1.9-7.69-1.9-11.83"/>
<path class="cls-1"
d="M65.46,26.18c9.03,0,17.19,3.2,23.6,8.44l18.64-18.62C96.34,6.11,81.77,0,65.46,0,40.13,0,18.36,14.48,7.94,35.65l21.57,16.47c4.97-15.09,19.14-25.94,35.95-25.94"/>
<path class="cls-4"
d="M65.46,101.82c-16.81,0-30.98-10.85-35.95-25.94l-21.57,16.47c10.42,21.17,32.19,35.65,57.52,35.65,15.63,0,30.56-5.55,41.76-15.95l-20.47-15.83c-5.78,3.64-13.05,5.6-21.28,5.6"/>
<path class="cls-2"
d="M126.63,64c0-3.78-.58-7.85-1.46-11.64h-59.72v24.73h34.38c-1.72,8.43-6.4,14.91-13.09,19.13l20.47,15.83c11.77-10.92,19.42-27.19,19.42-48.05"/>
</g>
</g>
</g>
</svg>

After

(image error) Size: 1.5 KiB

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="mailru" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 492.91 492.91">
<defs>
<style>
.cls-1 {
fill: #fff;
fill-rule: evenodd;
}
.cls-2 {
fill: #0874fc;
}
</style>
</defs>
<circle class="cls-2" cx="246.45" cy="246.46" r="246.46"/>
<g id="Logo">
<g id="yellow">
<path class="cls-1"
d="M241.66,168.96c21.11,0,40.96,9.33,55.53,23.94v.05c0-7.01,4.72-12.3,11.28-12.3l1.66-.02c10.25,0,12.35,9.7,12.35,12.78l.05,109.06c-.73,7.14,7.36,10.82,11.85,6.25,17.51-17.99,38.46-92.52-10.89-135.69-45.99-40.25-107.7-33.62-140.52-11-34.89,24.06-57.21,77.31-35.53,127.32,23.64,54.57,91.28,70.83,131.49,54.61,20.36-8.22,29.77,19.3,8.62,28.3-31.95,13.62-120.86,12.25-162.4-59.71-28.06-48.59-26.57-134.08,47.86-178.37,56.94-33.88,132.01-24.49,177.28,22.78,47.32,49.42,44.56,141.96-1.59,177.96-20.91,16.34-51.97.43-51.77-23.39l-.21-7.79c-14.56,14.45-33.94,22.88-55.05,22.88-41.71,0-78.41-36.71-78.41-78.4,0-42.13,36.7-79.25,78.41-79.25h0ZM294.16,245.19c-1.57-30.54-24.24-48.91-51.62-48.91h-1.03c-31.59,0-49.11,24.84-49.11,53.06,0,31.6,21.2,51.56,48.99,51.56,30.99,0,51.37-22.7,52.84-49.55l-.07-6.17Z"/>
</g>
</g>
</svg>

After

(image error) Size: 1.2 KiB

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="yandex" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<style>
.cls-1 {
fill: white;
}
</style>
</defs>
<g id="_x33_91-yandex">
<rect width="512" height="512" rx="256" ry="256" fill="red"/>
<g transform="scale(0.67, 0.67) translate(100, 128)">
<path class="cls-1"
d="M278.55,309.73l-78.52,176.27h-57.23l86.25-188.49c-40.52-20.58-67.56-57.86-67.56-126.77-.09-96.49,61.1-144.74,133.78-144.74h73.94v460h-49.5v-176.27h-41.15ZM319.7,67.78h-26.42c-39.89,0-78.52,26.41-78.52,102.96s35.4,97.75,78.52,97.75h26.42V67.78h0Z"/>
</g>
</g>
</svg>

After

(image error) Size: 665 B

@ -0,0 +1,77 @@
.provider-container {
display: flex;
gap: 16px;
flex-wrap: wrap;
justify-content: center;
margin: 0;
padding: 0;
}
.provider-container a {
display: inline-block;
text-align: center;
transition: transform 0.3s ease-in-out, filter 0.3s ease;
border-radius: 50%;
padding: 8px;
}
.provider-icon {
object-fit: contain;
user-select: none;
cursor: pointer;
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease, filter 0.3s ease;
border-radius: 50%;
}
.provider-container a:hover .provider-icon {
transform: scale(1.1); /* Slight zoom-in effect on hover */
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); /* Adding shadow for effect */
}
.provider-container .provider-item.disabled {
pointer-events: none; /* Disables click */
opacity: 0.5; /* Dims the icon to indicate it is disabled */
}
.provider-container .provider-item.disabled .provider-icon {
filter: grayscale(100%) contrast(100%); /* Desaturates image if disabled */
}
.provider-item {
width: 48px;
height: 48px;
position: relative;
}
.provider-item.provider-unlink {
filter: grayscale(50%) contrast(60%);
transition: filter 0.3s ease;
}
.provider-item.provider-unlink:hover {
filter: grayscale(0%) contrast(100%);
}
.provider-item.provider-unlink::after {
content: '×';
position: absolute;
top: 10%;
left: 90%;
transform: translate(-100%, 0%) scale(0.0);
font-size: 24px;
color: white;
background-color: red;
width: 24px;
height: 24px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
cursor: pointer;
transition: transform 0.15s ease;
}
.provider-item.provider-unlink:hover::after {
transform: translate(-50%, -50%) scale(1.0);
}

@ -0,0 +1,16 @@
@if (providers.length !== 0) {
<hr/>
<div>
<p class="mat-body-2 secondary">{{ message }}</p>
<div class="provider-container">
@for (provider of providers; track $index) {
<a class="provider-item" (click)="provider.disabled ? confirmDelete(provider) : openOAuth(provider)"
[class.disabled]="!canUnlink && provider.disabled" [class.provider-unlink]="canUnlink && provider.disabled">
<img [alt]="provider.providerName" [src]="provider.icon"
class="provider-icon" draggable="false"/>
</a>
}
</div>
</div>
}

@ -0,0 +1,126 @@
import {Component, Inject, Input, OnInit} from '@angular/core';
import AuthApiService, {OAuthProviderData} from "@api/v1/authApiService";
import {OAuthProvider} from "@model/oAuthProvider";
import {ToastrService} from "ngx-toastr";
import {
MAT_DIALOG_DATA, MatDialog,
MatDialogActions,
MatDialogContent,
MatDialogRef,
MatDialogTitle
} from "@angular/material/dialog";
import {MatButton} from "@angular/material/button";
interface AvailableOAuthProviders extends OAuthProviderData {
disabled: boolean;
}
@Component({
selector: 'app-delete-confirm-dialog',
template: `
<h1 mat-dialog-title>Удалить провайдера?</h1>
<mat-dialog-content>
<p>Вы уверены, что хотите удалить провайдера {{ data.provider.name }}?</p>
</mat-dialog-content>
<mat-dialog-actions style="display: flex; justify-content: flex-end;">
<button mat-button (click)="onCancel()">Отмена</button>
<button mat-raised-button color="warn" (click)="onConfirm()">Удалить</button>
</mat-dialog-actions>
`,
imports: [
MatDialogTitle,
MatDialogContent,
MatDialogActions,
MatButton
]
})
export class DeleteConfirmDialog {
constructor(
public dialogRef: MatDialogRef<DeleteConfirmDialog>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
}
onConfirm(): void {
this.dialogRef.close(true);
}
onCancel(): void {
this.dialogRef.close(false);
}
}
@Component({
selector: 'OAuthProviders',
imports: [],
templateUrl: './OAuthProviders.html',
styleUrl: './OAuthProviders.css',
providers: [AuthApiService]
})
export class OAuthProviders implements OnInit {
protected providers: AvailableOAuthProviders[] = [];
protected _activeProvidersId: OAuthProvider[] = [];
@Input() message: string = 'Вы можете войти в аккаунт через';
@Input() activeProviders: string[] = [];
@Input() set activeProvidersId(data: OAuthProvider[]) {
this._activeProvidersId = data;
this.updateDisabledProviders();
}
@Input() canUnlink: boolean = false;
constructor(authApi: AuthApiService, private notify: ToastrService, private dialog: MatDialog) {
authApi.availableProviders().subscribe(providers => this.updateDisabledProviders(providers));
}
private updateDisabledProviders(data: OAuthProviderData[] | null = null) {
this.providers = (data ?? this.providers).map(provider => {
return {
...provider,
disabled: this._activeProvidersId.includes(provider.provider) || this.activeProviders.includes(provider.providerName)
};
});
}
ngOnInit(): void {
window.addEventListener('message', (event) => {
if (event.data && event.data.success === false) {
console.error(event.data.message);
this.notify.error(event.data.message, 'OAuth ошибка');
} else {
this.activeProvidersId.push(event.data.provider);
this.updateDisabledProviders();
}
});
}
protected openOAuth(provider: AvailableOAuthProviders) {
console.log(provider.redirect);
const oauthWindow = window.open(
provider.redirect,
'_blank',
);
if (!oauthWindow) {
this.notify.error('Не удалось открыть OAuth окно');
return;
}
}
protected confirmDelete(provider: AvailableOAuthProviders) {
const dialogRef = this.dialog.open(DeleteConfirmDialog, {data: {provider}});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.deleteProvider(provider);
}
});
}
protected deleteProvider(provider: AvailableOAuthProviders) {
// todo: remove provider
}
}

@ -1,13 +1,11 @@
import {Component, Input} from '@angular/core'; import {Component, Input} from '@angular/core';
import {MatProgressSpinner} from "@angular/material/progress-spinner"; import {MatProgressSpinner} from "@angular/material/progress-spinner";
import {NgStyle} from "@angular/common";
@Component({ @Component({
selector: 'app-data-spinner', selector: 'app-data-spinner',
standalone: true, standalone: true,
imports: [ imports: [
MatProgressSpinner, MatProgressSpinner,
NgStyle
], ],
templateUrl: './data-spinner.component.html' templateUrl: './data-spinner.component.html'
}) })

@ -1,16 +1,13 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {MatToolbar} from "@angular/material/toolbar"; import {MatToolbar} from "@angular/material/toolbar";
import {MatAnchor, MatButton} from "@angular/material/button";
import {HasRoleDirective} from "@/directives/has-role.directive"; import {HasRoleDirective} from "@/directives/has-role.directive";
import {AuthRoles} from "@model/AuthRoles"; import {AuthRoles} from "@model/authRoles";
@Component({ @Component({
selector: 'app-header', selector: 'app-header',
standalone: true, standalone: true,
imports: [ imports: [
MatToolbar, MatToolbar,
MatButton,
MatAnchor,
HasRoleDirective HasRoleDirective
], ],
templateUrl: './header.component.html', templateUrl: './header.component.html',

@ -1,14 +1,13 @@
import {Component, EventEmitter, Input, Output} from '@angular/core'; import {Component, EventEmitter, Input, Output} from '@angular/core';
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component"; import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
import {MatIcon} from "@angular/material/icon"; import {MatIcon} from "@angular/material/icon";
import {MatButton, MatFabButton} from "@angular/material/button"; import {MatFabButton} from "@angular/material/button";
@Component({ @Component({
selector: 'app-loading-indicator', selector: 'app-loading-indicator',
standalone: true, standalone: true,
imports: [ imports: [
DataSpinnerComponent, DataSpinnerComponent,
MatButton,
MatIcon, MatIcon,
MatFabButton MatFabButton
], ],

@ -3,7 +3,6 @@ import {MatTableDataSource, MatTableModule} from "@angular/material/table";
import {MatIcon} from "@angular/material/icon"; import {MatIcon} from "@angular/material/icon";
import {DatePipe} from "@angular/common"; import {DatePipe} from "@angular/common";
import {addDays} from "@progress/kendo-date-math"; import {addDays} from "@progress/kendo-date-math";
import {MatDivider} from "@angular/material/divider";
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component"; import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
import {ScheduleResponse} from "@api/v1/scheduleResponse"; import {ScheduleResponse} from "@api/v1/scheduleResponse";
@ -23,7 +22,6 @@ interface Dictionary {
MatTableModule, MatTableModule,
MatIcon, MatIcon,
DatePipe, DatePipe,
MatDivider,
DataSpinnerComponent DataSpinnerComponent
], ],
templateUrl: './table.component.html', templateUrl: './table.component.html',

@ -23,7 +23,8 @@
Кабинет Кабинет
</mat-panel-title> </mat-panel-title>
</mat-expansion-panel-header> </mat-expansion-panel-header>
<mat-chip-listbox hideSingleSelectionIndicator (change)="onLectureHallSelected($event.value)" [formControl]="formLectureHalls" #lectureChip> <mat-chip-listbox hideSingleSelectionIndicator (change)="onLectureHallSelected($event.value)"
[formControl]="formLectureHalls" #lectureChip>
@for (lectureHall of lectureHallsFiltered; track $index) { @for (lectureHall of lectureHallsFiltered; track $index) {
<mat-chip-option [value]="lectureHall.id" color="accent"> <mat-chip-option [value]="lectureHall.id" color="accent">
{{ lectureHall.name }} {{ lectureHall.name }}

@ -1,5 +1,4 @@
import {Component, EventEmitter, ViewChild} from '@angular/core'; import {Component, EventEmitter, ViewChild} from '@angular/core';
import {AsyncPipe} from "@angular/common";
import {MatAccordion, MatExpansionModule, MatExpansionPanel} from "@angular/material/expansion"; import {MatAccordion, MatExpansionModule, MatExpansionPanel} from "@angular/material/expansion";
import {MatChipListbox, MatChipsModule} from "@angular/material/chips"; import {MatChipListbox, MatChipsModule} from "@angular/material/chips";
import {catchError} from "rxjs"; import {catchError} from "rxjs";
@ -22,7 +21,6 @@ enum Enclosure {
imports: [ imports: [
MatChipsModule, MatChipsModule,
MatExpansionModule, MatExpansionModule,
AsyncPipe,
ReactiveFormsModule, ReactiveFormsModule,
MatAccordion, MatAccordion,
LoadingIndicatorComponent LoadingIndicatorComponent

@ -1,11 +1,14 @@
<!--suppress CssInvalidPropertyValue --> <!--suppress CssInvalidPropertyValue -->
<button mat-button [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger" [id]="idButton" style="margin-bottom: 10px;">{{ textButton }}</button> <button mat-button [matMenuTriggerFor]="menu" #menuTrigger="matMenuTrigger" [id]="idButton"
style="margin-bottom: 10px;">{{ textButton }}
</button>
<mat-menu #menu="matMenu" [hasBackdrop]="false" class="menu-options"> <mat-menu #menu="matMenu" [hasBackdrop]="false" class="menu-options">
<div (click)="$event.stopPropagation()" (keydown)="$event.stopPropagation()" style="padding: 0 15px 15px"> <div (click)="$event.stopPropagation()" (keydown)="$event.stopPropagation()" style="padding: 0 15px 15px">
<div class="header-menu"> <div class="header-menu">
<mat-form-field appearance="outline" color="accent" style="display:flex;"> <mat-form-field appearance="outline" color="accent" style="display:flex;">
<input matInput placeholder="Поиск..." [(ngModel)]="searchQuery" [disabled]="data === null || data.length === 0"> <input matInput placeholder="Поиск..." [(ngModel)]="searchQuery"
[disabled]="data === null || data.length === 0">
<button mat-icon-button matSuffix (click)="clearSearchQuery()" [disabled]="data === null || data.length === 0"> <button mat-icon-button matSuffix (click)="clearSearchQuery()" [disabled]="data === null || data.length === 0">
<mat-icon style="color: var(--mdc-filled-button-label-text-color);">close</mat-icon> <mat-icon style="color: var(--mdc-filled-button-label-text-color);">close</mat-icon>
</button> </button>

@ -4,7 +4,6 @@ import {MatTab, MatTabGroup} from "@angular/material/tabs";
import {Observable} from "rxjs"; import {Observable} from "rxjs";
import {FormsModule, ReactiveFormsModule} from "@angular/forms"; import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {MatButton} from "@angular/material/button"; import {MatButton} from "@angular/material/button";
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
import {GroupComponent} from "@component/schedule/tabs/group/group.component"; import {GroupComponent} from "@component/schedule/tabs/group/group.component";
import {ProfessorComponent} from "@component/schedule/tabs/professor/professor.component"; import {ProfessorComponent} from "@component/schedule/tabs/professor/professor.component";
import {LectureHallComponent} from "@component/schedule/tabs/lecture-hall/lecture-hall.component"; import {LectureHallComponent} from "@component/schedule/tabs/lecture-hall/lecture-hall.component";
@ -15,7 +14,7 @@ import {DisciplineService} from "@api/v1/discipline.service";
import {LectureHallService} from "@api/v1/lectureHall.service"; import {LectureHallService} from "@api/v1/lectureHall.service";
import {GroupService} from "@api/v1/group.service"; import {GroupService} from "@api/v1/group.service";
import {ProfessorService} from "@api/v1/professor.service"; import {ProfessorService} from "@api/v1/professor.service";
import {AuthRoles} from "@model/AuthRoles"; import {AuthRoles} from "@model/authRoles";
import {HasRoleDirective} from "@/directives/has-role.directive"; import {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";
@ -36,7 +35,6 @@ export enum TabsSelect {
MatTab, MatTab,
ReactiveFormsModule, ReactiveFormsModule,
MatButton, MatButton,
DataSpinnerComponent,
GroupComponent, GroupComponent,
ProfessorComponent, ProfessorComponent,
LectureHallComponent, LectureHallComponent,

@ -1,6 +1,6 @@
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/authApiService";
import {AuthRoles} from "@model/AuthRoles"; import {AuthRoles} from "@model/authRoles";
import {catchError, of} from "rxjs"; import {catchError, of} from "rxjs";
@Directive({ @Directive({
@ -13,7 +13,8 @@ export class HasRoleDirective {
private templateRef: TemplateRef<any>, private templateRef: TemplateRef<any>,
private viewContainer: ViewContainerRef, private viewContainer: ViewContainerRef,
private authService: AuthApiService private authService: AuthApiService
) {} ) {
}
@Input() set appHasRole(role: AuthRoles) { @Input() set appHasRole(role: AuthRoles) {
this.viewContainer.clear(); this.viewContainer.clear();
@ -29,6 +30,6 @@ export class HasRoleDirective {
this.viewContainer.createEmbeddedView(this.templateRef); this.viewContainer.createEmbeddedView(this.templateRef);
else else
this.viewContainer.clear(); this.viewContainer.clear();
}) });
} }
} }

@ -8,7 +8,9 @@
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap" rel="stylesheet"> <link
href="https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"
rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head> </head>
<body class="mat-typography"> <body class="mat-typography">

@ -15,7 +15,8 @@ import {MatButton} from "@angular/material/button";
}) })
export class ConfirmDialogComponent { export class ConfirmDialogComponent {
constructor(public dialogRef: MatDialogRef<ConfirmDialogComponent>) { } constructor(public dialogRef: MatDialogRef<ConfirmDialogComponent>) {
}
protected onConfirm(): void { protected onConfirm(): void {
this.dialogRef.close(true); this.dialogRef.close(true);

@ -9,11 +9,14 @@
</mat-sidenav-content> </mat-sidenav-content>
<mat-sidenav-content> <mat-sidenav-content>
<app-table [currentWeek]="currentWeek" [startWeek]="startWeek" [data]="data" [isLoad]="isLoadTable" [disciplineWithWeeks]="disciplineWithWeeks"/> <app-table [currentWeek]="currentWeek" [startWeek]="startWeek" [data]="data" [isLoad]="isLoadTable"
[disciplineWithWeeks]="disciplineWithWeeks"/>
</mat-sidenav-content> </mat-sidenav-content>
<mat-sidenav-content style="display: flex; justify-content: space-between; align-items: center;"> <mat-sidenav-content style="display: flex; justify-content: space-between; align-items: center;">
<mat-checkbox (change)="changeDisciplineWeeksView($event.checked)" [checked]="disciplineWithWeeks">Показать недели в дисциплине</mat-checkbox> <mat-checkbox (change)="changeDisciplineWeeksView($event.checked)" [checked]="disciplineWithWeeks">Показать недели в
дисциплине
</mat-checkbox>
@if (excelImportLoader) { @if (excelImportLoader) {
<app-data-spinner/> <app-data-spinner/>
} @else { } @else {

@ -5,12 +5,12 @@ import {TabsComponent, TabsSelect} from "@component/schedule/tabs/tabs.component
import {catchError, Observable} from "rxjs"; import {catchError, Observable} from "rxjs";
import {ScheduleService} from "@api/v1/schedule.service"; import {ScheduleService} from "@api/v1/schedule.service";
import {ScheduleResponse} from "@api/v1/scheduleResponse"; import {ScheduleResponse} from "@api/v1/scheduleResponse";
import {PeriodTimes} from "@model/pairPeriodTime"; import {PairPeriodTime} from "@model/pairPeriodTime";
import {ActivatedRoute} from "@angular/router"; import {ActivatedRoute} from "@angular/router";
import {TabStorageService} from "@service/tab-storage.service"; import {TabStorageService} from "@service/tab-storage.service";
import {MatDialog} from "@angular/material/dialog"; import {MatDialog} from "@angular/material/dialog";
import {ConfirmDialogComponent} from "@page/schedule/confirm-dialog.component"; import {ConfirmDialogComponent} from "@page/schedule/confirm-dialog.component";
import {AuthRoles} from "@model/AuthRoles"; import {AuthRoles} from "@model/authRoles";
import {ImportService} from "@api/v1/import.service"; import {ImportService} from "@api/v1/import.service";
import {ScheduleRequest} from "@api/v1/scheduleRequest"; import {ScheduleRequest} from "@api/v1/scheduleRequest";
import {ToastrService} from "ngx-toastr"; import {ToastrService} from "ngx-toastr";
@ -50,7 +50,7 @@ export class ScheduleComponent {
protected data: ScheduleResponse[] = []; protected data: ScheduleResponse[] = [];
protected startTerm: Date; protected startTerm: Date;
protected isLoadTable: boolean = false; protected isLoadTable: boolean = false;
protected pairPeriods: PeriodTimes = {}; protected pairPeriods: PairPeriodTime | null = null;
protected disciplineWithWeeks: boolean = false; protected disciplineWithWeeks: boolean = false;
protected excelImportLoader: boolean = false; protected excelImportLoader: boolean = false;

@ -15,7 +15,7 @@
<mat-form-field color="accent"> <mat-form-field color="accent">
<mat-label>База данных</mat-label> <mat-label>База данных</mat-label>
<mat-select (valueChange)="onDatabaseChange($event)"> <mat-select (valueChange)="onDatabaseChange($event)" [value]="database">
<mat-option value="redis">Redis</mat-option> <mat-option value="redis">Redis</mat-option>
<mat-option value="memcached">Memcached</mat-option> <mat-option value="memcached">Memcached</mat-option>
</mat-select> </mat-select>
@ -29,7 +29,8 @@
<input matInput <input matInput
matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6' matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6'
required required
formControlName="server"> formControlName="server"
focusNext="serverNextFocus">
@if (databaseForm.get('server')?.hasError('required')) { @if (databaseForm.get('server')?.hasError('required')) {
<mat-error> <mat-error>
@ -49,7 +50,9 @@
<input matInput <input matInput
matTooltip="Укажите порт сервера" matTooltip="Укажите порт сервера"
required required
formControlName="port"> formControlName="port"
id="serverNextFocus"
focusNext="passwordNextFocus">
@if (databaseForm.get('port')?.hasError('required')) { @if (databaseForm.get('port')?.hasError('required')) {
<mat-error> <mat-error>
@ -69,7 +72,9 @@
<input matInput <input matInput
matTooltip="Укажите пароль" matTooltip="Укажите пароль"
formControlName="password" formControlName="password"
[type]="hidePass ? 'password' : 'text'"> [type]="hidePass ? 'password' : 'text'"
id="passwordNextFocus"
focusNext="nextButtonFocus">
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'" <button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
[attr.aria-pressed]="hidePass"> [attr.aria-pressed]="hidePass">

@ -8,6 +8,9 @@ import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip"; import {MatTooltip} from "@angular/material/tooltip";
import {MatIconButton} from "@angular/material/button"; import {MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon"; import {MatIcon} from "@angular/material/icon";
import {of} from "rxjs";
import {CacheType} from "@model/cacheType";
import {FocusNextDirective} from "@/directives/focus-next.directive";
@Component({ @Component({
selector: 'app-cache', selector: 'app-cache',
@ -19,7 +22,8 @@ import {MatIcon} from "@angular/material/icon";
MatInput, MatInput,
MatTooltip, MatTooltip,
MatIconButton, MatIconButton,
MatIcon MatIcon,
FocusNextDirective
], ],
templateUrl: './cache.component.html' templateUrl: './cache.component.html'
}) })
@ -40,6 +44,35 @@ export class CacheComponent {
this.databaseForm.valueChanges.subscribe(() => { this.databaseForm.valueChanges.subscribe(() => {
this.navigationService.setNextButtonState(this.databaseForm.valid); this.navigationService.setNextButtonState(this.databaseForm.valid);
}); });
this.api.cacheConfiguration().subscribe(response => {
if (!response)
return;
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => of(true);
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
this.databaseForm.patchValue({
server: response.server,
port: response.port,
password: response.password,
});
let type: string;
switch (response.type) {
case CacheType.Redis:
type = "redis";
break;
case CacheType.Memcached:
type = "memcached";
break;
}
this.database = type;
this.onDatabaseChange(type);
});
} }
onDatabaseChange(selectedDatabase: string) { onDatabaseChange(selectedDatabase: string) {

@ -28,7 +28,7 @@
@if (createAdminForm.get('user')?.hasError('pattern')) { @if (createAdminForm.get('user')?.hasError('pattern')) {
<mat-error> <mat-error>
Имя пользователя должен содержать латинские сиволы и цифры и быть не менее 4 символов Имя пользователя должен содержать латинские символы и цифры и быть не менее 4 символов
</mat-error> </mat-error>
} }
</mat-form-field> </mat-form-field>
@ -74,13 +74,23 @@
@if (createAdminForm.get('password')?.hasError('minlength')) { @if (createAdminForm.get('password')?.hasError('minlength')) {
<mat-error> <mat-error>
Пароль должен быть не менее 8 символов Пароль должен быть не менее {{ policy.minimumLength }} символов
</mat-error> </mat-error>
} }
@if (createAdminForm.get('password')?.hasError('pattern')) { @if (createAdminForm.get('password')?.hasError('pattern')) {
<mat-error> <mat-error>
Пароль должен содержать хотя бы один латинский символ верхнего регистра и специальный символ (!&#x40;#$%^&*) Пароль должен содержать:
@if (policy.requireLettersDifferentCase) {
* Латинские символы разных регистров
} @else if (policy.requireLetter) {
* Один латинский символ
} @else if (policy.requireDigit) {
* Одну цифру
}
@if (policy.requireSpecialCharacter) {
* специальный символ
}
</mat-error> </mat-error>
} }
</mat-form-field> </mat-form-field>
@ -105,5 +115,8 @@
</mat-error> </mat-error>
} }
</mat-form-field> </mat-form-field>
<OAuthProviders [canUnlink]="true" [activeProvidersId]="activatedProviders"
[message]="'Или можете получить часть данных от сторонних сервисов'"/>
</div> </div>
</form> </form>

@ -1,5 +1,5 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; import {FormBuilder, FormGroup, ReactiveFormsModule, ValidatorFn, Validators} from "@angular/forms";
import {NavigationService} from "@service/navigation.service"; import {NavigationService} from "@service/navigation.service";
import {passwordMatchValidator} from '@service/password-match.validator'; import {passwordMatchValidator} from '@service/password-match.validator';
import SetupService from "@api/v1/setup.service"; import SetupService from "@api/v1/setup.service";
@ -9,6 +9,10 @@ import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip"; import {MatTooltip} from "@angular/material/tooltip";
import {MatIconButton} from "@angular/material/button"; import {MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon"; import {MatIcon} from "@angular/material/icon";
import AuthApiService from "@api/v1/authApiService";
import {PasswordPolicy} from "@model/passwordPolicy";
import {OAuthProviders} from "@component/OAuthProviders/OAuthProviders";
import {OAuthProvider} from "@model/oAuthProvider";
@Component({ @Component({
selector: 'app-create-admin', selector: 'app-create-admin',
@ -20,15 +24,19 @@ import {MatIcon} from "@angular/material/icon";
MatInput, MatInput,
MatTooltip, MatTooltip,
MatIconButton, MatIconButton,
MatIcon MatIcon,
OAuthProviders
], ],
templateUrl: './create-admin.component.html' templateUrl: './create-admin.component.html',
providers: [AuthApiService]
}) })
export class CreateAdminComponent { export class CreateAdminComponent {
protected createAdminForm!: FormGroup; protected createAdminForm!: FormGroup;
protected hidePass = true; protected hidePass = true;
protected hideRetypePass = true; protected hideRetypePass = true;
protected policy!: PasswordPolicy;
protected activatedProviders: OAuthProvider[] = [];
constructor( constructor(
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) { private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
@ -41,12 +49,6 @@ export class CreateAdminComponent {
{validators: passwordMatchValidator('password', 'retype')} {validators: passwordMatchValidator('password', 'retype')}
); );
this.createAdminForm.get('password')?.setValidators([Validators.required,
Validators.pattern(/[A-Z]/),
Validators.pattern(/[!@#$%^&*]/),
Validators.minLength(8)
]);
this.navigationService.setNextButtonState(false); this.navigationService.setNextButtonState(false);
this.createAdminForm.valueChanges.subscribe(() => { this.createAdminForm.valueChanges.subscribe(() => {
this.navigationService.setNextButtonState(this.createAdminForm.valid); this.navigationService.setNextButtonState(this.createAdminForm.valid);
@ -60,6 +62,44 @@ export class CreateAdminComponent {
} }
); );
}; };
this.api.passwordPolicyConfiguration().subscribe(policy => {
this.policy = policy;
const passwordValidators = this.createPasswordValidators(policy);
this.createAdminForm.get('password')?.setValidators(passwordValidators);
this.createAdminForm.get('password')?.updateValueAndValidity();
});
this.api.adminConfiguration().subscribe(configuration => {
if (configuration) {
this.createAdminForm.get('email')?.setValue(configuration.email);
this.createAdminForm.get('user')?.setValue(configuration.username);
this.activatedProviders = configuration.usedOAuthProviders;
}
});
}
private createPasswordValidators(policy: PasswordPolicy): ValidatorFn[] {
const validators: ValidatorFn[] = [Validators.required];
if (policy.minimumLength) {
validators.push(Validators.minLength(policy.minimumLength));
}
if (policy.requireLettersDifferentCase) {
validators.push(Validators.pattern(/(?=.*[a-z])(?=.*[A-Z])/));
} else if (policy.requireLetter) {
validators.push(Validators.pattern(/[A-Za-z]/));
} else if (policy.requireDigit) {
validators.push(Validators.pattern(/\d/));
}
if (policy.requireSpecialCharacter) {
validators.push(Validators.pattern(/[!@#$%^&*(),.?":{}|<>]/));
}
return validators;
} }
protected togglePassword(event: MouseEvent) { protected togglePassword(event: MouseEvent) {

@ -17,9 +17,9 @@
</p> </p>
<mat-form-field color="accent"> <mat-form-field color="accent">
<mat-label>База данных</mat-label> <mat-label>База данных</mat-label>
<mat-select (valueChange)="onDatabaseChange($event)"> <mat-select (valueChange)="onDatabaseChange($event)" [value]="database">
<mat-option value="SetMysql">MySQL</mat-option> <mat-option value="mysql">MySQL</mat-option>
<mat-option value="SetPsql">PostgreSQL</mat-option> <mat-option value="psql">PostgreSQL</mat-option>
<mat-option value="sqlite">Sqlite</mat-option> <mat-option value="sqlite">Sqlite</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
@ -57,7 +57,8 @@
<input matInput <input matInput
matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6' matTooltip='Укажите сервер в формате: "winsomnia.net" или ip адреса формата IPv4 или IPv6'
required required
formControlName="server"> formControlName="server"
focusNext="portNextFocus">
@if (databaseForm.get('server')?.hasError('required')) { @if (databaseForm.get('server')?.hasError('required')) {
<mat-error> <mat-error>
@ -77,7 +78,9 @@
<input matInput <input matInput
matTooltip="Укажите порт сервера" matTooltip="Укажите порт сервера"
required required
formControlName="port"> formControlName="port"
id="portNextFocus"
focusNext="databaseNextFocus">
@if (databaseForm.get('port')?.hasError('required')) { @if (databaseForm.get('port')?.hasError('required')) {
<mat-error> <mat-error>
@ -97,7 +100,9 @@
<input matInput <input matInput
matTooltip="Укажите название базы данных" matTooltip="Укажите название базы данных"
required required
formControlName="database_name"> formControlName="database_name"
id="databaseNextFocus"
focusNext="userNextFocus">
@if (databaseForm.get('database_name')?.hasError('required')) { @if (databaseForm.get('database_name')?.hasError('required')) {
<mat-error> <mat-error>
@ -117,7 +122,9 @@
<input matInput <input matInput
matTooltip="Укажите пользователя, который имеет доступ к базе данных" matTooltip="Укажите пользователя, который имеет доступ к базе данных"
required required
formControlName="user"> formControlName="user"
id="userNextFocus"
focusNext="passwordNextFocus">
@if (databaseForm.get('user')?.hasError('required')) { @if (databaseForm.get('user')?.hasError('required')) {
<mat-error> <mat-error>
@ -137,7 +144,9 @@
<input matInput <input matInput
matTooltip="Укажите пароль" matTooltip="Укажите пароль"
formControlName="password" formControlName="password"
[type]="hidePass ? 'password' : 'text'"> [type]="hidePass ? 'password' : 'text'"
id="passwordNextFocus"
focusNext="nextButtonFocus">
<button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'" <button mat-icon-button matSuffix (click)="togglePassword($event)" [attr.aria-label]="'Hide password'"
[attr.aria-pressed]="hidePass"> [attr.aria-pressed]="hidePass">

@ -2,14 +2,17 @@ import {Component} from '@angular/core';
import {NavigationService} from "@service/navigation.service"; import {NavigationService} from "@service/navigation.service";
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms"; import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
import SetupService from "@api/v1/setup.service"; import SetupService from "@api/v1/setup.service";
import {DatabaseRequest} from "@api/v1/databaseRequest";
import {MatFormFieldModule} from "@angular/material/form-field"; import {MatFormFieldModule} from "@angular/material/form-field";
import {MatSelectModule} from "@angular/material/select"; import {MatSelectModule} from "@angular/material/select";
import {MatInput} from "@angular/material/input"; import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip"; import {MatTooltip} from "@angular/material/tooltip";
import {MatIconButton} from "@angular/material/button"; import {MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon" import {MatIcon} from "@angular/material/icon";
import {MatCheckbox} from "@angular/material/checkbox"; import {MatCheckbox} from "@angular/material/checkbox";
import {DatabaseRequest} from "@api/v1/configuration/databaseRequest";
import {of} from "rxjs";
import {DatabaseType} from "@model/databaseType";
import {FocusNextDirective} from "@/directives/focus-next.directive";
@Component({ @Component({
selector: 'app-database', selector: 'app-database',
@ -22,7 +25,8 @@ import {MatCheckbox} from "@angular/material/checkbox";
MatTooltip, MatTooltip,
MatIconButton, MatIconButton,
MatIcon, MatIcon,
MatCheckbox MatCheckbox,
FocusNextDirective
], ],
templateUrl: './database.component.html' templateUrl: './database.component.html'
}) })
@ -49,6 +53,42 @@ export class DatabaseComponent {
this.databaseForm.valueChanges.subscribe(() => { this.databaseForm.valueChanges.subscribe(() => {
this.navigationService.setNextButtonState(this.databaseForm.valid); this.navigationService.setNextButtonState(this.databaseForm.valid);
}); });
this.api.databaseConfiguration().subscribe(response => {
if (!response)
return;
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => of(true);
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
this.databaseForm.patchValue({
server: response.server,
port: response.port,
database_name: response.database,
user: response.user,
ssl: response.ssl,
password: response.password,
folder: response.pathToDatabase
});
let type: string;
switch (response.type) {
case DatabaseType.Mysql:
type = "mysql";
break;
case DatabaseType.PostgresSql:
type = "psql";
break;
case DatabaseType.Sqlite:
type = "sqlite";
break;
}
this.database = type;
this.onDatabaseChange(type);
});
} }
private createForm(database: string) { private createForm(database: string) {

@ -33,7 +33,3 @@
</mat-form-field> </mat-form-field>
</div> </div>
</form> </form>
<div style="display: flex; justify-content: center;">
<button mat-flat-button color="accent" (click)="skipButton()">Пропустить</button>
</div>

@ -6,8 +6,8 @@ import {MatFormFieldModule} from "@angular/material/form-field";
import {MatSelectModule} from "@angular/material/select"; import {MatSelectModule} from "@angular/material/select";
import {MatInput} from "@angular/material/input"; import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip"; import {MatTooltip} from "@angular/material/tooltip";
import {MatButton, MatIconButton} from "@angular/material/button";
import {MatCheckbox} from "@angular/material/checkbox"; import {MatCheckbox} from "@angular/material/checkbox";
import {of} from "rxjs";
@Component({ @Component({
selector: 'app-logging', selector: 'app-logging',
@ -18,9 +18,7 @@ import {MatCheckbox} from "@angular/material/checkbox";
MatSelectModule, MatSelectModule,
MatInput, MatInput,
MatTooltip, MatTooltip,
MatIconButton, MatCheckbox
MatCheckbox,
MatButton
], ],
templateUrl: './logging.component.html' templateUrl: './logging.component.html'
@ -39,12 +37,11 @@ export class LoggingComponent {
} }
} }
protected skipButton() {
this.navigationService.skipNavigation.emit(() => this.api.setLogging(null));
}
constructor( constructor(
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) { private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => this.api.setLogging(null);
this.loggingSettings = this.formBuilder.group({ this.loggingSettings = this.formBuilder.group({
enabled: [true, Validators.required], enabled: [true, Validators.required],
logPath: [''], logPath: [''],
@ -59,11 +56,23 @@ export class LoggingComponent {
this.navigationService.nextButtonAction = () => { this.navigationService.nextButtonAction = () => {
return this.api.setLogging({ return this.api.setLogging({
"enableLogToFile": this.loggingSettings.get('cron')?.value, "enableLogToFile": this.loggingSettings.get('enabled')?.value,
"logFileName": this.loggingSettings.get('logName')?.value, "logFileName": this.loggingSettings.get('logName')?.value,
"logFilePath": this.loggingSettings.get('logPath')?.value "logFilePath": this.loggingSettings.get('logPath')?.value
} }
); );
}; };
api.loggingConfiguration().subscribe(x => {
if (!x)
return;
this.navigationService.skipButtonAction = () => of(true);
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
this.loggingSettings.get('enabled')?.setValue(x.enableLogToFile);
this.loggingSettings.get('logName')?.setValue(x.logFileName);
this.loggingSettings.get('logPath')?.setValue(x.logFilePath);
});
} }
} }

@ -0,0 +1,48 @@
<h1>Настройка политики паролей</h1>
<hr/>
<p class="mat-body-2 secondary">
Задайте параметры для обеспечения безопасности паролей.
<br/>
Можно установить минимальную длину пароля и другие требования, чтобы усилить защиту учетных записей.
</p>
<form [formGroup]="policyForm">
<p>
Введите данные для настройки политики паролей:
</p>
<div style="display:flex; flex-direction: column;">
<mat-form-field color="accent">
<mat-label>Минимальная длина пароля</mat-label>
<input matInput
type="number"
matTooltip="Укажите минимальное количество длины пароля"
formControlName="minimumLength">
@if (policyForm.get('minimumLength')?.hasError('min')) {
<mat-error>
Пароль не может быть меньше 6 символов
</mat-error>
}
@if (policyForm.get('minimumLength')?.hasError('max')) {
<mat-error>
Пароль не может быть больше 12 символов
</mat-error>
}
</mat-form-field>
<mat-checkbox formControlName="requireLetter">
Требовать наличие букв в пароле
</mat-checkbox>
<mat-checkbox formControlName="requireLettersDifferentCase">
Требовать буквы разного регистра (заглавные и строчные)
</mat-checkbox>
<mat-checkbox formControlName="requireDigit">
Требовать наличие цифр в пароле
</mat-checkbox>
<mat-checkbox formControlName="requireSpecialCharacter">
Требовать наличие специальных символов (например, !, $, #)
</mat-checkbox>
</div>
</form>

@ -0,0 +1,77 @@
import {Component} from '@angular/core';
import {MatCheckbox} from "@angular/material/checkbox";
import {MatError, MatFormField, MatLabel} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip";
import {FormBuilder, FormGroup, ReactiveFormsModule, Validators} from "@angular/forms";
import {NavigationService} from "@service/navigation.service";
import SetupService from "@api/v1/setup.service";
import {of} from "rxjs";
@Component({
selector: 'app-password-policy',
standalone: true,
imports: [
MatCheckbox,
MatFormField,
MatInput,
MatLabel,
MatTooltip,
ReactiveFormsModule,
MatError
],
templateUrl: './password-policy.component.html'
})
export class PasswordPolicyComponent {
protected policyForm!: FormGroup;
constructor(
private navigationService: NavigationService, private formBuilder: FormBuilder, private api: SetupService) {
this.policyForm = this.formBuilder.group({
minimumLength: ['', [
Validators.required,
Validators.min(6),
Validators.max(12)
]],
requireLetter: [false],
requireLettersDifferentCase: [false],
requireDigit: [false],
requireSpecialCharacter: [false]
});
this.api.passwordPolicyConfiguration().subscribe(response => {
if (!response)
return;
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => of(true);
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
this.policyForm.patchValue({
minimumLength: response.minimumLength,
requireLetter: response.requireLetter,
requireLettersDifferentCase: response.requireLettersDifferentCase,
requireDigit: response.requireDigit,
requireSpecialCharacter: response.requireSpecialCharacter
});
});
this.navigationService.setNextButtonState(false);
this.policyForm.valueChanges.subscribe(() => {
this.navigationService.setNextButtonState(this.policyForm.valid);
});
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => this.api.setPasswordPolicy(null);
this.navigationService.nextButtonAction = () => {
return this.api.setPasswordPolicy(({
minimumLength: this.policyForm.get('minimumLength')?.value,
requireLetter: this.policyForm.get('requireLetter')?.value,
requireLettersDifferentCase: this.policyForm.get('requireLettersDifferentCase')?.value,
requireDigit: this.policyForm.get('requireDigit')?.value,
requireSpecialCharacter: this.policyForm.get('requireSpecialCharacter')?.value
}));
};
};
}

@ -7,9 +7,9 @@ import {MatFormFieldModule} from "@angular/material/form-field";
import {MatSelectModule} from "@angular/material/select"; import {MatSelectModule} from "@angular/material/select";
import {MatInput} from "@angular/material/input"; import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip"; import {MatTooltip} from "@angular/material/tooltip";
import {MatIconButton} from "@angular/material/button";
import {MatIcon} from "@angular/material/icon";
import {MatDatepickerModule} from "@angular/material/datepicker"; import {MatDatepickerModule} from "@angular/material/datepicker";
import {DateOnly} from "@model/dateOnly";
import {of} from "rxjs";
@Component({ @Component({
selector: 'app-schedule-conf', selector: 'app-schedule-conf',
@ -20,8 +20,6 @@ import {MatDatepickerModule} from "@angular/material/datepicker";
MatSelectModule, MatSelectModule,
MatInput, MatInput,
MatTooltip, MatTooltip,
MatIconButton,
MatIcon,
MatDatepickerModule, MatDatepickerModule,
MatNativeDateModule MatNativeDateModule
], ],
@ -55,5 +53,19 @@ export class ScheduleComponent {
} }
); );
}; };
api.scheduleConfiguration().subscribe(x => {
if (!x)
return;
this.scheduleSettings.get('startTerm')?.setValue(new DateOnly(x.startTerm).date);
this.scheduleSettings.get('cron')?.setValue(x.cronUpdateSchedule);
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => of(true);
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
});
} }
} }

@ -5,20 +5,23 @@
<div class="setup-navigation"> <div class="setup-navigation">
<div> <div>
<button mat-flat-button color="accent" <button mat-flat-button color="accent"
[disabled]="previousButtonDisabled" [hidden]="getIndex <= 2"
[hidden]="previousButtonRoute === ''" (click)="onPreviousClick()">
(click)="onPreviousClick()"
[routerLink]="previousButtonRoute">
Назад Назад
</button> </button>
</div> </div>
@if (!skipButtonDisabled) {
<button mat-flat-button color="accent" (click)="onSkipClick()">Пропустить</button>
}
@if (loaderActive) { @if (loaderActive) {
<app-data-spinner [scale]="40"/> <app-data-spinner [scale]="40"/>
} @else { } @else {
<button mat-flat-button color="accent" <button mat-flat-button color="accent"
[disabled]="nextButtonDisabled" [disabled]="nextButtonDisabled"
(click)="onNextClick()"> (click)="onNextClick()"
id="nextButtonFocus">
@if (getIndex === routes.length - 1) { @if (getIndex === routes.length - 1) {
Завершить Завершить
} @else { } @else {

@ -1,12 +1,13 @@
import {Component, ViewEncapsulation} from '@angular/core'; import {Component, ViewEncapsulation} from '@angular/core';
import {MatSidenavModule} from "@angular/material/sidenav"; import {MatSidenavModule} from "@angular/material/sidenav";
import {Router, RouterLink, RouterOutlet} from "@angular/router"; import {Router, RouterOutlet} from "@angular/router";
import {MatCard} from "@angular/material/card"; import {MatCard} from "@angular/material/card";
import {MatButton} from "@angular/material/button"; import {MatButton} from "@angular/material/button";
import {NavigationService} from "@service/navigation.service"; import {NavigationService} from "@service/navigation.service";
import {catchError, Observable} from "rxjs"; import {catchError, Observable} from "rxjs";
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component"; import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
import SetupService from "@api/v1/setup.service"; import SetupService from "@api/v1/setup.service";
import {ToastrService} from "ngx-toastr";
@Component({ @Component({
selector: 'app-setup', selector: 'app-setup',
@ -16,7 +17,6 @@ import SetupService from "@api/v1/setup.service";
RouterOutlet, RouterOutlet,
MatCard, MatCard,
MatButton, MatButton,
RouterLink,
DataSpinnerComponent DataSpinnerComponent
], ],
templateUrl: './setup.component.html', templateUrl: './setup.component.html',
@ -26,64 +26,71 @@ import SetupService from "@api/v1/setup.service";
}) })
export class SetupComponent { export class SetupComponent {
protected previousButtonDisabled: boolean = false;
protected previousButtonRoute: string = '';
protected nextButtonDisabled: boolean = false; protected nextButtonDisabled: boolean = false;
protected nextButtonRoute!: string; protected skipButtonDisabled: boolean = false;
protected loaderActive: boolean = false; protected loaderActive: boolean = false;
protected routes: Array<string> = ['', 'welcome', 'database', 'cache', 'create-admin', 'schedule', 'logging', 'summary']; protected routes: Array<string> = ['', 'welcome', 'database', 'cache', 'password-policy', 'schedule', 'logging', 'create-admin', 'two-factor', 'summary'];
private index: number = 1; private index: number = 1;
protected get getIndex() { protected get getIndex() {
return this.index; return this.index;
} }
constructor(private router: Router, private navigationService: NavigationService, api: SetupService) { constructor(private router: Router, private navigationService: NavigationService, api: SetupService, private notify: ToastrService) {
api.isConfigured().subscribe(x => { api.isConfigured().subscribe(x => {
if (x) if (x) this.router.navigate(['/']).then();
this.router.navigate(['/']).then();
}); });
if (!this.router.url.includes(this.routes[this.index])) if (!this.router.url.includes(this.routes[this.index])) {
this.router.navigate(['setup/', this.routes[this.index]]).then(); this.router.navigate(['setup/', this.routes[this.index]]).then();
}
this.setRoutes(); this.initializeButtonSubscriptions();
this.navigationService.autoSkipNavigation$.subscribe(action => {
if (!this.navigationService.isNavigationUserInitiated) {
this.executeAction(action);
}
});
}
private initializeButtonSubscriptions() {
this.navigationService.nextButtonState$.subscribe(state => { this.navigationService.nextButtonState$.subscribe(state => {
this.nextButtonDisabled = !state; this.nextButtonDisabled = !state;
}); });
this.navigationService.skipNavigation.subscribe(action => { this.navigationService.skipButtonState$.subscribe(state => {
this.executeAction(action); this.skipButtonDisabled = !state;
}); });
} }
private setRoutes() {
this.previousButtonRoute = this.routes[this.index - 1];
this.nextButtonRoute = this.routes[this.index + 1];
}
private executeAction(action: () => Observable<boolean>) { private executeAction(action: () => Observable<boolean>) {
this.loaderActive = true; this.loaderActive = true;
action().pipe( action()
.pipe(
catchError(error => { catchError(error => {
this.nextButtonDisabled = true; this.nextButtonDisabled = true;
this.loaderActive = false; this.loaderActive = false;
throw error; throw error;
}) })
) )
.subscribe(x => { .subscribe(success => {
this.nextButtonDisabled = x; if (success) {
this.loaderActive = !x; this.moveToNextPage();
if (x) { } else {
if (this.index < this.routes.length - 1) { this.notify.error('Некорректно введены данные');
this.router.navigate(['setup/', this.nextButtonRoute]).then(); this.nextButtonDisabled = true;
this.index++; }
this.setRoutes();
} else this.loaderActive = false;
this.router.navigate(['/']).then(); });
}
protected onSkipClick() {
this.navigationService.skipButtonAction().subscribe(success => {
if (success) {
this.moveToNextPage();
} }
}); });
} }
@ -93,9 +100,29 @@ export class SetupComponent {
} }
protected onPreviousClick() { protected onPreviousClick() {
if (this.index - 1 > 0) { this.navigationService.setUserInitiatedNavigation(true);
this.moveToPreviousPage();
}
private moveToNextPage() {
if (this.index < this.routes.length - 1) {
this.index++;
this.router.navigate(['setup/', this.routes[this.index]]).then();
this.initializePage();
} else {
this.router.navigate(['/']).then();
}
}
private moveToPreviousPage() {
if (this.index > 0) {
this.index--; this.index--;
this.setRoutes(); this.router.navigate(['setup/', this.routes[this.index]]).then();
this.initializePage();
} }
} }
private initializePage() {
this.navigationService.resetButtonStates();
}
} }

@ -8,7 +8,8 @@
<h4 style="margin-bottom: -5px;">Что дальше?</h4> <h4 style="margin-bottom: -5px;">Что дальше?</h4>
<p class="mat-body-2 secondary"> <p class="mat-body-2 secondary">
Теперь, когда основные настройки завершены, вы можете начать использовать систему с уверенностью, что все работает правильно. Теперь, когда основные настройки завершены, вы можете начать использовать систему с уверенностью, что все работает
правильно.
</p> </p>
<h4 style="margin-bottom: -5px;">Изменение настроек</h4> <h4 style="margin-bottom: -5px;">Изменение настроек</h4>
@ -17,6 +18,173 @@
Для изменения настроек перейдите в раздел настроек программы, который доступен в меню пользователя. Для изменения настроек перейдите в раздел настроек программы, который доступен в меню пользователя.
</p> </p>
<style>
.marginBottom {
margin-bottom: 25px;
}
.marginBottom h2 {
margin-bottom: 5px;
}
</style>
<h4 style="margin-bottom: -5px;">Ваши настройки:</h4>
<br/>
<mat-accordion>
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Настройки</mat-panel-title>
</mat-expansion-panel-header>
@if (databaseConfig) {
<div class="marginBottom">
<h2>Конфигурация базы данных</h2>
<div class="config-item">
Тип: {{ databaseConfig.type }}
</div>
@if (databaseConfig.server) {
<div>
Сервер: {{ databaseConfig.server }}
</div>
}
@if (databaseConfig.port) {
<div>
Порт: {{ databaseConfig.port }}
</div>
}
@if (databaseConfig.database) {
<div>
База данных: {{ databaseConfig.database }}
</div>
}
@if (databaseConfig.user) {
<div>
Пользователь: {{ databaseConfig.user }}
</div>
}
@if (databaseConfig.ssl) {
<div>
SSL: {{ databaseConfig.ssl ? 'Yes' : 'No' }}
</div>
}
@if (databaseConfig.password) {
<div>
Пароль: ***
</div>
}
@if (databaseConfig.pathToDatabase) {
<div>
Путь к базе данных: {{ databaseConfig.pathToDatabase }}
</div>
}
</div>
}
@if (cacheConfig) {
<div class="marginBottom">
<h2>Конфигурация кэша</h2>
<div class="config-item">
Тип: {{ cacheConfig.type }}
</div>
@if (cacheConfig.server) {
<div>
Сервер: {{ cacheConfig.server }}
</div>
}
@if (cacheConfig.port) {
<div>
Порт: {{ cacheConfig.port }}
</div>
}
@if (cacheConfig.password) {
<div>
Пароль: ***
</div>
}
</div>
}
@if (passwordPolicyConfig) {
<div class="marginBottom">
<h2>Политика паролей</h2>
<div>
Минимальная длина: {{ passwordPolicyConfig.minimumLength }}
</div>
<div>
Требуется буква: {{ passwordPolicyConfig.requireLetter ? 'Да' : 'Нет' }}
</div>
<div>
Требуются буквы разных регистров: {{ passwordPolicyConfig.requireLettersDifferentCase ? 'Да' : 'Нет' }}
</div>
<div>
Требуется число: {{ passwordPolicyConfig.requireDigit ? 'Да' : 'Нет' }}
</div>
<div>
Требуется специальный символ: {{ passwordPolicyConfig.requireSpecialCharacter ? 'Да' : 'Нет' }}
</div>
</div>
}
@if (adminConfig) {
<div class="marginBottom">
<h2>Конфигурация администратора</h2>
<div>
Email: {{ adminConfig.email }}
</div>
<div>
Username: {{ adminConfig.username }}
</div>
<div>
Двухфакторный аутентификатор
Включен: {{ adminConfig.twoFactorAuthenticatorEnabled ? 'Да' : 'Нет' }}
</div>
</div>
}
@if (loggingConfig) {
<div class="marginBottom">
<h2>Конфигурация ведения журнала</h2>
<div>
Включить запись журнала в файл: {{ loggingConfig.enableLogToFile ? 'Да' : 'Нет' }}
</div>
@if (loggingConfig.logFileName) {
<div>
Имя файла журнала: {{ loggingConfig.logFileName }}
</div>
}
@if (loggingConfig.logFilePath) {
<div>
Путь к файлу журнала: {{ loggingConfig.logFilePath }}
</div>
}
</div>
}
@if (scheduleConfig) {
<div class="marginBottom">
<h2>Настройка расписания</h2>
<div>
Расписание обновлений Cron: {{ scheduleConfig.cronUpdateSchedule }}
</div>
<div>
Дата начала семестра: {{ scheduleConfig.startTerm }}
</div>
</div>
}
</mat-expansion-panel>
</mat-accordion>
<p class="mat-body-2 secondary">
Помните, что вы всегда можете изменить некоторые настройки позже через интерфейс программы.
Для изменения настроек перейдите в раздел настроек программы, который доступен в меню пользователя.
</p>
<p class="mat-h3" style="color: red;font-weight: lighter;"> <p class="mat-h3" style="color: red;font-weight: lighter;">
Для того, чтобы настройки были применены нажмите кнопку "Завершить" и перезагрузите приложение Для того, чтобы настройки были применены нажмите кнопку "Завершить" и перезагрузите приложение
</p> </p>

@ -1,26 +1,60 @@
import {Component} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {MatButton} from "@angular/material/button";
import {NavigationService} from "@service/navigation.service"; import {NavigationService} from "@service/navigation.service";
import {MatFormFieldModule} from "@angular/material/form-field"; import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import SetupService from "@api/v1/setup.service"; import SetupService from "@api/v1/setup.service";
import {DatabaseResponse} from "@api/v1/configuration/databaseResponse";
import {CacheResponse} from "@api/v1/configuration/cacheResponse";
import {PasswordPolicy} from "@model/passwordPolicy";
import {UserResponse} from "@api/v1/userResponse";
import {LoggingRequest} from "@api/v1/configuration/loggingRequest";
import {ScheduleConfigurationRequest} from "@api/v1/configuration/scheduleConfigurationRequest";
import {MatExpansionModule} from "@angular/material/expansion";
@Component({ @Component({
selector: 'app-summary', selector: 'app-summary',
standalone: true, standalone: true,
imports: [ imports: [MatFormFieldModule, MatExpansionModule],
MatButton,
MatFormFieldModule,
MatInput
],
templateUrl: './summary.component.html' templateUrl: './summary.component.html'
}) })
export class SummaryComponent { export class SummaryComponent implements OnInit {
databaseConfig: DatabaseResponse | undefined;
cacheConfig: CacheResponse | undefined;
passwordPolicyConfig: PasswordPolicy | undefined;
adminConfig: UserResponse | undefined;
loggingConfig: LoggingRequest | undefined;
scheduleConfig: ScheduleConfigurationRequest | undefined;
constructor(private navigationService: NavigationService, private api: SetupService) { constructor(private navigationService: NavigationService, private api: SetupService) {
this.navigationService.nextButtonAction = () => { this.navigationService.nextButtonAction = () => {
return this.api.submit(); return this.api.submit();
}; };
this.navigationService.setNextButtonState(true); this.navigationService.setNextButtonState(true);
} }
ngOnInit(): void {
this.api.databaseConfiguration().subscribe(config => {
this.databaseConfig = config;
});
this.api.cacheConfiguration().subscribe(config => {
this.cacheConfig = config;
});
this.api.passwordPolicyConfiguration().subscribe(config => {
this.passwordPolicyConfig = config;
});
this.api.adminConfiguration().subscribe(config => {
this.adminConfig = config;
});
this.api.loggingConfiguration().subscribe(config => {
this.loggingConfig = config;
});
this.api.scheduleConfiguration().subscribe(config => {
this.scheduleConfig = config;
});
}
} }

@ -0,0 +1,50 @@
<h1>Настройка двухфакторной аутентификации (2FA)</h1>
<hr/>
<p class="mat-body-2 secondary">
На этой странице вы можете настроить двухфакторную аутентификацию (2FA) для повышения безопасности вашего аккаунта.
</p>
<p class="mat-body-2 secondary">
Чтобы настроить 2FA, отсканируйте QR-код или введите секретный код в приложение Google Authenticator, Authy или другие
подобные приложения для двухфакторной аутентификации.
</p>
<p class="mat-body-2 secondary">
Если вы не хотите настраивать 2FA сейчас, вы можете пропустить этот шаг.
</p>
<h3>Ваш код: <i><strong>{{ secret }}</strong></i></h3>
<div>
<img [src]="totpImage" alt="totp-qr-code"/>
</div>
<form [formGroup]="twoFactorForm">
<p>
Введите ключ из приложения:
</p>
<mat-form-field color="accent">
<mat-label>Код из приложения</mat-label>
<input matInput
matTooltip='Укажите код в цифровом формате'
required
formControlName="code">
@if (twoFactorForm.get('code')?.hasError('required')) {
<mat-error>
Код является <i>обязательным</i>
</mat-error>
}
@if (twoFactorForm.get('code')?.hasError('minlength')) {
<mat-error>
Код должен быть не меньше 6 символов
</mat-error>
}
@if (twoFactorForm.get('code')?.hasError('pattern')) {
<mat-error>
Код должен содержать только цифры
</mat-error>
}
</mat-form-field>
</form>

@ -0,0 +1,55 @@
import {Component} from '@angular/core';
import {FormBuilder, FormGroup, FormsModule, ReactiveFormsModule, ValidatorFn, Validators} from "@angular/forms";
import {NavigationService} from "@service/navigation.service";
import SetupService from "@api/v1/setup.service";
import {of} from "rxjs";
import {MatError, MatFormField, MatLabel} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input";
import {MatTooltip} from "@angular/material/tooltip";
import SecurityService from "@api/v1/securityService";
@Component({
selector: 'app-two-factor',
imports: [
FormsModule,
ReactiveFormsModule,
MatError,
MatFormField,
MatInput,
MatLabel,
MatTooltip
],
templateUrl: './two-factor.component.html',
providers: [SecurityService]
})
export class TwoFactorComponent {
protected twoFactorForm!: FormGroup;
protected secret!: string;
protected totpImage!: string;
constructor(
private navigationService: NavigationService, private formBuilder: FormBuilder, api: SetupService, apiSecurity: SecurityService) {
api.generateTotpKey().subscribe(x => {
this.secret = x;
});
api.adminConfiguration().subscribe(x => {
this.totpImage = apiSecurity.generateTotpQrCode(this.secret, x.username);
if (x.twoFactorAuthenticatorEnabled)
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
});
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => of(true);
const validators: ValidatorFn[] = [Validators.required, Validators.minLength(6), Validators.pattern('^[0-9]*$')];
this.twoFactorForm = this.formBuilder.group({
code: ['', validators],
});
this.navigationService.nextButtonAction = () => api.verifyTotp(this.twoFactorForm.get('code')?.value);
this.navigationService.setNextButtonState(this.twoFactorForm.valid);
this.twoFactorForm.valueChanges.subscribe(() => {
this.navigationService.setNextButtonState(this.twoFactorForm.valid);
});
}
}

@ -1,22 +1,19 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {MatButton} from "@angular/material/button";
import {NavigationService} from "@service/navigation.service"; import {NavigationService} from "@service/navigation.service";
import {MatFormFieldModule} from "@angular/material/form-field"; import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInput} from "@angular/material/input"; import {MatInput} from "@angular/material/input";
import {AsyncPipe} from "@angular/common";
import {FormControl, ReactiveFormsModule, Validators} from "@angular/forms"; import {FormControl, ReactiveFormsModule, Validators} from "@angular/forms";
import SetupService from "@api/v1/setup.service"; import SetupService from "@api/v1/setup.service";
import {environment} from "@environment"; import {environment} from "@environment";
import {AvailableVersion} from "@api/api.service"; import {AvailableVersion} from "@api/api.service";
import {of} from "rxjs";
@Component({ @Component({
selector: 'app-welcome', selector: 'app-welcome',
standalone: true, standalone: true,
imports: [ imports: [
MatButton,
MatFormFieldModule, MatFormFieldModule,
MatInput, MatInput,
AsyncPipe,
ReactiveFormsModule ReactiveFormsModule
], ],
templateUrl: './welcome.component.html' templateUrl: './welcome.component.html'
@ -33,13 +30,21 @@ export class WelcomeComponent {
constructor(private navigationService: NavigationService, private api: SetupService) { constructor(private navigationService: NavigationService, private api: SetupService) {
this.apiToGetToken += AvailableVersion[this.api.version]; this.apiToGetToken += AvailableVersion[this.api.version];
this.navigationService.nextButtonAction = () => {
return this.api.checkToken(this.tokenControl.value ?? '');
};
this.navigationService.setNextButtonState(false); this.navigationService.nextButtonAction = () => this.api.checkToken(this.tokenControl.value ?? '');
this.tokenControl.valueChanges.subscribe(() => { this.tokenControl.valueChanges.subscribe(() => {
this.navigationService.setNextButtonState(this.tokenControl.valid); this.navigationService.setNextButtonState(this.tokenControl.valid);
}); });
this.api.isConfiguredToken().subscribe(data => {
console.log(data);
if (!data)
return;
this.navigationService.setSkipButtonState(true);
this.navigationService.skipButtonAction = () => of(true);
this.navigationService.triggerAutoSkip(this.navigationService.skipButtonAction);
});
} }
} }

@ -1,18 +1,44 @@
import {EventEmitter, Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {BehaviorSubject, Observable} from "rxjs"; import {BehaviorSubject, Observable, Subject} from "rxjs";
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class NavigationService { export class NavigationService {
private nextButtonState = new BehaviorSubject<boolean>(false); private nextButtonState = new BehaviorSubject<boolean>(false);
private skipButtonState = new BehaviorSubject<boolean>(false);
private autoSkipNavigationSubject = new Subject<() => Observable<boolean>>();
private isUserInitiatedNavigation = false;
nextButtonState$ = this.nextButtonState.asObservable(); nextButtonState$ = this.nextButtonState.asObservable();
nextButtonAction!: () => Observable<boolean>; skipButtonState$ = this.skipButtonState.asObservable();
autoSkipNavigation$ = this.autoSkipNavigationSubject.asObservable();
skipNavigation: EventEmitter<() => Observable<boolean>> = new EventEmitter(); skipButtonAction!: () => Observable<boolean>;
nextButtonAction!: () => Observable<boolean>;
setNextButtonState(state: boolean) { setNextButtonState(state: boolean) {
this.nextButtonState.next(state); this.nextButtonState.next(state);
} }
setSkipButtonState(state: boolean) {
this.skipButtonState.next(state);
}
resetButtonStates() {
this.setNextButtonState(false);
this.setSkipButtonState(false);
}
setUserInitiatedNavigation(isUserInitiated: boolean) {
this.isUserInitiatedNavigation = isUserInitiated;
}
get isNavigationUserInitiated() {
return this.isUserInitiatedNavigation;
}
triggerAutoSkip(action: () => Observable<boolean>) {
this.autoSkipNavigationSubject.next(action);
}
} }

@ -0,0 +1,5 @@
export interface CreateUserRequest {
email: string;
username: string;
password: string;
}

@ -0,0 +1,4 @@
export interface LoginRequest {
username: string;
password: string;
}

@ -0,0 +1,7 @@
export interface ScheduleRequest {
groups?: Array<number>;
isEven?: boolean;
disciplines?: Array<number>;
professors?: Array<number>;
lectureHalls?: Array<number>;
}

@ -1,29 +0,0 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents a request to configure cache settings.
*/
export interface CacheRequest {
/**
* Gets or sets the server address.
*/
server: string;
/**
* Gets or sets the port number.
*/
port: number;
/**
* Gets or sets the password.
*/
password?: string;
}

@ -0,0 +1,5 @@
export interface CacheRequest {
server: string;
port: number;
password?: string;
}

@ -0,0 +1,8 @@
export interface DatabaseRequest {
server: string;
port: number;
database: string;
user: string;
ssl: boolean;
password?: string;
}

@ -0,0 +1,8 @@
export interface EmailRequest {
server: string;
from: string;
password: string;
port: number;
ssl: boolean;
user: string;
}

@ -0,0 +1,5 @@
export interface LoggingRequest {
enableLogToFile: boolean;
logFileName?: string;
logFilePath?: string;
}

@ -0,0 +1,4 @@
export interface ScheduleConfigurationRequest {
cronUpdateSchedule?: string;
startTerm: string;
}

@ -1,29 +1,5 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Request model for creating a user.
*/
export interface CreateUserRequest { export interface CreateUserRequest {
/**
* Gets or sets the email address of the user.
*/
email: string; email: string;
/**
* Gets or sets the username of the user.
*/
username: string; username: string;
/**
* Gets or sets the password of the user.
*/
password: string; password: string;
} }

@ -1,41 +0,0 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents a request to configure the database connection settings.
*/
export interface DatabaseRequest {
/**
* Gets or sets the server address.
*/
server: string;
/**
* Gets or sets the port number.
*/
port: number;
/**
* Gets or sets the database name.
*/
database: string;
/**
* Gets or sets the username.
*/
user: string;
/**
* Gets or sets a value indicating whether SSL is enabled.
*/
ssl: boolean;
/**
* Gets or sets the password.
*/
password?: string;
}

@ -1,29 +0,0 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents a request to configure logging settings.
*/
export interface LoggingRequest {
/**
* Gets or sets a value indicating whether logging to file is enabled.
*/
enableLogToFile: boolean;
/**
* Gets or sets the log file name.
*/
logFileName?: string;
/**
* Gets or sets the log file path.
*/
logFilePath?: string;
}

@ -1,25 +1,4 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
*
*/
export interface LoginRequest { export interface LoginRequest {
/**
*
*/
username: string; username: string;
/** password: string;
*
*/
password: number;
} }

@ -1,24 +0,0 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents a request to configure the schedule settings.
*/
export interface ScheduleConfigurationRequest {
/**
* Gets or sets the cron expression for updating the schedule.
*/
cronUpdateSchedule?: string;
/**
* Gets or sets the start date of the term.
*/
startTerm: string;
}

@ -1,37 +1,7 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents a request object for retrieving schedules based on various filters.
*/
export interface ScheduleRequest { export interface ScheduleRequest {
/**
* Gets or sets an array of group IDs.
*/
groups?: Array<number>; groups?: Array<number>;
/**
* Gets or sets a value indicating whether to retrieve schedules for even weeks.
*/
isEven?: boolean; isEven?: boolean;
/**
* Gets or sets an array of discipline IDs.
*/
disciplines?: Array<number>; disciplines?: Array<number>;
/**
* Gets or sets an array of professor IDs.
*/
professors?: Array<number>; professors?: Array<number>;
/**
* Gets or sets an array of lecture hall IDs.
*/
lectureHalls?: Array<number>; lectureHalls?: Array<number>;
} }

@ -0,0 +1,6 @@
import {TwoFactorAuthentication} from "@model/twoFactorAuthentication";
export interface TwoFactorAuthRequest {
code: string;
method: TwoFactorAuthentication;
}

@ -0,0 +1,5 @@
export interface CampusBasicInfoResponse {
id: number;
codeName: string;
fullName?: string;
}

@ -0,0 +1,6 @@
export interface CampusDetailsResponse {
id: number;
codeName: string;
fullName?: string;
address?: string;
}

@ -0,0 +1,4 @@
export interface DisciplineResponse {
id: number;
name: string;
}

@ -0,0 +1,4 @@
export interface ErrorResponse {
error: string;
code: number;
}

@ -0,0 +1,4 @@
export interface FacultyResponse {
id: number;
name: string;
}

@ -0,0 +1,7 @@
export interface GroupDetailsResponse {
id: number;
name: string;
courseNumber: number;
facultyId?: number;
facultyName?: string;
}

@ -0,0 +1,6 @@
export interface GroupResponse {
id: number;
name: string;
courseNumber: number;
facultyId?: number;
}

@ -0,0 +1,7 @@
export interface LectureHallDetailsResponse {
id: number;
name: string;
campusId: number;
campusName?: string;
campusCode?: string;
}

@ -0,0 +1,5 @@
export interface LectureHallResponse {
id: number;
name: string;
campusId: number;
}

@ -0,0 +1,5 @@
export interface ProfessorResponse {
id: number;
name: string;
altName?: string;
}

@ -0,0 +1,21 @@
import {DayOfWeek} from "@model/dayOfWeek";
export interface ScheduleResponse {
dayOfWeek: DayOfWeek;
pairNumber: number;
isEven: boolean;
discipline: string;
disciplineId: number;
isExcludedWeeks?: boolean;
weeks?: Array<number>;
typeOfOccupations: Array<string>;
group: string;
groupId: number;
lectureHalls: Array<string | null>;
lectureHallsId: Array<number | null>;
professors: Array<string | null>;
professorsId: Array<number | null>;
campus: Array<string | null>;
campusId: Array<number | null>;
linkToMeet: Array<string | null>;
}

@ -0,0 +1,7 @@
import {OAuthProvider} from "@model/oAuthProvider";
export interface AvailableOAuthProvidersResponse {
providerName: string;
provider: OAuthProvider;
redirect: string;
}

@ -1,29 +1,5 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents basic information about a campus.
*/
export interface CampusBasicInfoResponse { export interface CampusBasicInfoResponse {
/**
* Gets or sets the unique identifier of the campus.
*/
id: number; id: number;
/**
* Gets or sets the code name of the campus.
*/
codeName: string; codeName: string;
/**
* Gets or sets the full name of the campus (optional).
*/
fullName?: string; fullName?: string;
} }

@ -1,33 +1,6 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents detailed information about a campus.
*/
export interface CampusDetailsResponse { export interface CampusDetailsResponse {
/**
* Gets or sets the unique identifier of the campus.
*/
id: number; id: number;
/**
* Gets or sets the code name of the campus.
*/
codeName: string; codeName: string;
/**
* Gets or sets the full name of the campus (optional).
*/
fullName?: string; fullName?: string;
/**
* Gets or sets the address of the campus (optional).
*/
address?: string; address?: string;
} }

@ -0,0 +1,8 @@
import {CacheType} from "@model/cacheType";
export interface CacheResponse {
type: CacheType;
server?: string;
port: number;
password?: string;
}

@ -0,0 +1,12 @@
import {DatabaseType} from "@model/databaseType";
export interface DatabaseResponse {
type: DatabaseType;
server?: string;
port: number;
database?: string;
user?: string;
ssl: boolean;
password?: string;
pathToDatabase?: string;
}

@ -1,25 +1,4 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents information about a discipline.
*/
export interface DisciplineResponse { export interface DisciplineResponse {
/**
* Gets or sets the unique identifier of the discipline.
*/
id: number; id: number;
/**
* Gets or sets the name of the discipline.
*/
name: string; name: string;
} }

@ -1,41 +0,0 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents a request to configure email settings.
*/
export interface EmailRequest {
/**
* Gets or sets the server address.
*/
server: string;
/**
* Gets or sets the email address from which emails will be sent.
*/
from: string;
/**
* Gets or sets the password for the email account.
*/
password: string;
/**
* Gets or sets the port number.
*/
port: number;
/**
* Gets or sets a value indicating whether SSL is enabled.
*/
ssl: boolean;
/**
* Gets or sets the username.
*/
user: string;
}

@ -1,25 +1,4 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* A class for providing information about an error
*/
export interface ErrorResponse { export interface ErrorResponse {
/**
* The text or translation code of the error. This field may not contain information in specific scenarios. For example, it might be empty for HTTP 204 responses where no content is returned or if the validation texts have not been configured.
*/
error: string; error: string;
/**
* In addition to returning the response code in the header, it is also duplicated in this field. Represents the HTTP response code.
*/
code: number; code: number;
} }

@ -1,25 +1,4 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents basic information about a faculty.
*/
export interface FacultyResponse { export interface FacultyResponse {
/**
* Gets or sets the unique identifier of the faculty.
*/
id: number; id: number;
/**
* Gets or sets the name of the faculty.
*/
name: string; name: string;
} }

@ -1,37 +1,7 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents detailed information about a group.
*/
export interface GroupDetailsResponse { export interface GroupDetailsResponse {
/**
* Gets or sets the unique identifier of the group.
*/
id: number; id: number;
/**
* Gets or sets the name of the group.
*/
name: string; name: string;
/**
* Gets or sets the course number of the group.
*/
courseNumber: number; courseNumber: number;
/**
* Gets or sets the unique identifier of the faculty to which the group belongs (optional).
*/
facultyId?: number; facultyId?: number;
/**
* Gets or sets the name of the faculty to which the group belongs (optional).
*/
facultyName?: string; facultyName?: string;
} }

@ -1,33 +1,6 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents basic information about a group.
*/
export interface GroupResponse { export interface GroupResponse {
/**
* Gets or sets the unique identifier of the group.
*/
id: number; id: number;
/**
* Gets or sets the name of the group.
*/
name: string; name: string;
/**
* Gets or sets the course number of the group.
*/
courseNumber: number; courseNumber: number;
/**
* Gets or sets the unique identifier of the faculty to which the group belongs (optional).
*/
facultyId?: number; facultyId?: number;
} }

@ -1,37 +1,7 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents the detailed response model for a lecture hall.
*/
export interface LectureHallDetailsResponse { export interface LectureHallDetailsResponse {
/**
* Gets or sets the ID of the lecture hall.
*/
id: number; id: number;
/**
* Gets or sets the name of the lecture hall.
*/
name: string; name: string;
/**
* Gets or sets the ID of the campus to which the lecture hall belongs.
*/
campusId: number; campusId: number;
/**
* Gets or sets the name of the campus.
*/
campusName?: string; campusName?: string;
/**
* Gets or sets the code of the campus.
*/
campusCode?: string; campusCode?: string;
} }

@ -1,29 +1,5 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents the response model for a lecture hall.
*/
export interface LectureHallResponse { export interface LectureHallResponse {
/**
* Gets or sets the ID of the lecture hall.
*/
id: number; id: number;
/**
* Gets or sets the name of the lecture hall.
*/
name: string; name: string;
/**
* Gets or sets the ID of the campus to which the lecture hall belongs.
*/
campusId: number; campusId: number;
} }

@ -1,29 +1,5 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
* Represents information about a professor.
*/
export interface ProfessorResponse { export interface ProfessorResponse {
/**
* Gets or sets the unique identifier of the professor.
*/
id: number; id: number;
/**
* Gets or sets the name of the professor.
*/
name: string; name: string;
/**
* Gets or sets the alternate name of the professor (optional).
*/
altName?: string; altName?: string;
} }

@ -1,84 +1,21 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
import {DayOfWeek} from "@model/dayOfWeek"; import {DayOfWeek} from "@model/dayOfWeek";
/**
* Represents a response object containing schedule information.
*/
export interface ScheduleResponse { export interface ScheduleResponse {
dayOfWeek: DayOfWeek; dayOfWeek: DayOfWeek;
/**
* Gets or sets the pair number for the schedule entry.
*/
pairNumber: number; pairNumber: number;
/**
* Gets or sets a value indicating whether the pair is on an even week.
*/
isEven: boolean; isEven: boolean;
/**
* Gets or sets the name of the discipline for the schedule entry.
*/
discipline: string; discipline: string;
/**
* Gets or sets the ID of the discipline for the schedule entry.
*/
disciplineId: number; disciplineId: number;
/**
* Gets or sets exclude or include weeks for a specific discipline.
*/
isExcludedWeeks?: boolean; isExcludedWeeks?: boolean;
/**
* The week numbers required for the correct display of the schedule. Whether there will be Mirea.Api.Dto.Responses.Schedule.ScheduleResponse.Discipline during the week or not depends on the Mirea.Api.Dto.Responses.Schedule.ScheduleResponse.IsExcludedWeeks property.
*/
weeks?: Array<number>; weeks?: Array<number>;
/**
* Gets or sets the type of occupation for the schedule entry.
*/
typeOfOccupations: Array<string>; typeOfOccupations: Array<string>;
/**
* Gets or sets the name of the group for the schedule entry.
*/
group: string; group: string;
/**
* Gets or sets the ID of the group for the schedule entry.
*/
groupId: number; groupId: number;
/** lectureHalls: Array<string | null>;
* Gets or sets the names of the lecture halls for the schedule entry. lectureHallsId: Array<number | null>;
*/ professors: Array<string | null>;
lectureHalls: Array<string>; professorsId: Array<number | null>;
/** campus: Array<string | null>;
* Gets or sets the IDs of the lecture halls for the schedule entry. campusId: Array<number | null>;
*/ linkToMeet: Array<string | null>;
lectureHallsId: Array<number>;
/**
* Gets or sets the names of the professors for the schedule entry.
*/
professors: Array<string>;
/**
* Gets or sets the IDs of the professors for the schedule entry.
*/
professorsId: Array<number>;
/**
* Gets or sets the names of the campuses for the schedule entry.
*/
campus: Array<string>;
/**
* Gets or sets the IDs of the campuses for the schedule entry.
*/
campusId: Array<number>;
/**
* Gets or sets the links to online meetings for the schedule entry.
*/
linkToMeet: Array<string>;
} }

@ -1,25 +0,0 @@
/**
* MIREA Schedule Web API
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team.
*
* OpenAPI spec version: 1.0
* Contact: support@winsomnia.net
*
* NOTE: This class is auto generated by the swagger code generator program.
* https://github.com/swagger-api/swagger-codegen.git
* Do not edit the class manually.
*/
/**
*
*/
export interface TokenResponse {
/**
*
*/
accessToken : string;
/**
*
*/
expiresIn: Date;
}

@ -0,0 +1,8 @@
import {OAuthProvider} from "@model/oAuthProvider";
export interface UserResponse {
email: string;
username: string;
twoFactorAuthenticatorEnabled: boolean;
usedOAuthProviders: OAuthProvider[];
}

@ -0,0 +1,4 @@
export enum CacheType {
Memcached,
Redis
}

@ -0,0 +1,5 @@
export enum DatabaseType {
Mysql,
Sqlite,
PostgresSql
}

@ -0,0 +1,9 @@
export enum DayOfWeek {
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}

@ -0,0 +1,5 @@
export enum OAuthProvider {
Google,
Yandex,
MailRu
}

@ -0,0 +1,4 @@
export interface PairPeriodTime {
start: string;
end: string;
}

@ -0,0 +1,7 @@
export interface PasswordPolicy {
minimumLength: number;
requireLetter: boolean;
requireLettersDifferentCase: boolean;
requireDigit: boolean;
requireSpecialCharacter: boolean;
}

@ -9,10 +9,10 @@ export class TimeOnly {
if (hourOrTime instanceof Date) { if (hourOrTime instanceof Date) {
this._ticks = hourOrTime.getTime(); this._ticks = hourOrTime.getTime();
} else if (typeof hourOrTime === 'number' && minute !== undefined && second !== undefined) { } else if (typeof hourOrTime === 'number' && minute !== undefined && second !== undefined) {
this._ticks = new Date(2000, 0, 1, hourOrTime, minute, second, 0).getTime() this._ticks = new Date(2000, 0, 1, hourOrTime, minute, second, 0).getTime();
} else if (typeof hourOrTime === 'string') { } else if (typeof hourOrTime === 'string') {
const [h, m, s] = hourOrTime.split(':').map(Number); const [h, m, s] = hourOrTime.split(':').map(Number);
this._ticks = new Date(2000, 0, 1, h, m, s, 0).getTime() this._ticks = new Date(2000, 0, 1, h, m, s, 0).getTime();
} else { } else {
throw new Error('Invalid constructor arguments'); throw new Error('Invalid constructor arguments');
} }
@ -35,7 +35,7 @@ export class TimeOnly {
} }
toTimeWithoutSeconds(): string { toTimeWithoutSeconds(): string {
return `${String(this.hour).padStart(2, '0')}:${String(this.minute).padStart(2, '0')}` return `${String(this.hour).padStart(2, '0')}:${String(this.minute).padStart(2, '0')}`;
} }
toString(): string { toString(): string {

@ -0,0 +1,4 @@
export enum TwoFactorAuthentication {
None,
TotpRequired
}

@ -1,23 +1,9 @@
/** export enum DayOfWeek {
* MIREA Schedule Web API Sunday,
* This API provides a convenient interface for retrieving data stored in the database. Special attention was paid to the lightweight and easy transfer of all necessary data. Made by the Winsomnia team. Monday,
* Tuesday,
* OpenAPI spec version: 1.0 Wednesday,
* Contact: support@winsomnia.net Thursday,
* Friday,
* NOTE: This class is auto generated by the swagger code generator program. Saturday
* https://github.com/swagger-api/swagger-codegen.git }
* Do not edit the class manually.
*/
export type DayOfWeek = 0 | 1 | 2 | 3 | 4 | 5 | 6;
export const DayOfWeek = {
NUMBER_0: 0 as DayOfWeek,
NUMBER_1: 1 as DayOfWeek,
NUMBER_2: 2 as DayOfWeek,
NUMBER_3: 3 as DayOfWeek,
NUMBER_4: 4 as DayOfWeek,
NUMBER_5: 5 as DayOfWeek,
NUMBER_6: 6 as DayOfWeek
};

Some files were not shown because too many files have changed in this diff Show More