Compare commits

...

6 Commits

Author SHA1 Message Date
eda6ca4b1a refactor: use RFC 7807 standard for error handling
All checks were successful
Build and Deploy Angular App / build (push) Successful in 1m30s
2024-12-22 07:17:21 +03:00
10bf53adec feat: add integration with seq 2024-12-22 07:16:54 +03:00
e10075dfed fix: message error text 2024-12-18 08:47:36 +03:00
2b482d2b2d fix: set max height 2024-12-18 08:47:22 +03:00
9017e87175 fix: bypassing cors 2024-12-18 08:41:29 +03:00
16e25905dc feat: add spinner 2024-12-18 08:40:54 +03:00
12 changed files with 130 additions and 59 deletions

View File

@ -158,7 +158,7 @@ export default abstract class ApiService {
private handleError(error: HttpErrorResponse): void {
// todo: change to Retry-After condition
if (error.error && error.error.toString().includes("setup")) {
if (error.error && error.error.detail.includes("setup")) {
this.router.navigate(['/setup/']).then();
return;
}
@ -167,6 +167,10 @@ export default abstract class ApiService {
let message: string | undefined = undefined;
if (error.error instanceof ErrorEvent) {
title = `Произошла ошибка: ${error.error.message}`;
} else {
if (error.error && error.error.type && error.error.title) {
title = error.error.title || `Ошибка с кодом ${error.status}`;
message = error.error.detail || 'Неизвестная ошибка';
} else {
switch (error.status) {
case 0:
@ -195,11 +199,9 @@ export default abstract class ApiService {
title = `Сервер вернул код ошибки: ${error.status}`;
break;
}
if (error.error?.Error)
message = error.error.Error;
else if (error.error instanceof String)
message = error.error.toString();
else
}
if (!message)
message = error.error.statusMessage;
}
this.notify.error(message == '' ? undefined : message, title);

View File

@ -75,3 +75,11 @@
.provider-item.provider-unlink:hover::after {
transform: translate(-50%, -50%) scale(1.0);
}
.provider-item .provider-spinner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(1.0);
border-radius: 50%;
}

View File

@ -6,9 +6,13 @@
<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">
[class.disabled]="!canUnlink && provider.disabled || provider.active"
[class.provider-unlink]="canUnlink && provider.disabled">
<img [alt]="provider.providerName" [src]="provider.icon"
class="provider-icon" draggable="false"/>
@if (provider.active) {
<app-data-spinner class="provider-spinner"/>
}
</a>
}
</div>

View File

@ -1,4 +1,4 @@
import {Component, Inject, Input, OnInit} from '@angular/core';
import {Component, EventEmitter, Inject, Input, Output} from '@angular/core';
import AuthApiService, {OAuthProviderData} from "@api/v1/authApiService";
import {OAuthProvider} from "@model/oAuthProvider";
import {ToastrService} from "ngx-toastr";
@ -10,9 +10,11 @@ import {
MatDialogTitle
} from "@angular/material/dialog";
import {MatButton} from "@angular/material/button";
import {DataSpinnerComponent} from "@component/common/data-spinner/data-spinner.component";
interface AvailableOAuthProviders extends OAuthProviderData {
disabled: boolean;
active: boolean;
}
@Component({
@ -53,17 +55,24 @@ export class DeleteConfirmDialog {
@Component({
selector: 'OAuthProviders',
imports: [],
imports: [
DataSpinnerComponent
],
templateUrl: './OAuthProviders.html',
styleUrl: './OAuthProviders.css',
providers: [AuthApiService]
})
export class OAuthProviders implements OnInit {
export class OAuthProviders {
protected providers: AvailableOAuthProviders[] = [];
protected _activeProvidersId: OAuthProvider[] = [];
protected _activeProviders: string[] = [];
@Input() message: string = 'Вы можете войти в аккаунт через';
@Input() activeProviders: string[] = [];
@Input() set activeProviders(data: string[]) {
this._activeProviders = data;
this.updateDisabledProviders();
}
@Input() set activeProvidersId(data: OAuthProvider[]) {
this._activeProvidersId = data;
@ -72,6 +81,8 @@ export class OAuthProviders implements OnInit {
@Input() canUnlink: boolean = false;
@Output() public oAuthUpdateProviders = new EventEmitter();
constructor(authApi: AuthApiService, private notify: ToastrService, private dialog: MatDialog) {
authApi.availableProviders().subscribe(providers => this.updateDisabledProviders(providers));
}
@ -80,23 +91,12 @@ export class OAuthProviders implements OnInit {
this.providers = (data ?? this.providers).map(provider => {
return {
...provider,
disabled: this._activeProvidersId.includes(provider.provider) || this.activeProviders.includes(provider.providerName)
disabled: this._activeProvidersId.includes(provider.provider) || this._activeProviders.includes(provider.providerName),
active: false
};
});
}
ngOnInit(): void {
window.addEventListener('message', (event) => {
if (event.data && event.data.success === false) {
console.error(event.data.message);
this.notify.error(event.data.message, 'OAuth ошибка');
} else {
this.activeProvidersId.push(event.data.provider);
this.updateDisabledProviders();
}
});
}
protected openOAuth(provider: AvailableOAuthProviders) {
console.log(provider.redirect);
const oauthWindow = window.open(
@ -108,6 +108,16 @@ export class OAuthProviders implements OnInit {
this.notify.error('Не удалось открыть OAuth окно');
return;
}
provider.active = true;
const checkInterval = setInterval(() => {
if (oauthWindow.closed) {
clearInterval(checkInterval);
this.oAuthUpdateProviders.emit();
provider.active = false;
}
}, 1500);
}
protected confirmDelete(provider: AvailableOAuthProviders) {

View File

@ -82,7 +82,7 @@ export class LoginComponent {
})
.pipe(catchError(error => {
this.loaderActive = false;
this.errorText = error.error instanceof String ? error.error : error.statusText;
this.errorText = error.error instanceof String ? error.statusText : error.error;
this.loginButtonIsDisable = true;
throw error;
}))

View File

@ -116,7 +116,7 @@
}
</mat-form-field>
<OAuthProviders [canUnlink]="true" [activeProvidersId]="activatedProviders"
<OAuthProviders [canUnlink]="true" [activeProvidersId]="activatedProviders" (oAuthUpdateProviders)="updateProviders()"
[message]="'Или можете получить часть данных от сторонних сервисов'"/>
</div>
</form>

View File

@ -70,9 +70,16 @@ export class CreateAdminComponent {
this.createAdminForm.get('password')?.updateValueAndValidity();
});
this.updateAdminData();
}
private updateAdminData() {
this.api.adminConfiguration().subscribe(configuration => {
if (configuration) {
if (this.createAdminForm.get('email')?.value == 0)
this.createAdminForm.get('email')?.setValue(configuration.email);
if (this.createAdminForm.get('user')?.value == 0)
this.createAdminForm.get('user')?.setValue(configuration.username);
this.activatedProviders = configuration.usedOAuthProviders;
@ -111,4 +118,8 @@ export class CreateAdminComponent {
this.hideRetypePass = !this.hideRetypePass;
event.stopPropagation();
}
protected updateProviders() {
this.updateAdminData();
}
}

View File

@ -4,6 +4,11 @@
Настройте систему логирования как будет удобно для отображения.
Можно настроить путь к файлу, имена файлов или вовсе отключить логирование в файл.
</p>
<p class="mat-body-2 secondary">
Также вы можете настроить интеграцию с Seq.
Введите необходимые данные и мы отправим тестовый лог на сервер Seq. Его уровень будет Warning.
Если тестовый лог не появился вернитесь на данный шаг и перепроверьте данные.
</p>
<form [formGroup]="loggingSettings">
<p>
@ -31,5 +36,18 @@
matTooltip="Укажите название файла, в который будут записаны логи"
formControlName="logName">
</mat-form-field>
<mat-form-field color="accent">
<mat-label>Сервер Seq</mat-label>
<input matInput
matTooltip="Укажите сервер Seq вначале указав схему (http/https)"
formControlName="seqServer">
</mat-form-field>
<mat-form-field color="accent">
<mat-label>Api ключ Seq</mat-label>
<input matInput
matTooltip="Укажите ключ API, который вы создали в Seq"
formControlName="seqKey">
</mat-form-field>
</div>
</form>

View File

@ -45,7 +45,9 @@ export class LoggingComponent {
this.loggingSettings = this.formBuilder.group({
enabled: [true, Validators.required],
logPath: [''],
logName: ['']
logName: [''],
seqServer: [''],
seqKey: ['']
}
);
@ -56,9 +58,11 @@ export class LoggingComponent {
this.navigationService.nextButtonAction = () => {
return this.api.setLogging({
"enableLogToFile": this.loggingSettings.get('enabled')?.value,
"logFileName": this.loggingSettings.get('logName')?.value,
"logFilePath": this.loggingSettings.get('logPath')?.value
enableLogToFile: this.loggingSettings.get('enabled')?.value,
logFileName: this.loggingSettings.get('logName')?.value,
logFilePath: this.loggingSettings.get('logPath')?.value,
apiServerSeq: this.loggingSettings.get('seqServer')?.value,
apiKeySeq: this.loggingSettings.get('seqKey')?.value
}
);
};
@ -73,6 +77,8 @@ export class LoggingComponent {
this.loggingSettings.get('enabled')?.setValue(x.enableLogToFile);
this.loggingSettings.get('logName')?.setValue(x.logFileName);
this.loggingSettings.get('logPath')?.setValue(x.logFilePath);
this.loggingSettings.get('seqServer')?.setValue(x.apiServerSeq);
this.loggingSettings.get('seqKey')?.setValue(x.apiKeySeq);
});
}
}

View File

@ -163,6 +163,16 @@
Путь к файлу журнала: {{ loggingConfig.logFilePath }}
</div>
}
@if (loggingConfig.apiServerSeq) {
<div>
Сервер Seq: {{ loggingConfig.apiServerSeq }}
</div>
}
@if (loggingConfig.apiKeySeq) {
<div>
Ключ Seq: {{ loggingConfig.apiKeySeq }}
</div>
}
</div>
}

View File

@ -14,7 +14,7 @@
<h3>Ваш код: <i><strong>{{ secret }}</strong></i></h3>
<div>
<img [src]="totpImage" alt="totp-qr-code"/>
<img [src]="totpImage" alt="totp-qr-code" style="max-height: 60vh;"/>
</div>
<form [formGroup]="twoFactorForm">

View File

@ -2,4 +2,6 @@ export interface LoggingRequest {
enableLogToFile: boolean;
logFileName?: string;
logFilePath?: string;
apiServerSeq?: string;
apiKeySeq?: string;
}