Compare commits

...

7 Commits

Author SHA1 Message Date
1ffbfad37a build: update ref
Some checks failed
Build and Deploy Angular App / build (push) Failing after 1m36s
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

View File

@ -1,6 +1,6 @@
# 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)
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

View File

@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "1.0.0-b9",
"version": "1.0.0-b10",
"scripts": {
"ng": "ng",
"start": "ng serve",
@ -10,35 +10,34 @@
},
"private": true,
"dependencies": {
"@angular/animations": "^18.2.9",
"@angular/cdk": "~18.2.10",
"@angular/cdk-experimental": "^18.2.10",
"@angular/common": "^18.2.9",
"@angular/compiler": "^18.2.9",
"@angular/core": "^18.2.9",
"@angular/forms": "^18.2.9",
"@angular/material": "~18.2.10",
"@angular/platform-browser": "^18.2.9",
"@angular/platform-browser-dynamic": "^18.2.9",
"@angular/router": "^18.2.9",
"@dhutaryan/ngx-mat-timepicker": "^18.0.2",
"@angular/animations": "^19.0.4",
"@angular/cdk": "~19.0.3",
"@angular/cdk-experimental": "^19.0.3",
"@angular/common": "^19.0.4",
"@angular/compiler": "^19.0.4",
"@angular/core": "^19.0.4",
"@angular/forms": "^19.0.4",
"@angular/material": "~19.0.3",
"@angular/platform-browser": "^19.0.4",
"@angular/platform-browser-dynamic": "^19.0.4",
"@angular/router": "^19.0.4",
"@progress/kendo-date-math": "^1.5.14",
"ngx-toastr": "^19.0.0",
"rxjs": "~7.8.1",
"tslib": "^2.8.0",
"zone.js": "^0.14.10"
"tslib": "^2.8.1",
"zone.js": "^0.15.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^18.2.10",
"@angular/cli": "^18.2.10",
"@angular/compiler-cli": "^18.2.9",
"@types/jasmine": "~5.1.4",
"jasmine-core": "~5.4.0",
"@angular-devkit/build-angular": "^19.0.5",
"@angular/cli": "^19.0.5",
"@angular/compiler-cli": "^19.0.4",
"@types/jasmine": "~5.1.5",
"jasmine-core": "~5.5.0",
"karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "^5.5.4"
"typescript": "^5.6.3"
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

After

Width:  |  Height:  |  Size: 665 B

View File

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

View File

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

View File

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

View File

@ -1 +1 @@
<mat-progress-spinner [color]="color" mode="indeterminate" [diameter]="scale" />
<mat-progress-spinner [color]="color" mode="indeterminate" [diameter]="scale"/>

View File

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

View File

@ -22,8 +22,8 @@
<hr/>
<div class="app-footer-copyright">
<span>Powered by <a href="https://winsomnia.net">Winsomnia</a> &copy;{{ currentYear }}.</span>
<a href="https://opensource.org/license/mit/">Code licensed under an MIT-style License.</a>
<span>Current Version: {{ version }}</span>
<a href="https://opensource.org/license/mit/">Code licensed under an MIT-style License.</a>
<span>Current Version: {{ version }}</span>
</div>
</div>
</footer>

View File

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

View File

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

View File

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

View File

@ -90,7 +90,7 @@
}
@if (((filteredGroupsSpecialist && filteredGroupsSpecialist.length > 0 && filteredGroupsBehaviour && filteredGroupsBehaviour.length > 0) ||
((!filteredGroupsSpecialist || filteredGroupsSpecialist.length === 0) && filteredGroupsBehaviour && filteredGroupsBehaviour.length > 0)) &&
((!filteredGroupsSpecialist || filteredGroupsSpecialist.length === 0) && filteredGroupsBehaviour && filteredGroupsBehaviour.length > 0)) &&
filteredGroupsMagistracy && filteredGroupsMagistracy.length > 0) {
<div class="div-wrapper">
<hr/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import {bootstrapApplication} from '@angular/platform-browser';
import {appConfig} from './app/app.config';
import {AppComponent} from './app/app.component';
bootstrapApplication(AppComponent, appConfig)
.catch((err) => console.error(err));

View File

@ -17,7 +17,7 @@
.formLogin form {
display: flex;
flex-direction:column;
flex-direction: column;
}
.formLoginButton {

View File

@ -57,7 +57,7 @@
</form>
<mat-error>
{{errorText}}
{{ errorText }}
</mat-error>
<div class="formLoginButton">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,13 +15,13 @@
Для получения ключа используете тот же хост, что и Backend приложение и выполните:
</p>
<code>
curl -X 'GET' '{{apiToGetToken}}/Setup/GenerateToken' -H 'accept: application/json'
curl -X 'GET' '{{ apiToGetToken }}/Setup/GenerateToken' -H 'accept: application/json'
</code>
<p>
Или
</p>
<code>
Invoke-RestMethod -Uri "{{apiToGetToken}}/Setup/GenerateToken" -Method Get -Headers &#x40;&#123;accept="application/json"&#125;
Invoke-RestMethod -Uri "{{ apiToGetToken }}/Setup/GenerateToken" -Method Get -Headers &#x40;&#123;accept="application/json"&#125;
</code>
<div style="display: flex; flex-direction: column; margin: 25px 0;">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

@ -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;
}

View File

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

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}

View File

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

View File

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

View File

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

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