From 3c84c5f68a6365c2f4fb691a3fec99653ffdcb39 Mon Sep 17 00:00:00 2001 From: "maxim.gorbatyuk" Date: Mon, 27 Jan 2025 22:20:57 +0500 Subject: [PATCH 1/4] Added TOTP + QR --- src/app/models/application-user.ts | 1 + .../extended/application-user-extended.ts | 4 ++ src/app/modules/admin/admin-routing.module.ts | 7 ++- src/app/modules/admin/admin.module.ts | 4 ++ .../background-jobs.component.html | 19 ------ .../background-jobs.component.ts | 9 --- .../currencies-page.component.html | 26 +++++++++ .../currencies-page.component.scss | 0 .../currencies-page.component.spec.ts | 34 +++++++++++ .../currencies-page.component.ts | 37 ++++++++++++ .../currency-item.ts | 0 .../generate-qr-page.component.html | 14 +++++ .../generate-qr-page.component.scss | 0 .../generate-qr-page.component.spec.ts | 34 +++++++++++ .../generate-qr-page.component.ts | 40 +++++++++++++ .../auth-callback.component.html | 13 ++++- .../auth-callback/auth-callback.component.ts | 58 +++++++++++++++---- src/app/services/admin-tools.service.ts | 4 ++ src/app/services/index.ts | 3 + src/app/services/totp.service.ts | 27 +++++++++ .../shared/guards/active-user.guard.spec.ts | 2 +- src/app/shared/services/auth/auth.service.ts | 37 +++++------- .../shared/test-utils/mock-auth.service.ts | 7 ++- .../models/test-application-user.ts | 2 + 24 files changed, 316 insertions(+), 66 deletions(-) create mode 100644 src/app/modules/admin/components/currencies-page/currencies-page.component.html create mode 100644 src/app/modules/admin/components/currencies-page/currencies-page.component.scss create mode 100644 src/app/modules/admin/components/currencies-page/currencies-page.component.spec.ts create mode 100644 src/app/modules/admin/components/currencies-page/currencies-page.component.ts rename src/app/modules/admin/components/{background-jobs => currencies-page}/currency-item.ts (100%) create mode 100644 src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.html create mode 100644 src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.scss create mode 100644 src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.spec.ts create mode 100644 src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.ts create mode 100644 src/app/services/totp.service.ts 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..752a24c6 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..b80bb8e4 --- /dev/null +++ b/src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.html @@ -0,0 +1,14 @@ +QR код + +
+
+
+ + +
+
+
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..4bf42efe --- /dev/null +++ b/src/app/modules/admin/components/generate-qr-code-page/generate-qr-page.component.ts @@ -0,0 +1,40 @@ +import { Component, OnDestroy, OnInit } 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 { + + 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((qrCode) => { + this.generatedQRBase64 = qrCode; + }); + } + + 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..04215e11 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 @@ -8,9 +8,18 @@
-
-
+
+
Please wait for a while...
We are preparing some awesome features for you
+ +
+

