diff --git a/src/app/app.component.ts b/src/app/app.component.ts index cb6986f7..c4bdd9cd 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -24,16 +24,7 @@ export class AppComponent implements OnInit, OnDestroy { private readonly router: Router ) {} - ngOnInit(): void { - this.authService - .getCurrentUser() - .pipe(untilDestroyed(this)) - .subscribe((user) => { - if (user != null) { - this.isAuthenticated = true; - } - }); - } + ngOnInit(): void {} ngOnDestroy(): void {} } diff --git a/src/app/components/admin-navbar/admin-navbar.component.ts b/src/app/components/admin-navbar/admin-navbar.component.ts index 883f34d9..b1016efb 100644 --- a/src/app/components/admin-navbar/admin-navbar.component.ts +++ b/src/app/components/admin-navbar/admin-navbar.component.ts @@ -94,8 +94,16 @@ export class AdminNavbarComponent { title: "Инструменты", links: [ { - title: "Хз как назвать", - url: "/admin/background-jobs", + title: "Общее", + url: "/admin/tools/background-jobs", + }, + { + title: "Курсы валют", + url: "/admin/tools/currencies", + }, + { + title: "QR код", + url: "/admin/tools/generate-qr", }, ], }, diff --git a/src/app/components/navbar/navbar.component.ts b/src/app/components/navbar/navbar.component.ts index 12f3236b..14592576 100644 --- a/src/app/components/navbar/navbar.component.ts +++ b/src/app/components/navbar/navbar.component.ts @@ -37,35 +37,20 @@ export class NavbarComponent implements OnInit, OnDestroy { constructor( private readonly authService: AuthService, - private readonly spinner: SpinnerService, - private readonly healthService: HealthCheckService + private readonly spinner: SpinnerService ) {} ngOnInit(): void { this.setupSubscribers(); - this.authService - .getCurrentUser() - //.pipe(untilDestroyed(this)) - .subscribe((currentUser) => { - if (currentUser != null) { - this.currentUser = currentUser; - } + this.loginButtonAvailable = true; + this.authService + .getCurrentUserFromStorage() + .pipe(untilDestroyed(this)) + .subscribe((user) => { + this.currentUser = user; this.renderNavbar(); }); - - this.healthService - .appHealth() - .pipe(untilDestroyed(this)) - .subscribe( - (result) => { - this.loginButtonAvailable = true; - }, - (err) => { - console.error(err); - this.healthCheckError = true; - } - ); } private setupSubscribers(): void { diff --git a/src/app/models/application-user.ts b/src/app/models/application-user.ts index b2f184ff..82b7d536 100644 --- a/src/app/models/application-user.ts +++ b/src/app/models/application-user.ts @@ -10,6 +10,7 @@ export interface ApplicationUser { salariesCount: number; emailConfirmed: boolean; identityId: number | null; + isMfaEnabled: boolean; deletedAt: Date | null; createdAt: Date; updatedAt: Date; diff --git a/src/app/models/extended/application-user-extended.ts b/src/app/models/extended/application-user-extended.ts index f63735a3..893d1350 100644 --- a/src/app/models/extended/application-user-extended.ts +++ b/src/app/models/extended/application-user-extended.ts @@ -58,6 +58,10 @@ export class ApplicationUserExtended implements ApplicationUser { return this.instance.roles; } + get isMfaEnabled(): boolean { + return this.instance.isMfaEnabled; + } + constructor(public readonly instance: ApplicationUser) { Assertion.notNull(instance, "instance", ApplicationUserExtended.name); diff --git a/src/app/modules/admin/admin-routing.module.ts b/src/app/modules/admin/admin-routing.module.ts index af29c419..253b951f 100644 --- a/src/app/modules/admin/admin-routing.module.ts +++ b/src/app/modules/admin/admin-routing.module.ts @@ -13,6 +13,8 @@ import { TelegramBotUsagesComponent } from "./components/telegram/telegram-bot-u import { TelegramUserSettingsComponent } from "./components/telegram/telegram-user-settings/telegram-user-settings.component"; import { SourcedSalariesAdminPageComponent } from "./components/salaries/sourced-salaries-admin-page/sourced-salaries-admin-page.component"; import { StatDataCacheRecordsComponent } from "./components/telegram/stat-data-change-subscriptions/stat-data-cache-records.component"; +import { CurrenciesPageComponent } from "./components/currencies-page/currencies-page.component"; +import { GenerateQrPageComponent } from "./components/generate-qr-code-page/generate-qr-page.component"; const routes: Routes = [ { path: "", component: AdminStartPageComponent }, @@ -21,7 +23,6 @@ const routes: Routes = [ path: "interview-templates", component: InterviewTemplatesAdminPageComponent, }, - { path: "background-jobs", component: BackgroundJobsComponent }, { path: "skills", component: SkillsPaginatedTableComponent }, { path: "work-industries", component: WorkIndustriesPaginatedTableComponent }, { path: "professions", component: ProfessionsPaginatedTableComponent }, @@ -40,6 +41,10 @@ const routes: Routes = [ path: "telegram/stat-data-change-subscriptions", component: StatDataCacheRecordsComponent, }, + + { path: "tools/background-jobs", component: BackgroundJobsComponent }, + { path: "tools/currencies", component: CurrenciesPageComponent }, + { path: "tools/generate-qr", component: GenerateQrPageComponent }, ]; @NgModule({ diff --git a/src/app/modules/admin/admin.module.ts b/src/app/modules/admin/admin.module.ts index a7768ee0..beb21a9b 100644 --- a/src/app/modules/admin/admin.module.ts +++ b/src/app/modules/admin/admin.module.ts @@ -20,6 +20,8 @@ import { TelegramBotUsagesComponent } from "./components/telegram/telegram-bot-u import { TelegramUserSettingsComponent } from "./components/telegram/telegram-user-settings/telegram-user-settings.component"; import { SourcedSalariesAdminPageComponent } from "./components/salaries/sourced-salaries-admin-page/sourced-salaries-admin-page.component"; import { StatDataCacheRecordsComponent } from "./components/telegram/stat-data-change-subscriptions/stat-data-cache-records.component"; +import { GenerateQrPageComponent } from "./components/generate-qr-code-page/generate-qr-page.component"; +import { CurrenciesPageComponent } from "./components/currencies-page/currencies-page.component"; @NgModule({ declarations: [ @@ -39,6 +41,8 @@ import { StatDataCacheRecordsComponent } from "./components/telegram/stat-data-c TelegramBotUsagesComponent, TelegramUserSettingsComponent, StatDataCacheRecordsComponent, + CurrenciesPageComponent, + GenerateQrPageComponent, ], imports: [ CommonModule, diff --git a/src/app/modules/admin/components/background-jobs/background-jobs.component.html b/src/app/modules/admin/components/background-jobs/background-jobs.component.html index 99d2f385..3ee4fea7 100644 --- a/src/app/modules/admin/components/background-jobs/background-jobs.component.html +++ b/src/app/modules/admin/components/background-jobs/background-jobs.component.html @@ -10,25 +10,6 @@ > -
- - - - - - - - - - - - - - - -
ВалютаКурсДата
{{ item.currencyString }}{{ item.value }} kzt{{ item.pubDate | date : "yyyy-MM-dd" }}
-
-
Конфиги
{{ configs }}
diff --git a/src/app/modules/admin/components/background-jobs/background-jobs.component.ts b/src/app/modules/admin/components/background-jobs/background-jobs.component.ts index 161403ef..a808cb60 100644 --- a/src/app/modules/admin/components/background-jobs/background-jobs.component.ts +++ b/src/app/modules/admin/components/background-jobs/background-jobs.component.ts @@ -5,7 +5,6 @@ import { AuthService } from "@shared/services/auth/auth.service"; import { HealthCheckItem } from "../health-check-table/health-check-item"; import { AdminToolsService } from "@services/admin-tools.service"; import { untilDestroyed } from "@shared/subscriptions/until-destroyed"; -import { CurrencyItem } from "./currency-item"; @Component({ selector: "app-background-jobs", @@ -16,7 +15,6 @@ export class BackgroundJobsComponent implements OnInit, OnDestroy { authorizationToken: string | null = null; configs: string | null = null; healthCheckItems: Array = []; - currencies: Array = []; constructor( private readonly authService: AuthService, @@ -38,13 +36,6 @@ export class BackgroundJobsComponent implements OnInit, OnDestroy { this.checkHealth(); - this.adminToolsService - .getCurrencies() - .pipe(untilDestroyed(this)) - .subscribe((currencies) => { - this.currencies = currencies.map((x) => new CurrencyItem(x)); - }); - this.adminToolsService .getConfigs() .pipe(untilDestroyed(this)) diff --git a/src/app/modules/admin/components/currencies-page/currencies-page.component.html b/src/app/modules/admin/components/currencies-page/currencies-page.component.html new file mode 100644 index 00000000..8e3d2ce6 --- /dev/null +++ b/src/app/modules/admin/components/currencies-page/currencies-page.component.html @@ -0,0 +1,26 @@ +Курсы валюты + +
+
+
+
+ + + + + + + + + + + + + + + +
ВалютаКурсДата
{{ item.currencyString }}{{ item.value }} kzt{{ item.pubDate | date : "yyyy-MM-dd" }}
+
+
+
+
diff --git a/src/app/modules/admin/components/currencies-page/currencies-page.component.scss b/src/app/modules/admin/components/currencies-page/currencies-page.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/modules/admin/components/currencies-page/currencies-page.component.spec.ts b/src/app/modules/admin/components/currencies-page/currencies-page.component.spec.ts new file mode 100644 index 00000000..e562c8c4 --- /dev/null +++ b/src/app/modules/admin/components/currencies-page/currencies-page.component.spec.ts @@ -0,0 +1,34 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { + mostUsedImports, + mostUsedServices, + testUtilStubs, +} from "@shared/test-utils"; + +import { AdminToolsService } from "@services/admin-tools.service"; +import { CurrenciesPageComponent } from "./currencies-page.component"; + +describe("CurrenciesPageComponent", () => { + let component: CurrenciesPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CurrenciesPageComponent], + imports: [...mostUsedImports], + providers: [...testUtilStubs, ...mostUsedServices, AdminToolsService], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(CurrenciesPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/modules/admin/components/currencies-page/currencies-page.component.ts b/src/app/modules/admin/components/currencies-page/currencies-page.component.ts new file mode 100644 index 00000000..b4afa230 --- /dev/null +++ b/src/app/modules/admin/components/currencies-page/currencies-page.component.ts @@ -0,0 +1,37 @@ +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { TitleService } from "@services/title.service"; +import { HealthCheckItem } from "../health-check-table/health-check-item"; +import { AdminToolsService } from "@services/admin-tools.service"; +import { untilDestroyed } from "@shared/subscriptions/until-destroyed"; +import { CurrencyItem } from "./currency-item"; + +@Component({ + templateUrl: "./currencies-page.component.html", + styleUrls: ["./currencies-page.component.scss"], +}) +export class CurrenciesPageComponent implements OnInit, OnDestroy { + authorizationToken: string | null = null; + configs: string | null = null; + healthCheckItems: Array = []; + currencies: Array = []; + + constructor( + private readonly titleService: TitleService, + private readonly adminToolsService: AdminToolsService + ) { + this.titleService.setTitle("Курсы валют"); + } + + ngOnInit(): void { + this.adminToolsService + .getCurrencies() + .pipe(untilDestroyed(this)) + .subscribe((currencies) => { + this.currencies = currencies.map((x) => new CurrencyItem(x)); + }); + } + + ngOnDestroy(): void { + this.titleService.resetTitle(); + } +} diff --git a/src/app/modules/admin/components/background-jobs/currency-item.ts b/src/app/modules/admin/components/currencies-page/currency-item.ts similarity index 100% rename from src/app/modules/admin/components/background-jobs/currency-item.ts rename to src/app/modules/admin/components/currencies-page/currency-item.ts diff --git a/src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.html b/src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.html new file mode 100644 index 00000000..d22fcfbb --- /dev/null +++ b/src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.html @@ -0,0 +1,36 @@ +QR код + +
+
+
+
+ +
+
+ +
+
+
+ + +
+ QR code +
+
+
diff --git a/src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.scss b/src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.spec.ts b/src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.spec.ts new file mode 100644 index 00000000..ba404c60 --- /dev/null +++ b/src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.spec.ts @@ -0,0 +1,34 @@ +import { ComponentFixture, TestBed } from "@angular/core/testing"; +import { CUSTOM_ELEMENTS_SCHEMA } from "@angular/core"; +import { + mostUsedImports, + mostUsedServices, + testUtilStubs, +} from "@shared/test-utils"; + +import { AdminToolsService } from "@services/admin-tools.service"; +import { GenerateQrPageComponent } from "./generate-qr-page.component"; + +describe("GenerateQrPageComponent", () => { + let component: GenerateQrPageComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [GenerateQrPageComponent], + imports: [...mostUsedImports], + providers: [...testUtilStubs, ...mostUsedServices, AdminToolsService], + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }).compileComponents(); + }); + + beforeEach(() => { + fixture = TestBed.createComponent(GenerateQrPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it("should create", () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.ts b/src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.ts new file mode 100644 index 00000000..75c3bad5 --- /dev/null +++ b/src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.ts @@ -0,0 +1,44 @@ +import { Component, OnDestroy } from "@angular/core"; +import { TitleService } from "@services/title.service"; +import { AdminToolsService } from "@services/admin-tools.service"; +import { untilDestroyed } from "@shared/subscriptions/until-destroyed"; + +@Component({ + templateUrl: "./generate-qr-page.component.html", + styleUrls: ["./generate-qr-page.component.scss"], +}) +export class GenerateQrPageComponent implements OnDestroy { + shoQrCodeDialog = false; + qrCodeSource: string | null = null; + generatedQRBase64: string | null = null; + + constructor( + private readonly titleService: TitleService, + private readonly adminToolsService: AdminToolsService + ) { + this.titleService.setTitle("Генерация QR-кода"); + } + + generateQrCode(): void { + if (this.qrCodeSource == null) { + return; + } + + this.adminToolsService + .generateQR(this.qrCodeSource) + .pipe(untilDestroyed(this)) + .subscribe((r) => { + this.generatedQRBase64 = "data:image/jpg;base64," + r.imageBase64; + this.shoQrCodeDialog = true; + }); + } + + onQrModalDlgClose(): void { + this.shoQrCodeDialog = false; + this.generatedQRBase64 = null; + } + + ngOnDestroy(): void { + this.titleService.resetTitle(); + } +} diff --git a/src/app/modules/home/components/auth-callback/auth-callback.component.html b/src/app/modules/home/components/auth-callback/auth-callback.component.html index 653fb18b..358667de 100644 --- a/src/app/modules/home/components/auth-callback/auth-callback.component.html +++ b/src/app/modules/home/components/auth-callback/auth-callback.component.html @@ -1,16 +1,41 @@ -
-
+
+
- -
-
-
-
-
-
Please wait for a while...
-
We are preparing some awesome features for you
+
+
Добро пожаловать
+
Загружаем данные
+
+ +
+
+ +
+
+
Введите код MFA (TOTP)
+
+ +
+
+ +
+
+
+
diff --git a/src/app/modules/home/components/auth-callback/auth-callback.component.ts b/src/app/modules/home/components/auth-callback/auth-callback.component.ts index cb930036..0b0daddd 100644 --- a/src/app/modules/home/components/auth-callback/auth-callback.component.ts +++ b/src/app/modules/home/components/auth-callback/auth-callback.component.ts @@ -1,22 +1,30 @@ -import { Component, OnInit } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { AuthService } from "@shared/services/auth/auth.service"; import { Router, ActivatedRoute } from "@angular/router"; import { CookieService } from "ngx-cookie-service"; +import { TotpService } from "@services/totp.service"; +import { untilDestroyed } from "@shared/subscriptions/until-destroyed"; @Component({ templateUrl: "./auth-callback.component.html", }) -export class AuthCallbackComponent implements OnInit { +export class AuthCallbackComponent implements OnInit, OnDestroy { private readonly urlToRedirectAfterLogin = "/me"; showErrorBlock = false; showInfoblock = true; + showMfaBlock = false; + + showTotpInvalid = false; + totpCode = ""; + totpCodeSent = false; constructor( private readonly authService: AuthService, private readonly router: Router, private readonly route: ActivatedRoute, - private readonly cookieService: CookieService + private readonly cookieService: CookieService, + private readonly totpService: TotpService ) {} async ngOnInit(): Promise { @@ -27,23 +35,66 @@ export class AuthCallbackComponent implements OnInit { this.route.snapshot.fragment.indexOf("error") >= 0 ) { this.showErrorBlock = true; + this.showMfaBlock = false; this.showInfoblock = false; return Promise.resolve(); } - this.authService.completeAuthentication().subscribe((x) => { - if (this.cookieService.check("url")) { - const url = this.cookieService.get("url") ?? ""; - this.cookieService.delete("url"); - - if (url.includes("?")) { - this.router.navigateByUrl(url); + this.authService + .completeAuthentication() + .pipe(untilDestroyed(this)) + .subscribe((x) => { + if (x.isMfaEnabled) { + this.showMfaBlock = true; + this.showInfoblock = false; } else { - this.router.navigate([url]); + this.showInfoblock = true; + + this.authService.getCurrentUser().subscribe((user) => { + this.redirectToMainPageOrUrl(); + }); } - } else { - this.router.navigate([this.urlToRedirectAfterLogin]); + }); + } + + validateTotp(): void { + if (this.totpCode == null || this.totpCode.length !== 6) { + this.showTotpInvalid = true; + this.totpCodeSent = false; + return; + } + + this.totpService.verifyTotp(this.totpCode).subscribe((result) => { + if (result.result) { + this.showTotpInvalid = false; + this.showInfoblock = true; + this.totpCodeSent = true; + + this.authService.getCurrentUser().subscribe((user) => { + this.redirectToMainPageOrUrl(); + }); } + + this.showInfoblock = false; + this.showTotpInvalid = true; + this.totpCodeSent = false; }); } + + private redirectToMainPageOrUrl(): void { + if (this.cookieService.check("url")) { + const url = this.cookieService.get("url") ?? ""; + this.cookieService.delete("url"); + + if (url.includes("?")) { + this.router.navigateByUrl(url); + } else { + this.router.navigate([url]); + } + } else { + this.router.navigate([this.urlToRedirectAfterLogin]); + } + } + + ngOnDestroy(): void {} } diff --git a/src/app/modules/home/components/me/me.component.html b/src/app/modules/home/components/me/me.component.html index bc9b275f..91a7b2c8 100644 --- a/src/app/modules/home/components/me/me.component.html +++ b/src/app/modules/home/components/me/me.component.html @@ -1,7 +1,85 @@ {{ user?.fullName }}
- +
+
+
+
    +
  • +
    Email
    +
    + {{ user.email ?? "-" }} +
    +
  • + +
  • +
    Статус email
    +
    + Не подтвержден +
    +
  • + +
  • +
    Статус аккаунта
    +
    + Удален +
    +
  • + +
  • +
    Роли
    +
    + +
    +
  • + +
  • +
    Статус MFA
    +
    +
    + Включен +
    +
    + +
    +
    +
  • + +
  • +
    Статус MFA
    +
    +
    + Выключен +
    +
    + +
    +
    +
  • +
+
+
+ +
+
+
Пока работаем над этим.
+
+
+
@@ -9,3 +87,19 @@
+ + +
+ QR code +
+
+ + diff --git a/src/app/modules/home/components/me/me.component.ts b/src/app/modules/home/components/me/me.component.ts index b5f4fdb9..fb6101dc 100644 --- a/src/app/modules/home/components/me/me.component.ts +++ b/src/app/modules/home/components/me/me.component.ts @@ -4,29 +4,78 @@ import { ApplicationUserExtended } from "@models/extended"; import { TitleService } from "@services/title.service"; import { untilDestroyed } from "@shared/subscriptions/until-destroyed"; import { AuthService } from "@shared/services/auth/auth.service"; +import { TotpService } from "@services/totp.service"; +import { DialogMessage } from "@shared/components/dialogs/models/dialog-message"; +import { ConfirmMsg } from "@shared/components/dialogs/models/confirm-msg"; +import { UserRole } from "@models/enums"; @Component({ templateUrl: "./me.component.html", }) export class MeComponent implements OnInit, OnDestroy { user: ApplicationUserExtended | null = null; + mfaQrCodeImage: string | null = null; + confirmDisablingMfaMessage: DialogMessage | null = null; + showEnableMfaBlock = false; constructor( private readonly authorizationService: AuthorizationService, - private readonly auth: AuthService, - private readonly titleService: TitleService - ) {} + private readonly titleService: TitleService, + private readonly totpService: TotpService + ) { + this.titleService.setTitle("Мой профиль"); + } ngOnInit(): void { + this.reloadUser(); + } + + openMfaSetupDialog(): void { + if (this.user?.isMfaEnabled) { + return; + } + + this.totpService + .enableMfa() + .pipe(untilDestroyed(this)) + .subscribe((response) => { + this.mfaQrCodeImage = + "data:image/jpg;base64," + response.totpSetupQRBase64; + }); + } + + openDisableMfaDialog(): void { + this.confirmDisablingMfaMessage = new DialogMessage( + new ConfirmMsg( + "Отключить мультфакторную авторизацию", + "Вы уверены?", + () => { + this.totpService + .disableMfa() + .pipe(untilDestroyed(this)) + .subscribe(() => { + this.confirmDisablingMfaMessage = null; + this.reloadUser(); + }); + } + ) + ); + } + + onQrModalDlgClose(): void { + this.mfaQrCodeImage = null; + this.reloadUser(); + } + + ngOnDestroy(): void {} + + private reloadUser(): void { this.authorizationService .getMe() .pipe(untilDestroyed(this)) .subscribe((user) => { this.user = new ApplicationUserExtended(user); + this.showEnableMfaBlock = this.user.hasRole(UserRole.Admin); }); - - this.titleService.setTitle("My profile"); } - - ngOnDestroy(): void {} } diff --git a/src/app/modules/home/home.module.ts b/src/app/modules/home/home.module.ts index 19eed961..74728ee5 100644 --- a/src/app/modules/home/home.module.ts +++ b/src/app/modules/home/home.module.ts @@ -13,6 +13,7 @@ import { PrivacyPolicyPageComponent } from "./components/privacy-policy-page/pri import { AboutUsComponent } from "./components/about-us/about-us.component"; import { LogoutCallbackComponent } from "./components/logout-callback/logout-callback.component"; import { TelegramBotABoutComponent } from "./components/telegram-bot/telegram-bot.component"; +import { FormsModule } from "@angular/forms"; @NgModule({ declarations: [ @@ -28,6 +29,6 @@ import { TelegramBotABoutComponent } from "./components/telegram-bot/telegram-bo LogoutCallbackComponent, TelegramBotABoutComponent, ], - imports: [CommonModule, SharedModule, HomeRoutingModule], + imports: [CommonModule, SharedModule, HomeRoutingModule, FormsModule], }) export class HomeModule {} diff --git a/src/app/modules/salaries/components/historical-charts-page/historical-survey-chart/historical-survey-chart.component.html b/src/app/modules/salaries/components/historical-charts-page/historical-survey-chart/historical-survey-chart.component.html index 7c013f2f..3bb15c5a 100644 --- a/src/app/modules/salaries/components/historical-charts-page/historical-survey-chart/historical-survey-chart.component.html +++ b/src/app/modules/salaries/components/historical-charts-page/historical-survey-chart/historical-survey-chart.component.html @@ -67,7 +67,7 @@
Польза статистики по мнению тех, кто работает на удаленке
- +
{{ usefulnessGradeRemoteChart @@ -104,7 +104,7 @@
Ожидания по статистике у тех, кто работает на удаленке
- +
{{ expectationGradeRemoteChart diff --git a/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.html b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.html index 75d31182..6c703fb0 100644 --- a/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.html +++ b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.html @@ -259,9 +259,9 @@ target="_blank" class="text-reset" >Kolesa Group. Статистика была собрана в летом 2024 года. Список валют - показывает те рейты, которые были актуальны на декабрь 2024 года по - данным нацбанка РК. + >. Статистика была собрана в летом 2024 года. Список валют показывает + те рейты, которые были актуальны на декабрь 2024 года по данным + нацбанка РК.
Спасибо команде Kolesa Group за предоставленные данные. diff --git a/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.ts b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.ts index 018f3f51..b8c78f8e 100644 --- a/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.ts +++ b/src/app/modules/salaries/components/salaries-chart/salaries-chart.component.ts @@ -162,9 +162,10 @@ export class SalariesChartComponent implements OnInit, OnDestroy { this.filterData = data; const selectedGrade = data.grade ? DeveloperGrade[data.grade] : "empty"; - const selectedSourceType = data.salarySourceTypes.length > 0 - ? SalarySourceType[data.salarySourceTypes[0]] - : "empty"; + const selectedSourceType = + data.salarySourceTypes.length > 0 + ? SalarySourceType[data.salarySourceTypes[0]] + : "empty"; this.gtag.event( "salaries_filters_applied_grade", @@ -274,11 +275,16 @@ export class SalariesChartComponent implements OnInit, OnDestroy { x.currentUserSalary.professionId === developerProfessionId; this.noImportSourceWasSelected = - filterToApply.salarySourceTypes == null || filterToApply.salarySourceTypes.length === 0; + filterToApply.salarySourceTypes == null || + filterToApply.salarySourceTypes.length === 0; this.kolesaImportedSalariesWasSelected = - filterToApply.salarySourceTypes.length > 0 && filterToApply.salarySourceTypes[0] == SalarySourceType.KolesaDevelopersCsv2022; + filterToApply.salarySourceTypes.length > 0 && + filterToApply.salarySourceTypes[0] == + SalarySourceType.KolesaDevelopersCsv2022; this.kolesaImportedDataAnalyticsSalariesWasSelected = - filterToApply.salarySourceTypes.length > 0 && filterToApply.salarySourceTypes[0] == SalarySourceType.KolesaDataAnalyticsCsv2024; + filterToApply.salarySourceTypes.length > 0 && + filterToApply.salarySourceTypes[0] == + SalarySourceType.KolesaDataAnalyticsCsv2024; } }); } diff --git a/src/app/modules/salaries/components/salaries-chart/salary-block-remote-value/salary-block-remote-value.component.ts b/src/app/modules/salaries/components/salaries-chart/salary-block-remote-value/salary-block-remote-value.component.ts index 51ee4c7b..47ad49ce 100644 --- a/src/app/modules/salaries/components/salaries-chart/salary-block-remote-value/salary-block-remote-value.component.ts +++ b/src/app/modules/salaries/components/salaries-chart/salary-block-remote-value/salary-block-remote-value.component.ts @@ -13,14 +13,10 @@ export class SalaryBlockRemoteValueComponent { source: SalariesChart | null = null; get median(): string { - return FormatAsMoneyPipe.formatNumber( - this.source?.medianRemoteSalary - ); + return FormatAsMoneyPipe.formatNumber(this.source?.medianRemoteSalary); } get average(): string { - return FormatAsMoneyPipe.formatNumber( - this.source?.averageRemoteSalary - ); + return FormatAsMoneyPipe.formatNumber(this.source?.averageRemoteSalary); } } diff --git a/src/app/modules/salaries/components/salaries-chart/salary-block-value/salary-block-value.component.ts b/src/app/modules/salaries/components/salaries-chart/salary-block-value/salary-block-value.component.ts index c4bdb469..d346ed22 100644 --- a/src/app/modules/salaries/components/salaries-chart/salary-block-value/salary-block-value.component.ts +++ b/src/app/modules/salaries/components/salaries-chart/salary-block-value/salary-block-value.component.ts @@ -38,12 +38,8 @@ export class SalaryBlockValueComponent implements OnInit, OnDestroy { return; } - this.median = FormatAsMoneyPipe.formatNumber( - this.source.medianSalary - ); - this.average = FormatAsMoneyPipe.formatNumber( - this.source.averageSalary - ); + this.median = FormatAsMoneyPipe.formatNumber(this.source.medianSalary); + this.average = FormatAsMoneyPipe.formatNumber(this.source.averageSalary); this.medianRemote = FormatAsMoneyPipe.formatNumber( this.source.medianRemoteSalary ); diff --git a/src/app/modules/salaries/components/salaries-per-profession.ts b/src/app/modules/salaries/components/salaries-per-profession.ts index cf9da10d..e7adbb88 100644 --- a/src/app/modules/salaries/components/salaries-per-profession.ts +++ b/src/app/modules/salaries/components/salaries-per-profession.ts @@ -29,7 +29,7 @@ export class SalariesPerProfession { const salary = salaries[index]; if (salary.company == CompanyType.Local) { if (salary.professionId == null) { - console.log("Profession is null", salary); + console.error("Profession is null", salary); } localSalaries.push(salary); diff --git a/src/app/modules/salaries/components/shared/global-filters-form-group.ts b/src/app/modules/salaries/components/shared/global-filters-form-group.ts index 146188ee..b690f9b0 100644 --- a/src/app/modules/salaries/components/shared/global-filters-form-group.ts +++ b/src/app/modules/salaries/components/shared/global-filters-form-group.ts @@ -82,8 +82,10 @@ export class GlobalFiltersFormGroup extends FormGroup { skills: new FormControl(filterData?.skills, []), salarySourceType: new FormControl( filterData != null && filterData.salarySourceTypes.length > 0 - ? filterData.salarySourceTypes[0] - : null, []), + ? filterData.salarySourceTypes[0] + : null, + [] + ), quarterTo: new FormControl(filterData?.quarterTo, []), yearTo: new FormControl(filterData?.yearTo, []), }); @@ -126,7 +128,7 @@ export class GlobalFiltersFormGroup extends FormGroup { profsToInclude, cities, skills, - salarySourceType != null ? [ salarySourceType ] : [], + salarySourceType != null ? [salarySourceType] : [], quarterTo, yearTo ); diff --git a/src/app/modules/users/components/user-page/user-page.component.html b/src/app/modules/users/components/user-page/user-page.component.html index 79ee4cf8..5677167c 100644 --- a/src/app/modules/users/components/user-page/user-page.component.html +++ b/src/app/modules/users/components/user-page/user-page.component.html @@ -1,7 +1,49 @@ {{ user?.firstName }} {{ user?.lastName }}
- +
+
+
+
    +
  • +
    Email
    +
    + {{ user.email ?? "-" }} +
    +
  • + +
  • +
    Email подтвержден
    +
    + Not confirmed +
    +
  • + +
  • +
    Статус
    +
    + Удален +
    +
  • + +
  • +
    Роли
    +
    + +
    +
  • +
+
+
+ +
+
+
Пока работаем над этим.
+
+
+
diff --git a/src/app/services/admin-tools.service.ts b/src/app/services/admin-tools.service.ts index 574c6b77..6d79726c 100644 --- a/src/app/services/admin-tools.service.ts +++ b/src/app/services/admin-tools.service.ts @@ -21,6 +21,10 @@ export interface CurrencyData { pubDate: Date; } +export interface GenerateQrResponse { + imageBase64: string; +} + @Injectable() export class AdminToolsService { private readonly apiUrl: string; @@ -36,4 +40,11 @@ export class AdminToolsService { getConfigs(): Observable { return this.api.get(this.apiUrl + "configs"); } + + generateQR(value: string): Observable { + return this.api.post(this.apiUrl + "generate-qr", { + value, + pizelSize: 20, + }); + } } diff --git a/src/app/services/authorization.service.ts b/src/app/services/authorization.service.ts index e83f2b34..e13b94a8 100644 --- a/src/app/services/authorization.service.ts +++ b/src/app/services/authorization.service.ts @@ -3,6 +3,12 @@ import { Injectable } from "@angular/core"; import { ApplicationUser } from "../models"; import { ApiService } from "./api.service"; +export interface CheckTotpResponse { + id: number; + email: string; + isMfaEnabled: boolean; +} + @Injectable({ providedIn: "root", }) @@ -12,4 +18,8 @@ export class AuthorizationService { getMe(): Observable { return this.api.get("/api/account/me"); } + + checkTotpRequired(): Observable { + return this.api.get("/api/account/check-totp"); + } } diff --git a/src/app/services/index.ts b/src/app/services/index.ts index 20f247c9..92b5fb04 100644 --- a/src/app/services/index.ts +++ b/src/app/services/index.ts @@ -18,6 +18,7 @@ import { SurveyService } from "./salaries-survey.service"; import { MetaTagService } from "./meta-tags.service"; import { AdminToolsService } from "./admin-tools.service"; import { HistoricalChartsService } from "./historical-charts.service"; +import { TotpService } from "./totp.service"; export * from "./authorization.service"; export * from "./api.service"; @@ -28,6 +29,7 @@ export * from "./interviews.service"; export * from "./user-admin.service"; export * from "./user-labels.service"; export * from "./users.service"; +export * from "./totp.service"; export const applicationServices = [ SessionStorageWrapper, @@ -50,4 +52,5 @@ export const applicationServices = [ MetaTagService, AdminToolsService, HistoricalChartsService, + TotpService, ]; diff --git a/src/app/services/totp.service.ts b/src/app/services/totp.service.ts new file mode 100644 index 00000000..bf33400a --- /dev/null +++ b/src/app/services/totp.service.ts @@ -0,0 +1,31 @@ +import { Observable } from "rxjs"; +import { Injectable } from "@angular/core"; +import { ApiService } from "./api.service"; + +export interface SetupTotpResponse { + totpMfaUrl: string; + totpSetupQRBase64: string; +} + +export interface VerifyTotpResponse { + result: boolean; +} + +@Injectable({ + providedIn: "root", +}) +export class TotpService { + constructor(private api: ApiService) {} + + enableMfa(): Observable { + return this.api.post("/api/totp/enable"); + } + + disableMfa(): Observable { + return this.api.post("/api/totp/disable"); + } + + verifyTotp(code: string): Observable { + return this.api.post("/api/totp/verify", { totpCode: code }); + } +} diff --git a/src/app/shared/components/user-profile-page/user-profile-page.component.html b/src/app/shared/components/user-profile-page/user-profile-page.component.html deleted file mode 100644 index bbed9138..00000000 --- a/src/app/shared/components/user-profile-page/user-profile-page.component.html +++ /dev/null @@ -1,43 +0,0 @@ -
-
-
-
    -
  • -
    Email
    -
    - {{ user.email ?? "-" }} -
    -
  • - -
  • -
    Email подтвержден
    -
    - Not confirmed -
    -
  • - -
  • -
    Статус
    -
    - Удален -
    -
  • - -
  • -
    Роли
    -
    - -
    -
  • -
-
-
- -
-
-
Пока работаем над этим.
-
-
-
diff --git a/src/app/shared/components/user-profile-page/user-profile-page.component.spec.ts b/src/app/shared/components/user-profile-page/user-profile-page.component.spec.ts deleted file mode 100644 index dcff7deb..00000000 --- a/src/app/shared/components/user-profile-page/user-profile-page.component.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ComponentFixture, TestBed } from "@angular/core/testing"; - -import { UserProfilePageComponent } from "./user-profile-page.component"; - -describe("UserProfilePageComponent", () => { - let component: UserProfilePageComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - declarations: [UserProfilePageComponent], - }).compileComponents(); - }); - - beforeEach(() => { - fixture = TestBed.createComponent(UserProfilePageComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it("should create", () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/src/app/shared/components/user-profile-page/user-profile-page.component.ts b/src/app/shared/components/user-profile-page/user-profile-page.component.ts deleted file mode 100644 index b11f2d22..00000000 --- a/src/app/shared/components/user-profile-page/user-profile-page.component.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Component, Input } from "@angular/core"; -import { ApplicationUser } from "@models/application-user"; - -@Component({ - selector: "app-user-profile-page", - templateUrl: "./user-profile-page.component.html", -}) -export class UserProfilePageComponent { - @Input() - user: ApplicationUser | null = null; -} diff --git a/src/app/shared/directives/format-as-money.pipe.ts b/src/app/shared/directives/format-as-money.pipe.ts index bae35094..81c46262 100644 --- a/src/app/shared/directives/format-as-money.pipe.ts +++ b/src/app/shared/directives/format-as-money.pipe.ts @@ -1,18 +1,21 @@ -import { Pipe, PipeTransform } from '@angular/core'; +import { Pipe, PipeTransform } from "@angular/core"; @Pipe({ - name: 'formatAsMoney' + name: "formatAsMoney", }) export class FormatAsMoneyPipe implements PipeTransform { transform(value: number | null, fractionSize: number = 2): string { return FormatAsMoneyPipe.formatNumber(value, fractionSize); } - public static formatNumber(value: number | null | undefined, fractionSize: number = 2): string { + public static formatNumber( + value: number | null | undefined, + fractionSize: number = 2 + ): string { if (value == null) { - return ''; + return ""; } - return value.toFixed(fractionSize).replace(/\B(?=(\d{3})+(?!\d))/g, ' '); + return value.toFixed(fractionSize).replace(/\B(?=(\d{3})+(?!\d))/g, " "); } } diff --git a/src/app/shared/guards/active-user.guard.spec.ts b/src/app/shared/guards/active-user.guard.spec.ts index 02d8fdb5..46f18421 100644 --- a/src/app/shared/guards/active-user.guard.spec.ts +++ b/src/app/shared/guards/active-user.guard.spec.ts @@ -5,7 +5,7 @@ import { Observable, of } from "rxjs"; import { ApplicationUserExtended } from "@models/extended"; import { ApplicationUser } from "@models/application-user"; import { ActiveUserGuard } from "./active-user.guard"; -import { IdToken } from "@auth0/auth0-angular"; +import { CheckTotpResponse } from "@services/authorization.service"; class AuthStub extends AuthService { constructor(private readonly user: ApplicationUser | null) { @@ -22,9 +22,11 @@ class AuthStub extends AuthService { override login(): Promise { throw new Error("Method not implemented."); } - override completeAuthentication(): Observable { + + override completeAuthentication(): Observable { throw new Error("Method not implemented."); } + override getAuthorizationHeaderValue(): string { throw new Error("Method not implemented."); } diff --git a/src/app/shared/guards/auth.guard.ts b/src/app/shared/guards/auth.guard.ts index d36d7caf..3e2399b1 100644 --- a/src/app/shared/guards/auth.guard.ts +++ b/src/app/shared/guards/auth.guard.ts @@ -26,7 +26,6 @@ export class AuthGuard implements CanActivate { if (state !== null && state.url != null) { // set expire date + 10 hours - console.log("Url to redirect", state.url); this.cookieService.set("url", state.url, Date.now(), "/"); } diff --git a/src/app/shared/services/auth/auth.service.ts b/src/app/shared/services/auth/auth.service.ts index 1bc3e9a8..b8e9bc5d 100644 --- a/src/app/shared/services/auth/auth.service.ts +++ b/src/app/shared/services/auth/auth.service.ts @@ -2,8 +2,11 @@ import { Subject, Observable, of } from "rxjs"; import { Injectable } from "@angular/core"; import { OidcUserManager } from "./oidc-user-manager.service"; import { ApplicationUser } from "@models/application-user"; -import { AuthorizationService } from "@services/authorization.service"; -import { map } from "rxjs/operators"; +import { + AuthorizationService, + CheckTotpResponse, +} from "@services/authorization.service"; +import { map, switchMap } from "rxjs/operators"; import { ApplicationUserExtended } from "@models/extended"; import { AuthSessionService } from "./auth.session.service"; import { IdToken, User } from "@auth0/auth0-angular"; @@ -11,9 +14,11 @@ import { IdToken, User } from "@auth0/auth0-angular"; export interface IAuthService { getCurrentUser(): Observable; + getCurrentUserFromStorage(): Observable; + login(): Promise; - completeAuthentication(): Observable; + completeAuthentication(): Observable; getAuthorizationHeaderValue(): string | null; @@ -58,37 +63,47 @@ export class AuthService implements IAuthService { ); } - login(): Promise { - if (this.isAuthenticated()) { - this.reloadInternalProperties(); - return Promise.resolve(); + getCurrentUserFromStorage(): Observable { + this.tryLoadUserFromSession(); + + if (this.authorizationInfo == null) { + return of(null); + } + + if (this.applicationUser != null) { + return of(this.applicationUser); } - return this.oidcManager.login(); + return of(null); } - reload(): void { - if (this.isAuthenticated()) { - this.reloadInternalProperties(); + async login(): Promise { + if (this.applicationUser == null) { + await this.oidcManager.login(); } } - completeAuthentication(): Observable { + completeAuthentication(): Observable { return this.oidcManager.completeAuthentication().pipe( - map((x) => { + switchMap((x) => { this.authorizationInfo = x; - this.reloadInternalProperties(); - return x ?? null; + this.session.auth = this.authorizationInfo ?? null; + + return this.authorizationService + .checkTotpRequired() + .pipe(map((r) => r)); }) ); } - private reloadInternalProperties(): void { + private reloadInternalProperties(): Observable { this.session.auth = this.authorizationInfo ?? null; - - this.authorizationService.getMe().subscribe((appUser) => { - this.saveCurrentUser(appUser); - }); + return this.authorizationService.getMe().pipe( + map((appUser) => { + this.saveCurrentUser(appUser); + return appUser; + }) + ); } private saveCurrentUser(appUser: ApplicationUser): void { @@ -141,6 +156,7 @@ export class AuthService implements IAuthService { if (this.authorizationInfo == null) { this.authorizationInfo = this.session.auth; const user = this.session.applicationUser; + this.applicationUser = user != null ? new ApplicationUserExtended(user) : null; } diff --git a/src/app/shared/services/auth/oidc-user-manager.service.ts b/src/app/shared/services/auth/oidc-user-manager.service.ts index 0d2fcee1..e4dfa596 100644 --- a/src/app/shared/services/auth/oidc-user-manager.service.ts +++ b/src/app/shared/services/auth/oidc-user-manager.service.ts @@ -18,7 +18,11 @@ export class OidcUserManager { } login(): Promise { - return this.auth.loginWithRedirect().toPromise(); + return this.auth + .loginWithRedirect({ + appState: { target: "/auth-callback" }, + }) + .toPromise(); } completeAuthentication(): Observable { diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 55b56792..89e03e87 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -23,7 +23,6 @@ import { AppPageHeaderComponent } from "./components/app-page-header/app-page-he import { DeveloperGradeLabelComponent } from "./components/developer-grade-label/developer-grade-label.component"; import { VisibilityLabelComponent } from "./components/visibility-label/visibility-label.component"; import { UserRolesLabelComponent } from "./components/user-roles-label/user-roles-label.component"; -import { UserProfilePageComponent } from "./components/user-profile-page/user-profile-page.component"; import { DialogComponent } from "./components/dialogs/dialog/dialog.component"; import { LoadingSpinnerComponent } from "./components/loading-spinner/loading-spinner.component"; import { DataLoadingInfoBlockComponent } from "./components/data-loading-info-block/data-loading-info-block.component"; @@ -61,7 +60,6 @@ const componentsToDeclareAndExport = [ DeveloperGradeLabelComponent, VisibilityLabelComponent, UserRolesLabelComponent, - UserProfilePageComponent, LoadingSpinnerComponent, DataLoadingInfoBlockComponent, PaginationButtonsComponent, diff --git a/src/app/shared/test-utils/mock-auth.service.ts b/src/app/shared/test-utils/mock-auth.service.ts index 210ecb8d..3aede9b5 100644 --- a/src/app/shared/test-utils/mock-auth.service.ts +++ b/src/app/shared/test-utils/mock-auth.service.ts @@ -2,6 +2,10 @@ import { Subject, of, Observable } from "rxjs"; import { IAuthService } from "../services/auth/auth.service"; import { ApplicationUserExtended } from "@models/extended"; import { IdToken } from "@auth0/auth0-angular"; +import { ApplicationUser } from "@models/application-user"; +import { TestApplicationUser } from "./models"; +import { UserRole } from "@models/enums"; +import { CheckTotpResponse } from "@services/authorization.service"; export class MockAuthService implements IAuthService { public readonly loggedOutInvoked$: Subject = new Subject(); @@ -16,12 +20,25 @@ export class MockAuthService implements IAuthService { return ""; } - completeAuthentication(): Observable { - return of(); + completeAuthentication(): Observable { + const user = new TestApplicationUser(UserRole.Interviewer); + return of({ + id: user.id, + email: user.email, + isMfaEnabled: user.isMfaEnabled, + } as CheckTotpResponse); } getCurrentUser(): Observable { - return of(null); + return of( + new ApplicationUserExtended(new TestApplicationUser(UserRole.Interviewer)) + ); + } + + getCurrentUserFromStorage(): Observable { + return of( + new ApplicationUserExtended(new TestApplicationUser(UserRole.Interviewer)) + ); } login(): Promise { diff --git a/src/app/shared/test-utils/models/test-application-user.ts b/src/app/shared/test-utils/models/test-application-user.ts index b194a6cc..4f243ff7 100644 --- a/src/app/shared/test-utils/models/test-application-user.ts +++ b/src/app/shared/test-utils/models/test-application-user.ts @@ -16,8 +16,10 @@ export class TestApplicationUser implements ApplicationUser { this.updatedAt = new Date(); this.fullname = `${this.firstName} ${this.lastName}`; this.salariesCount = 0; + this.isMfaEnabled = false; } + isMfaEnabled: boolean; email: string | null; firstName: string | null; lastName: string | null;