Validate 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..b81bdb3a 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 @@ -2,6 +2,7 @@ import { Component, 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"; @Component({ templateUrl: "./auth-callback.component.html", @@ -11,12 +12,17 @@ export class AuthCallbackComponent implements OnInit { showErrorBlock = false; showInfoblock = true; + showMfaBlock = false; + + showTotpInvalid = false; + totpCode = ""; 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 { @@ -32,18 +38,48 @@ export class AuthCallbackComponent implements OnInit { } 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); - } else { - this.router.navigate([url]); + + if (x.isMfaEnabled) { + this.showMfaBlock = true; + return; + } + + this.redirectToMainPageOrUrl(); + }); + } + + validateTotp(): void { + + if (this.totpCode == null || + this.totpCode.length !== 6) { + + this.showTotpInvalid = true; + return; + } + + this.totpService.verifyTotp(this.totpCode) + .subscribe((result) => { + if (result.result) { + this.showTotpInvalid = false; + this.redirectToMainPageOrUrl(); } + + this.showTotpInvalid = true; + }); + } + + 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([this.urlToRedirectAfterLogin]); + this.router.navigate([url]); } - }); + } else { + this.router.navigate([this.urlToRedirectAfterLogin]); + } } } diff --git a/src/app/services/admin-tools.service.ts b/src/app/services/admin-tools.service.ts index 574c6b77..2f4f6b75 100644 --- a/src/app/services/admin-tools.service.ts +++ b/src/app/services/admin-tools.service.ts @@ -36,4 +36,8 @@ 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/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..e8e56224 --- /dev/null +++ b/src/app/services/totp.service.ts @@ -0,0 +1,27 @@ +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) {} + + setupTotp(): Observable { + return this.api.get("/api/totp/setup"); + } + + verifyTotp(code: string): Observable { + return this.api.post("/api/totp/verify", { totpCode: code }); + } +} diff --git a/src/app/shared/guards/active-user.guard.spec.ts b/src/app/shared/guards/active-user.guard.spec.ts index 02d8fdb5..45406cfa 100644 --- a/src/app/shared/guards/active-user.guard.spec.ts +++ b/src/app/shared/guards/active-user.guard.spec.ts @@ -22,7 +22,7 @@ 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 { diff --git a/src/app/shared/services/auth/auth.service.ts b/src/app/shared/services/auth/auth.service.ts index 1bc3e9a8..8380eabd 100644 --- a/src/app/shared/services/auth/auth.service.ts +++ b/src/app/shared/services/auth/auth.service.ts @@ -3,7 +3,7 @@ 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 { map, switchMap } from "rxjs/operators"; import { ApplicationUserExtended } from "@models/extended"; import { AuthSessionService } from "./auth.session.service"; import { IdToken, User } from "@auth0/auth0-angular"; @@ -13,7 +13,7 @@ export interface IAuthService { login(): Promise; - completeAuthentication(): Observable; + completeAuthentication(): Observable; getAuthorizationHeaderValue(): string | null; @@ -58,37 +58,32 @@ export class AuthService implements IAuthService { ); } - login(): Promise { + async login(): Promise { if (this.isAuthenticated()) { - this.reloadInternalProperties(); - return Promise.resolve(); + await this.reloadInternalProperties(); } - return this.oidcManager.login(); + await this.oidcManager.login(); } - reload(): void { - if (this.isAuthenticated()) { - this.reloadInternalProperties(); - } - } - - completeAuthentication(): Observable { + completeAuthentication(): Observable { return this.oidcManager.completeAuthentication().pipe( - map((x) => { + switchMap((x) => { this.authorizationInfo = x; - this.reloadInternalProperties(); - return x ?? null; + return this.reloadInternalProperties().pipe(map((appUser) => appUser)); }) ); } - 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 { diff --git a/src/app/shared/test-utils/mock-auth.service.ts b/src/app/shared/test-utils/mock-auth.service.ts index 210ecb8d..e79ce08e 100644 --- a/src/app/shared/test-utils/mock-auth.service.ts +++ b/src/app/shared/test-utils/mock-auth.service.ts @@ -2,6 +2,9 @@ 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"; export class MockAuthService implements IAuthService { public readonly loggedOutInvoked$: Subject = new Subject(); @@ -16,8 +19,8 @@ export class MockAuthService implements IAuthService { return ""; } - completeAuthentication(): Observable { - return of(); + completeAuthentication(): Observable { + return of(new TestApplicationUser(UserRole.Interviewer)); } getCurrentUser(): Observable { 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; From 5ee5b7825c922788ebdc8593a6b8acd1add41e0b Mon Sep 17 00:00:00 2001 From: "maxim.gorbatyuk" Date: Tue, 28 Jan 2025 09:20:51 +0500 Subject: [PATCH 2/4] TOTP implementation --- .../admin-navbar/admin-navbar.component.ts | 12 +++++-- .../extended/application-user-extended.ts | 2 +- .../generate-qr-page.component.html | 31 +++++++++++++++---- .../generate-qr-page.component.ts | 16 ++++++---- .../auth-callback.component.html | 15 +++++---- .../auth-callback/auth-callback.component.ts | 29 +++++++++-------- src/app/modules/home/home.module.ts | 3 +- .../historical-survey-chart.component.html | 4 +-- .../salaries-chart.component.html | 6 ++-- .../salaries-chart.component.ts | 18 +++++++---- .../salary-block-remote-value.component.ts | 8 ++--- .../salary-block-value.component.ts | 8 ++--- .../shared/global-filters-form-group.ts | 8 +++-- src/app/services/admin-tools.service.ts | 11 +++++-- src/app/services/authorization.service.ts | 10 ++++++ src/app/services/totp.service.ts | 8 ++--- .../shared/directives/format-as-money.pipe.ts | 13 +++++--- src/app/shared/services/auth/auth.service.ts | 28 ++++++++++------- .../shared/test-utils/mock-auth.service.ts | 14 +++++++-- 19 files changed, 158 insertions(+), 86 deletions(-) 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/models/extended/application-user-extended.ts b/src/app/models/extended/application-user-extended.ts index 752a24c6..893d1350 100644 --- a/src/app/models/extended/application-user-extended.ts +++ b/src/app/models/extended/application-user-extended.ts @@ -59,7 +59,7 @@ export class ApplicationUserExtended implements ApplicationUser { } get isMfaEnabled(): boolean { - return this.instance.isMfaEnabled + return this.instance.isMfaEnabled; } constructor(public readonly instance: ApplicationUser) { 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 index b80bb8e4..f1bec234 100644 --- 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 @@ -3,12 +3,31 @@
- - +
+ +
+
+ +
+ + + QR code +
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 index 4bf42efe..75c3bad5 100644 --- 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 @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; +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"; @@ -8,9 +8,8 @@ import { untilDestroyed } from "@shared/subscriptions/until-destroyed"; styleUrls: ["./generate-qr-page.component.scss"], }) export class GenerateQrPageComponent implements OnDestroy { - + shoQrCodeDialog = false; qrCodeSource: string | null = null; - generatedQRBase64: string | null = null; constructor( @@ -21,7 +20,6 @@ export class GenerateQrPageComponent implements OnDestroy { } generateQrCode(): void { - if (this.qrCodeSource == null) { return; } @@ -29,11 +27,17 @@ export class GenerateQrPageComponent implements OnDestroy { this.adminToolsService .generateQR(this.qrCodeSource) .pipe(untilDestroyed(this)) - .subscribe((qrCode) => { - this.generatedQRBase64 = qrCode; + .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 04215e11..b54fd066 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 @@ -2,7 +2,7 @@
@@ -10,16 +10,19 @@
-
Please wait for a while...
-
We are preparing some awesome features for you
+
Добро пожаловать
+
Загружаем данные
-

Validate TOTP

+

Введите код 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 b81bdb3a..ed1f5982 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 @@ -38,34 +38,37 @@ export class AuthCallbackComponent implements OnInit { } this.authService.completeAuthentication().subscribe((x) => { - if (x.isMfaEnabled) { this.showMfaBlock = true; + this.showInfoblock = false; return; } - this.redirectToMainPageOrUrl(); + this.showInfoblock = true; + this.authService.getCurrentUser().subscribe((user) => { + this.redirectToMainPageOrUrl(); + }); }); } validateTotp(): void { - - if (this.totpCode == null || - this.totpCode.length !== 6) { - + if (this.totpCode == null || this.totpCode.length !== 6) { this.showTotpInvalid = true; return; } - this.totpService.verifyTotp(this.totpCode) - .subscribe((result) => { - if (result.result) { - this.showTotpInvalid = false; + this.totpService.verifyTotp(this.totpCode).subscribe((result) => { + if (result.result) { + this.showTotpInvalid = false; + this.showInfoblock = true; + this.authService.getCurrentUser().subscribe((user) => { this.redirectToMainPageOrUrl(); - } + }); + } - this.showTotpInvalid = true; - }); + this.showInfoblock = false; + this.showTotpInvalid = true; + }); } private redirectToMainPageOrUrl(): 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/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/services/admin-tools.service.ts b/src/app/services/admin-tools.service.ts index 2f4f6b75..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; @@ -37,7 +41,10 @@ export class AdminToolsService { return this.api.get(this.apiUrl + "configs"); } - generateQR(value: string): Observable { - return this.api.post(this.apiUrl + "generate-qr", { value, pizelSize: 20 }); + 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/totp.service.ts b/src/app/services/totp.service.ts index e8e56224..ded68fb9 100644 --- a/src/app/services/totp.service.ts +++ b/src/app/services/totp.service.ts @@ -8,7 +8,7 @@ export interface SetupTotpResponse { } export interface VerifyTotpResponse { - result: boolean; + result: boolean; } @Injectable({ @@ -21,7 +21,7 @@ export class TotpService { return this.api.get("/api/totp/setup"); } - verifyTotp(code: string): Observable { - return this.api.post("/api/totp/verify", { totpCode: code }); - } + verifyTotp(code: string): Observable { + return this.api.post("/api/totp/verify", { totpCode: code }); + } } 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/services/auth/auth.service.ts b/src/app/shared/services/auth/auth.service.ts index 8380eabd..77a00d36 100644 --- a/src/app/shared/services/auth/auth.service.ts +++ b/src/app/shared/services/auth/auth.service.ts @@ -2,7 +2,10 @@ 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 { + AuthorizationService, + CheckTotpResponse, +} from "@services/authorization.service"; import { map, switchMap } from "rxjs/operators"; import { ApplicationUserExtended } from "@models/extended"; import { AuthSessionService } from "./auth.session.service"; @@ -13,7 +16,7 @@ export interface IAuthService { login(): Promise; - completeAuthentication(): Observable; + completeAuthentication(): Observable; getAuthorizationHeaderValue(): string | null; @@ -66,24 +69,27 @@ export class AuthService implements IAuthService { await this.oidcManager.login(); } - completeAuthentication(): Observable { + completeAuthentication(): Observable { return this.oidcManager.completeAuthentication().pipe( switchMap((x) => { this.authorizationInfo = x; - return this.reloadInternalProperties().pipe(map((appUser) => appUser)); + this.session.auth = this.authorizationInfo ?? null; + + return this.authorizationService + .checkTotpRequired() + .pipe(map((r) => r)); }) ); } private reloadInternalProperties(): Observable { this.session.auth = this.authorizationInfo ?? null; - return this.authorizationService.getMe() - .pipe( - map((appUser) => { - this.saveCurrentUser(appUser); - return appUser; - }) - ); + return this.authorizationService.getMe().pipe( + map((appUser) => { + this.saveCurrentUser(appUser); + return appUser; + }) + ); } private saveCurrentUser(appUser: ApplicationUser): void { diff --git a/src/app/shared/test-utils/mock-auth.service.ts b/src/app/shared/test-utils/mock-auth.service.ts index e79ce08e..40deeebf 100644 --- a/src/app/shared/test-utils/mock-auth.service.ts +++ b/src/app/shared/test-utils/mock-auth.service.ts @@ -5,6 +5,7 @@ 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(); @@ -19,12 +20,19 @@ export class MockAuthService implements IAuthService { return ""; } - completeAuthentication(): Observable { - return of(new TestApplicationUser(UserRole.Interviewer)); + 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)) + ); } login(): Promise { From 2fd64dd1fb037f64c4c821b59f0a5eab7d42d784 Mon Sep 17 00:00:00 2001 From: "maxim.gorbatyuk" Date: Tue, 28 Jan 2025 09:26:00 +0500 Subject: [PATCH 3/4] QR adjusted --- .../generate-qr-code-page/generate-qr-page.component.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index f1bec234..d22fcfbb 100644 --- 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 @@ -27,7 +27,10 @@ [show]="shoQrCodeDialog" (closed)="onQrModalDlgClose()" [header]="'Сгенерированный QR'" + [additionalCss]="'modal-lg'" > - QR code +
+ QR code +
From c00625917f4a24fa97a114283da0eb6ea35fb1ba Mon Sep 17 00:00:00 2001 From: "maxim.gorbatyuk" Date: Tue, 28 Jan 2025 20:03:12 +0500 Subject: [PATCH 4/4] Added TOTP validation --- src/app/app.component.ts | 11 +-- src/app/components/navbar/navbar.component.ts | 29 ++---- .../auth-callback.component.html | 55 +++++++---- .../auth-callback/auth-callback.component.ts | 36 ++++--- .../home/components/me/me.component.html | 96 ++++++++++++++++++- .../home/components/me/me.component.ts | 63 ++++++++++-- .../components/salaries-per-profession.ts | 2 +- .../user-page/user-page.component.html | 44 ++++++++- src/app/services/totp.service.ts | 8 +- .../user-profile-page.component.html | 43 --------- .../user-profile-page.component.spec.ts | 24 ----- .../user-profile-page.component.ts | 11 --- .../shared/guards/active-user.guard.spec.ts | 6 +- src/app/shared/guards/auth.guard.ts | 1 - src/app/shared/services/auth/auth.service.ts | 23 ++++- .../auth/oidc-user-manager.service.ts | 6 +- src/app/shared/shared.module.ts | 2 - .../shared/test-utils/mock-auth.service.ts | 6 ++ 18 files changed, 301 insertions(+), 165 deletions(-) delete mode 100644 src/app/shared/components/user-profile-page/user-profile-page.component.html delete mode 100644 src/app/shared/components/user-profile-page/user-profile-page.component.spec.ts delete mode 100644 src/app/shared/components/user-profile-page/user-profile-page.component.ts 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/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/modules/home/components/auth-callback/auth-callback.component.html b/src/app/modules/home/components/auth-callback/auth-callback.component.html index b54fd066..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,28 +1,41 @@ -
-
+
+
- -
-
-
-
-
-
Добро пожаловать
-
Загружаем данные
-
+
+
Добро пожаловать
+
Загружаем данные
+
+ +
+
-
-

Введите код MFA (TOTP)

- - +
+
+
Введите код 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 ed1f5982..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,13 +1,14 @@ -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; @@ -16,6 +17,7 @@ export class AuthCallbackComponent implements OnInit { showTotpInvalid = false; totpCode = ""; + totpCodeSent = false; constructor( private readonly authService: AuthService, @@ -33,27 +35,32 @@ 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 (x.isMfaEnabled) { - this.showMfaBlock = true; - this.showInfoblock = false; - return; - } + this.authService + .completeAuthentication() + .pipe(untilDestroyed(this)) + .subscribe((x) => { + if (x.isMfaEnabled) { + this.showMfaBlock = true; + this.showInfoblock = false; + } else { + this.showInfoblock = true; - this.showInfoblock = true; - this.authService.getCurrentUser().subscribe((user) => { - this.redirectToMainPageOrUrl(); + this.authService.getCurrentUser().subscribe((user) => { + this.redirectToMainPageOrUrl(); + }); + } }); - }); } validateTotp(): void { if (this.totpCode == null || this.totpCode.length !== 6) { this.showTotpInvalid = true; + this.totpCodeSent = false; return; } @@ -61,6 +68,8 @@ export class AuthCallbackComponent implements OnInit { if (result.result) { this.showTotpInvalid = false; this.showInfoblock = true; + this.totpCodeSent = true; + this.authService.getCurrentUser().subscribe((user) => { this.redirectToMainPageOrUrl(); }); @@ -68,6 +77,7 @@ export class AuthCallbackComponent implements OnInit { this.showInfoblock = false; this.showTotpInvalid = true; + this.totpCodeSent = false; }); } @@ -85,4 +95,6 @@ export class AuthCallbackComponent implements OnInit { 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/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/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/totp.service.ts b/src/app/services/totp.service.ts index ded68fb9..bf33400a 100644 --- a/src/app/services/totp.service.ts +++ b/src/app/services/totp.service.ts @@ -17,8 +17,12 @@ export interface VerifyTotpResponse { export class TotpService { constructor(private api: ApiService) {} - setupTotp(): Observable { - return this.api.get("/api/totp/setup"); + enableMfa(): Observable { + return this.api.post("/api/totp/enable"); + } + + disableMfa(): Observable { + return this.api.post("/api/totp/disable"); } verifyTotp(code: string): Observable { 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/guards/active-user.guard.spec.ts b/src/app/shared/guards/active-user.guard.spec.ts index 45406cfa..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 77a00d36..b8e9bc5d 100644 --- a/src/app/shared/services/auth/auth.service.ts +++ b/src/app/shared/services/auth/auth.service.ts @@ -14,6 +14,8 @@ import { IdToken, User } from "@auth0/auth0-angular"; export interface IAuthService { getCurrentUser(): Observable; + getCurrentUserFromStorage(): Observable; + login(): Promise; completeAuthentication(): Observable; @@ -61,12 +63,24 @@ export class AuthService implements IAuthService { ); } - async login(): Promise { - if (this.isAuthenticated()) { - await this.reloadInternalProperties(); + getCurrentUserFromStorage(): Observable { + this.tryLoadUserFromSession(); + + if (this.authorizationInfo == null) { + return of(null); + } + + if (this.applicationUser != null) { + return of(this.applicationUser); } - await this.oidcManager.login(); + return of(null); + } + + async login(): Promise { + if (this.applicationUser == null) { + await this.oidcManager.login(); + } } completeAuthentication(): Observable { @@ -142,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 40deeebf..3aede9b5 100644 --- a/src/app/shared/test-utils/mock-auth.service.ts +++ b/src/app/shared/test-utils/mock-auth.service.ts @@ -35,6 +35,12 @@ export class MockAuthService implements IAuthService { ); } + getCurrentUserFromStorage(): Observable { + return of( + new ApplicationUserExtended(new TestApplicationUser(UserRole.Interviewer)) + ); + } + login(): Promise { return Promise.resolve(); }