From ab5f68cc3ba81a4f534dd22e630a596c40ad64d5 Mon Sep 17 00:00:00 2001 From: Celiant Date: Wed, 11 Mar 2026 18:23:19 +0400 Subject: [PATCH 01/10] add 2fa --- api-gateway/src/api/service/account.ts | 88 ++++++++++- api-gateway/src/helpers/users.ts | 21 ++- .../validation/schemas/accounts.ts | 7 +- auth-service/package.json | 9 +- auth-service/src/api/account-service.ts | 84 +++++++++- auth-service/src/entity/otp-secret.ts | 39 +++++ auth-service/src/helpers/otp-helper.ts | 119 +++++++++++++++ common/src/helpers/users.ts | 4 + frontend/src/app/app.module.ts | 14 +- frontend/src/app/services/auth.service.ts | 20 ++- .../src/app/views/login/login.component.ts | 59 ++++--- .../otp-codes-dialog.component.html | 18 +++ .../otp-codes-dialog.component.scss | 144 ++++++++++++++++++ .../otp-codes-dialog.component.ts | 57 +++++++ .../otp-config-dialog.component.html | 37 +++++ .../otp-config-dialog.component.scss | 140 +++++++++++++++++ .../otp-config-dialog.component.ts | 64 ++++++++ .../otp-dialog/otp-dialog.component.html | 19 +++ .../otp-dialog/otp-dialog.component.scss | 117 ++++++++++++++ .../login/otp-dialog/otp-dialog.component.ts | 50 ++++++ .../otp-disable-dialog.component.html | 16 ++ .../otp-disable-dialog.component.scss | 122 +++++++++++++++ .../otp-disable-dialog.component.ts | 36 +++++ .../root-profile/root-profile.component.html | 25 +++ .../root-profile/root-profile.component.scss | 20 +++ .../root-profile/root-profile.component.ts | 46 ++++++ .../user-profile/user-profile.component.html | 25 +++ .../user-profile/user-profile.component.scss | 17 +++ .../user-profile/user-profile.component.ts | 48 ++++++ interfaces/src/type/messages/auth-events.ts | 6 +- 30 files changed, 1434 insertions(+), 37 deletions(-) create mode 100644 auth-service/src/entity/otp-secret.ts create mode 100644 auth-service/src/helpers/otp-helper.ts create mode 100644 frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.html create mode 100644 frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.scss create mode 100644 frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.ts create mode 100644 frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.html create mode 100644 frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.scss create mode 100644 frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.ts create mode 100644 frontend/src/app/views/login/otp-dialog/otp-dialog.component.html create mode 100644 frontend/src/app/views/login/otp-dialog/otp-dialog.component.scss create mode 100644 frontend/src/app/views/login/otp-dialog/otp-dialog.component.ts create mode 100644 frontend/src/app/views/login/otp-disable-dialog/otp-disable-dialog.component.html create mode 100644 frontend/src/app/views/login/otp-disable-dialog/otp-disable-dialog.component.scss create mode 100644 frontend/src/app/views/login/otp-disable-dialog/otp-disable-dialog.component.ts diff --git a/api-gateway/src/api/service/account.ts b/api-gateway/src/api/service/account.ts index 0fc43f2efa..0e4a799130 100644 --- a/api-gateway/src/api/service/account.ts +++ b/api-gateway/src/api/service/account.ts @@ -139,9 +139,9 @@ export class AccountApi { @Body() body: LoginUserDTO ): Promise { try { - const { username, password } = body; + const { username, password, otp } = body; const users = new Users(); - return await users.generateNewToken(username, password); + return await users.generateNewToken(username, password, otp); } catch (error) { await this.logger.warn(error.message, ['API_GATEWAY'], null); throw new HttpException(error.message, error.code || HttpStatus.UNAUTHORIZED); @@ -385,4 +385,88 @@ export class AccountApi { await InternalException(error, this.logger, user.id); } } + + @Post('otp/generate') + @Auth() + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + type: InternalServerErrorDTO, + }) + async generateOtp(@AuthUser() user: IAuthUser,) { + const users = new Users(); + try { + const code = await users.otpGenerateSecret(user.id); + + return code + + } catch (error) { + await this.logger.error(error.message, ['API_GATEWAY']); + throw new HttpException(error.message, error.code || HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Post('otp/confirm') + @Auth() + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + type: InternalServerErrorDTO, + }) + async confirmOtp( + @AuthUser() user: IAuthUser, + @Body() body, + ) { + const users = new Users(); + try { + const token = body.token; + const result = await users.otpConfirmSecret(user.id, token); + + return result; + + } catch (error) { + await this.logger.error(error.message, ['API_GATEWAY']); + throw new HttpException(error.message, error.code || HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Get('otp/status') + @Auth() + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + type: InternalServerErrorDTO, + }) + async getOtpStatus( + @AuthUser() user: IAuthUser, + ){ + const users = new Users(); + try { + const result = await users.otpGetStatus(user.id); + + return result; + + } catch (error) { + await this.logger.error(error.message, ['API_GATEWAY']); + throw new HttpException(error.message, error.code || HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Post('otp/deactivate') + @Auth() + @ApiInternalServerErrorResponse({ + description: 'Internal server error.', + type: InternalServerErrorDTO, + }) + async deactivateOtp( + @AuthUser() user: IAuthUser, + ){ + const users = new Users(); + try { + const result = await users.otpDeactivate(user.id); + + return result; + + } catch (error) { + await this.logger.error(error.message, ['API_GATEWAY']); + throw new HttpException(error.message, error.code || HttpStatus.INTERNAL_SERVER_ERROR); + } + } } diff --git a/api-gateway/src/helpers/users.ts b/api-gateway/src/helpers/users.ts index b1045d74a4..b973bef156 100644 --- a/api-gateway/src/helpers/users.ts +++ b/api-gateway/src/helpers/users.ts @@ -175,9 +175,10 @@ export class Users extends NatsService { * Register new token * @param username * @param password + * @param otp */ - public async generateNewToken(username: string, password: string): Promise { - return await this.sendMessage(AuthEvents.GENERATE_NEW_TOKEN, { username, password }); + public async generateNewToken(username: string, password: string, otp?: string): Promise { + return await this.sendMessage(AuthEvents.GENERATE_NEW_TOKEN, { username, password, otp }); } public async generateNewAccessToken(refreshToken: string): Promise { @@ -447,6 +448,22 @@ export class Users extends NatsService { ): Promise { return await this.sendMessage(AuthEvents.GENERATE_RELAYER_ACCOUNT, { user }); } + + public async otpGenerateSecret(userId: string) { + return await this.sendMessage(AuthEvents.OTP_GENERATE_SECRET, { userId }); + } + + public async otpConfirmSecret(userId: any, token: string) { + return await this.sendMessage(AuthEvents.OTP_CONFIRM_SECRET, { userId, token }); + } + + public async otpGetStatus(userId: string) { + return await this.sendMessage(AuthEvents.OTP_GET_STATUS, { userId }); + } + + public async otpDeactivate(userId: string) { + return await this.sendMessage(AuthEvents.OTP_DEACTIVATE, { userId }); + } } @Injectable() diff --git a/api-gateway/src/middlewares/validation/schemas/accounts.ts b/api-gateway/src/middlewares/validation/schemas/accounts.ts index cb8ff6f5ad..c10e0626f8 100644 --- a/api-gateway/src/middlewares/validation/schemas/accounts.ts +++ b/api-gateway/src/middlewares/validation/schemas/accounts.ts @@ -1,6 +1,6 @@ import * as yup from 'yup'; import fieldsValidation from '../fields-validation.js' -import { IsIn, IsNotEmpty, IsString } from 'class-validator'; +import { IsIn, IsNotEmpty, IsString, IsOptional } from 'class-validator'; import { UserRole } from '@guardian/interfaces'; import { Expose } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger'; @@ -67,6 +67,11 @@ export class LoginUserDTO { @IsString() @IsNotEmpty() password: string; + + @ApiProperty() + @IsString() + @IsOptional() + otp: string; } export class RegisterUserDTO { diff --git a/auth-service/package.json b/auth-service/package.json index 4bb872b564..bad9f7171d 100644 --- a/auth-service/package.json +++ b/auth-service/package.json @@ -18,9 +18,11 @@ "@nestjs/common": "^11.0.11", "@nestjs/core": "^11.0.11", "@nestjs/microservices": "^11.0.11", + "@sendgrid/mail": "^7.7.0", "axios": "^1.8.3", "base-x": "^4.0.0", "base64url": "^3.0.1", + "cron": "^2.4.0", "dotenv": "^16.0.0", "express": "^5.1.0", "gulp": "^5.0.0", @@ -28,12 +30,17 @@ "gulp-sourcemaps": "^3.0.0", "gulp-typescript": "^6.0.0-alpha.1", "jsonwebtoken": "^8.5.1", + "moment": "^2.29.4", + "moment-timezone": "^0.5.45", + "node-quickbooks": "^2.0.43", "node-vault": "^0.10.0", "pako": "^2.1.0", "prom-client": "^14.1.1", "prometheus-api-metrics": "4.0.0", "reflect-metadata": "^0.1.13", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "time2fa": "^1.4.2", + "ts-enum-util": "^4.0.2" }, "description": "", "devDependencies": { diff --git a/auth-service/src/api/account-service.ts b/auth-service/src/api/account-service.ts index 594bf7ddcc..7be6bd528e 100644 --- a/auth-service/src/api/account-service.ts +++ b/auth-service/src/api/account-service.ts @@ -1,5 +1,5 @@ import { User } from '../entity/user.js'; -import { DatabaseServer, MessageError, MessageResponse, NatsService, PinoLogger, ProviderAuthUser, Singleton } from '@guardian/common'; +import { DatabaseServer, MessageError, MessageResponse, NatsService, PinoLogger, ProviderAuthUser, Singleton, DataBaseHelper } from '@guardian/common'; import { AuthEvents, GenerateUUIDv4, @@ -23,6 +23,7 @@ import { import { UserUtils, UserPassword, PasswordType, UserAccessTokenService, UserProp } from '#utils'; import { passwordComplexity, PasswordError } from '#constants'; import { HttpStatus } from '@nestjs/common'; +import { OtpHelper } from '../helpers/otp-helper.js'; /** * Account service @@ -351,13 +352,19 @@ export class AccountService extends NatsService { }); this.getMessages(AuthEvents.GENERATE_NEW_TOKEN, - async (msg: { username: string, password: string }) => { + async (msg: { username: string, password: string, otp: string }) => { try { - const { username, password } = msg; + const { username, password, otp } = msg; const user = await UserUtils.getUser({ username, template: { $ne: true } }, UserProp.RAW); if (user) { if (user.passwordVersion === PasswordType.V2) { if (await UserPassword.verifyPasswordV2(user, password)) { + if (await OtpHelper.isConfiguredFor(user) && !otp) { + return new MessageResponse({ success: false, otprequired: true }); + } + if (!await OtpHelper.checkOtp(user, otp)) { + return new MessageError('OTP not valid'); + } const userAccessTokenService = await UserAccessTokenService.New(); const token = userAccessTokenService.generateRefreshToken(user); if (!Array.isArray(user.refreshToken)) { @@ -538,5 +545,76 @@ export class AccountService extends NatsService { return new MessageError(error); } }); + + this.getMessages(AuthEvents.OTP_GENERATE_SECRET, async (msg) => { + try { + const { userId } = msg; + + const user = await new DataBaseHelper(User).findOne({ id: userId }); + if (!user) + return new MessageError('Invalid user'); + + const key = await OtpHelper.generateNewSecretFor(user); + + return new MessageResponse(key); + } catch (error) { + await logger.error(error, ['AUTH_SERVICE', 'OTP_GENERATE_SECRET']); + return new MessageError(error); + } + }); + + // + this.getMessages(AuthEvents.OTP_CONFIRM_SECRET, async (msg) => { + try { + const { userId, token } = msg; + + const user = await new DataBaseHelper(User).findOne({ id: userId }); + if (!user) + return new MessageError('Invalid user'); + const result = await OtpHelper.confirmNewSecret(user, token); + if (result) { + const codes = await OtpHelper.generateBackupCodes(user); + return new MessageResponse({ success: true, backupCodes: codes }); + } + else + return new MessageResponse({ success: false }); + } catch (error) { + await logger.error(error, ['AUTH_SERVICE', 'OTP_GENERATE_SECRET']); + return new MessageError(error); + } + }); + + + this.getMessages(AuthEvents.OTP_GET_STATUS, async (msg) => { + try { + const { userId } = msg; + + const user = await new DataBaseHelper(User).findOne({ id: userId }); + if (!user) + return new MessageError('Invalid user'); + const result = await OtpHelper.isConfiguredFor(user); + + return new MessageResponse({ enabled: result }); + } catch (error) { + await logger.error(error, ['AUTH_SERVICE', 'OTP_GET_STATUS']); + return new MessageError(error); + } + }); + + this.getMessages(AuthEvents.OTP_DEACTIVATE, async (msg) => { + try { + const { userId } = msg; + + const user = await new DataBaseHelper(User).findOne({ id: userId }); + if (!user) + return new MessageError('Invalid user'); + const result = await OtpHelper.deactivate(user); + + return new MessageResponse({ enabled: result }); + } catch (error) { + await logger.error(error, ['AUTH_SERVICE', 'OTP_DEACTIVATE']); + return new MessageError(error); + } + }); } } diff --git a/auth-service/src/entity/otp-secret.ts b/auth-service/src/entity/otp-secret.ts new file mode 100644 index 0000000000..d1a41810ce --- /dev/null +++ b/auth-service/src/entity/otp-secret.ts @@ -0,0 +1,39 @@ +import { Entity, Property} from '@mikro-orm/core'; +import { BaseEntity } from '@guardian/common'; + +/** + * Invite collection + */ +@Entity() +export class OtpSecret extends BaseEntity { + /** + * User Id + */ + @Property({ nullable: true }) + userId: string; + + /** + * Otp Secret + */ + @Property({ nullable: true }) + secret: string; + + /** + * Otp Secret + */ + @Property({ nullable: true }) + config: any; + + @Property({ nullable: true }) + backupCodes: string[]; + + @Property() + enabled: boolean; + + /** + * Encripted + */ + @Property() + encrypted: boolean; + +} \ No newline at end of file diff --git a/auth-service/src/helpers/otp-helper.ts b/auth-service/src/helpers/otp-helper.ts new file mode 100644 index 0000000000..d074cacd52 --- /dev/null +++ b/auth-service/src/helpers/otp-helper.ts @@ -0,0 +1,119 @@ +import { Totp, generateBackupCodes } from "time2fa"; +import { OtpSecret } from "../entity/otp-secret.js"; +import { DataBaseHelper } from "@guardian/common"; +import { User } from "../entity/user.js"; + +export class OtpHelper { + + private static getFilter(user: User) { + return { userId: user.id } + } + + private static async deleteOtp(user: User): Promise { + await new DataBaseHelper(OtpSecret).delete({ ...this.getFilter(user), enabled: true }); + } + + private static async getOtp(user: User): Promise { + return await new DataBaseHelper(OtpSecret).findOne({ ...this.getFilter(user), enabled: true }); + } + + private static getAccountName(user: User): string { + return `${user.username}`; + } + + public static async generateNewSecretFor(user: User) { + + //Delete prev temp secret if exists + await new DataBaseHelper(OtpSecret).delete({ ...this.getFilter(user), enabled: false }); + + //Generate secret + const key = Totp.generateKey({ issuer: "OS Guardian", user: this.getAccountName(user) }); + const entity = await new DataBaseHelper(OtpSecret).create({ + userId: user.id, + secret: key.secret, + config: key.config, + encrypted: false, + enabled: false + }); + new DataBaseHelper(OtpSecret).save(entity); + + return key; + } + + public static async confirmNewSecret(user: User, token: string): Promise { + const temp = await new DataBaseHelper(OtpSecret).findOne({ ...this.getFilter(user), enabled: false }); + if (!temp) + return false; + try { + const valid = Totp.validate({ passcode: token, secret: temp.secret }); + if (!valid) + return false; + //Delete prev secret if exists + await this.deleteOtp(user); + temp.enabled = true; + await new DataBaseHelper(OtpSecret).save(temp); + + } catch (e) { + return false; + } + return true; + } + + public static async generateBackupCodes(user: User): Promise { + const otp = await this.getOtp(user); + if (!otp) + return undefined; + if (otp.backupCodes && otp.backupCodes.length > 0) + throw new Error('Backup codes already cenerated'); + otp.backupCodes = generateBackupCodes(); + await new DataBaseHelper(OtpSecret).save(otp); + return otp.backupCodes; + } + + public static async isConfiguredFor(user: User): Promise { + const key = await this.getOtp(user); + if (key) + return true + else + return false + } + + public static async isValidToken(user: User, token: string): Promise { + + const key = await this.getOtp(user); + if (!key) + return false;// not configured + + if(!token) + return false;// configured but not provided + + const result = Totp.validate({ passcode: token, secret: key.secret }) + return result; + + } + + /** + * Check user otp if required + * @returns + */ + public static async checkOtp(user: User, token: string): Promise { + const configured = await this.isConfiguredFor(user); + if (!configured) + return true; //Not required - ignore + if (!token) + return false; //Required and empty otp - reject + + //Validate otp + try { + const isValid = this.isValidToken(user, token); + return isValid; + } + catch (e) { + return false; + } + } + + public static async deactivate(user: User): Promise { + await this.deleteOtp(user); + } +} \ No newline at end of file diff --git a/common/src/helpers/users.ts b/common/src/helpers/users.ts index 2f7584fd94..c72b60f194 100644 --- a/common/src/helpers/users.ts +++ b/common/src/helpers/users.ts @@ -364,4 +364,8 @@ export class Users extends NatsService { public async getRemoteUsers(did: string, userId: string | null): Promise { return await this.sendMessage(AuthEvents.GET_REMOTE_USERS, { did, userId }); } + + public async otpGenerateSecret(userId: string) { + return await this.sendMessage(AuthEvents.OTP_GENERATE_SECRET, { userId }); + } } diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index d3a6f5ec25..eb33cf5089 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -9,6 +9,7 @@ import { AppRoutingModule, PermissionsGuard } from './app-routing.module'; import { AppComponent } from './app.component'; import { SchemaHelper } from '@guardian/interfaces'; import { CheckboxModule } from 'primeng/checkbox'; +import { ClipboardModule } from '@angular/cdk/clipboard'; //Services import { AuthInterceptor, AuthService } from './services/auth.service'; import { ProfileService } from './services/profile.service'; @@ -61,6 +62,10 @@ import { NotificationsComponent } from './views/notifications/notifications.comp import { RolesViewComponent } from './views/roles/roles-view.component'; import { UsersManagementComponent } from './views/user-management/user-management.component'; import { UsersManagementDetailComponent } from './views/user-management-detail/user-management-detail.component'; +import { OtpDialogComponent } from './views/login/otp-dialog/otp-dialog.component'; +import { OtpConfigDialogComponent } from './views/login/otp-config-dialog/otp-config-dialog.component'; +import { OtpDisableDialogComponent } from './views/login/otp-disable-dialog/otp-disable-dialog.component'; +import { OtpCodesDialogComponent } from './views/login/otp-codes-dialog/otp-codes-dialog.component'; //Components import { InfoComponent } from './components/info/info/info.component'; import { BrandingComponent } from './views/branding/branding.component'; @@ -185,7 +190,11 @@ import { TreeTableModule } from 'primeng/treetable' RelayerAccountsComponent, UsersManagementDetailComponent, WorkerTasksComponent, - UserKeysDialog + UserKeysDialog, + OtpDialogComponent, + OtpConfigDialogComponent, + OtpDisableDialogComponent, + OtpCodesDialogComponent ], exports: [], bootstrap: [AppComponent], @@ -205,7 +214,8 @@ import { TreeTableModule } from 'primeng/treetable' ToastrModule.forRoot(), QRCodeModule, ButtonModule, - InputTextModule, + InputTextModule, + ClipboardModule, SelectButtonModule, DropdownModule, ButtonModule, diff --git a/frontend/src/app/services/auth.service.ts b/frontend/src/app/services/auth.service.ts index 41468debdb..20a4055917 100644 --- a/frontend/src/app/services/auth.service.ts +++ b/frontend/src/app/services/auth.service.ts @@ -21,14 +21,30 @@ export class AuthService { this.refreshTokenSubject = new Subject(); } - public login(username: string, password: string): Observable { - return this.http.post(`${this.url}/login`, { username, password }); + public login(username: string, password: string, otp?: string): Observable { + return this.http.post(`${this.url}/login`, { username, password, otp }); } public changePassword(username: string, oldPassword: string, newPassword: string): Observable { return this.http.post(`${this.url}/change-password`, { username, oldPassword, newPassword }); } + public generateOtpSecret(): Observable { + return this.http.post(`${this.url}/otp/generate`, {}); + } + + public confirmOtpSecret(token: string): Observable { + return this.http.post(`${this.url}/otp/confirm`, { token }); + } + + public getOtpStatus(): Observable { + return this.http.get(`${this.url}/otp/status`); + } + + public deactivateOtp(): Observable { + return this.http.post(`${this.url}/otp/deactivate`, {}); + } + public updateAccessToken(): Observable { return this.http.post(`${this.url}/access-token`, { refreshToken: this.getRefreshToken() }).pipe( map(result => { diff --git a/frontend/src/app/views/login/login.component.ts b/frontend/src/app/views/login/login.component.ts index 93193863ae..7ef82bfe1d 100644 --- a/frontend/src/app/views/login/login.component.ts +++ b/frontend/src/app/views/login/login.component.ts @@ -1,26 +1,27 @@ -import {Component, OnDestroy, OnInit, AfterViewChecked, QueryList, ViewChildren, ElementRef} from '@angular/core'; -import {Router} from '@angular/router'; -import {AbstractControl, UntypedFormControl, UntypedFormGroup, ValidationErrors, Validators,} from '@angular/forms'; -import {AuthService} from '../../services/auth.service'; -import {UserCategory, UserRole} from '@guardian/interfaces'; -import {AuthStateService} from 'src/app/services/auth-state.service'; -import {Observable, Subject, Subscription} from 'rxjs'; -import {noWhitespaceValidator} from 'src/app/validators/no-whitespace-validator'; -import {WebSocketService} from 'src/app/services/web-socket.service'; -import {QrCodeDialogComponent} from 'src/app/components/qr-code-dialog/qr-code-dialog.component'; -import {MeecoVCSubmitDialogComponent} from 'src/app/components/meeco-vc-submit-dialog/meeco-vc-submit-dialog.component'; -import {environment} from 'src/environments/environment'; -import {takeUntil} from 'rxjs/operators'; -import {BrandingService} from '../../services/branding.service'; -import {DialogService, DynamicDialogRef} from 'primeng/dynamicdialog'; +import { Component, OnDestroy, OnInit, AfterViewChecked, QueryList, ViewChildren, ElementRef } from '@angular/core'; +import { Router } from '@angular/router'; +import { AbstractControl, UntypedFormControl, UntypedFormGroup, ValidationErrors, Validators, } from '@angular/forms'; +import { AuthService } from '../../services/auth.service'; +import { UserCategory, UserRole } from '@guardian/interfaces'; +import { AuthStateService } from 'src/app/services/auth-state.service'; +import { Observable, Subject, Subscription } from 'rxjs'; +import { noWhitespaceValidator } from 'src/app/validators/no-whitespace-validator'; +import { WebSocketService } from 'src/app/services/web-socket.service'; +import { QrCodeDialogComponent } from 'src/app/components/qr-code-dialog/qr-code-dialog.component'; +import { MeecoVCSubmitDialogComponent } from 'src/app/components/meeco-vc-submit-dialog/meeco-vc-submit-dialog.component'; +import { environment } from 'src/environments/environment'; +import { takeUntil } from 'rxjs/operators'; +import { BrandingService } from '../../services/branding.service'; +import { DialogService, DynamicDialogRef } from 'primeng/dynamicdialog'; import { AccountTypeSelectorDialogComponent } from './register-dialogs/account-type-selector-dialog/account-type-selector-dialog.component'; -import {ForgotPasswordDialogComponent} from './forgot-password-dialog/forgot-password-dialog.component'; -import {RegisterDialogComponent} from './register-dialogs/register-dialog/register-dialog.component'; -import {DemoService} from '../../services/demo.service'; -import {ChangePasswordComponent} from './change-password/change-password.component'; +import { ForgotPasswordDialogComponent } from './forgot-password-dialog/forgot-password-dialog.component'; +import { RegisterDialogComponent } from './register-dialogs/register-dialog/register-dialog.component'; +import { DemoService } from '../../services/demo.service'; +import { ChangePasswordComponent } from './change-password/change-password.component'; import { InformService } from 'src/app/services/inform.service'; +import { OtpDialogComponent } from './otp-dialog/otp-dialog.component'; /** * Login page. @@ -145,10 +146,10 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewChecked { } } - private login(login: string, password: string) { + private login(login: string, password: string, otp?: string) { this.loading = true; this.wrongNameOrPassword = false; - this.auth.login(login, password) + this.auth.login(login, password, otp) .subscribe((result) => { this.auth.setRefreshToken(result.refreshToken); this.auth.setUsername(login); @@ -158,6 +159,18 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewChecked { this.router.navigate([home]); }); + if (result.otprequired) { + this.dialogService.open(OtpDialogComponent, { + header: 'Enter Verification Code', + width: '40vw', + closable: false, + }).onClose.subscribe(token => { + if (token) { + this.login(login, password, token); + } + }); + } + if (result.weakPassword) { this.informService.shortWarnMessage( 'Your password is considered weak. For your security, please update it to meet our minimum complexity requirements.', @@ -195,7 +208,7 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewChecked { return; } this.login(userData.username, userData.password); - }, ({error}) => { + }, ({ error }) => { this.error = error.message; this.loading = false; this.brandingLoading = false; @@ -344,7 +357,7 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewChecked { data: { document: event.vc, presentationRequestId: - event.presentation_request_id, + event.presentation_request_id, submissionId: event.submission_id, userRole: event.role, }, diff --git a/frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.html b/frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.html new file mode 100644 index 0000000000..1bfeb29190 --- /dev/null +++ b/frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.html @@ -0,0 +1,18 @@ +
+
If you lose access to your authentication app, use your recovery codes to restore your Guardian + account. Each code can be used only once!
+
+
{{code}}
+
+
+ + +
+
+ \ No newline at end of file diff --git a/frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.scss b/frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.scss new file mode 100644 index 0000000000..616c93c864 --- /dev/null +++ b/frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.scss @@ -0,0 +1,144 @@ +form { + width: 100%; + position: relative; +} + +.codes { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; + margin: 15px; + /* margin-left: auto; */ + /* margin-right: auto; */ + font-size: 24px; + & > div { + text-align: center; + } +} + +button .pi { + margin-right: 8px; +} + +.download-actions { + display: flex; + flex-direction: row; + gap: 8px; + +} + +.dialog-body { + position: relative; + height: auto; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 24px; + + .form-input-container { + display: flex; + flex-direction: column; + width: 100%; + margin-bottom: 24px; + } + + .p-field-label { + color: var(--color-grey-black-1, #181818); + font-family: Inter, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 14px; + margin-bottom: 6px; + } + + .p-field-input { + width: 100%; + border-radius: 8px; + border: 1px solid var(--color-grey-3, #e1e7ef); + background: var(--color-grey-white, #fff); + outline: none !important; + box-shadow: unset !important; + padding: 12px 0 12px 16px; + align-items: center; + align-self: stretch; + transition: border 0.25s ease-in-out; + box-sizing: border-box; + resize: vertical; + + color: var(--color-grey-black-1, #181818); + font-family: Inter, sans-serif; + font-size: 24px; + font-style: normal; + font-weight: 400; + line-height: 16px; + } + + .primary-text-button { + color: var(--color-primary, #4169e2); + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 30px; + /* 214.286% */ + cursor: pointer; + } +} + +.primary-text-button { + color: var(--color-primary, #4169e2); + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 30px; + /* 214.286% */ + cursor: pointer; +} + +:host ::ng-deep .p-button:focus { + box-shadow: unset !important; +} + +.action-buttons { + display: flex; + align-items: center; + button { + width: 100%; + } +} + +.cancel-button { + margin-left: auto; + margin-right: 15px; +} + +.dialog-footer { + border-top: 1px solid var(--guardian-border-color); + padding-top: 20px; +} + +::ng-deep { + .p-dialog-title { + font-family: Poppins, sans-serif; + font-size: 24px !important; + font-style: normal; + font-weight: 600 !important; + } + + .p-dialog { + box-shadow: none; + } + + .p-dialog-header { + border-top-left-radius: 16px !important; + border-top-right-radius: 16px !important; + } + + .p-dialog-content { + border-bottom-left-radius: 16px; + border-bottom-right-radius: 16px; + } +} \ No newline at end of file diff --git a/frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.ts b/frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.ts new file mode 100644 index 0000000000..07c9f42ad7 --- /dev/null +++ b/frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.ts @@ -0,0 +1,57 @@ +import { Component, OnInit } from '@angular/core'; +import { UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { AuthStateService } from 'src/app/services/auth-state.service'; +import { AuthService } from 'src/app/services/auth.service'; +import { noWhitespaceValidator } from 'src/app/validators/no-whitespace-validator'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { ToastrService } from "ngx-toastr"; + +/** + * Registration page. + */ +@Component({ + selector: 'app-otp-codes-dialog', + templateUrl: './otp-codes-dialog.component.html', + styleUrls: ['./otp-codes-dialog.component.scss'] +}) +export class OtpCodesDialogComponent implements OnInit { + public codes: string[] | undefined; + public codesText: string; + constructor( + private dialogRef: DynamicDialogRef, + private dialogConfig: DynamicDialogConfig, + private toastr: ToastrService, + ) { + this.codes = this.dialogConfig.data?.codes; + this.codesText = this.codes?.join('\t') || ''; + } + + ngOnInit() { + } + + onCancelClick() { + this.dialogRef.close(false); + } + + onOkClick() { + this.dialogRef.close(true); + } + + onCopy() { + this.toastr.success('Codes copied'); + } + + saveToFile() { + const downloadLink = document.createElement('a'); + downloadLink.href = window.URL.createObjectURL( + new Blob([Uint8Array.from(new TextEncoder().encode(this.codesText))], { + type: 'application/text' + }) + ); + downloadLink.setAttribute('download', `mgs_recovery_codes.txt`); + document.body.appendChild(downloadLink); + downloadLink.click(); + downloadLink.remove(); + } +} \ No newline at end of file diff --git a/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.html b/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.html new file mode 100644 index 0000000000..0717c1f6c0 --- /dev/null +++ b/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.html @@ -0,0 +1,37 @@ +
+
+
Step 1
+ Use an authentication app (such as Google Authenticator or another) on your mobile device to scan this QR + code. + +
+ +
+
If the scan isn't working, manually enter this key into your authentication app.
+
{{config.secret}} +
+ +
+ +
+
Step 2
+ Enter the code given by the app on your mobile device. +
+
+
+ + +
Wrong or expired code! Please, enter code again to confirm setup +
+
+ +
+
+ \ No newline at end of file diff --git a/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.scss b/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.scss new file mode 100644 index 0000000000..ad2cfc058c --- /dev/null +++ b/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.scss @@ -0,0 +1,140 @@ +form { + width: 100%; + position: relative; +} + +.dialog-body { + position: relative; + display: flex; + flex-direction: column; + gap: 24px; + + .secret { + color: var(--color-primary); + font-size: 14px; + display: flex; + gap: 8px; + i { + cursor: pointer; + } + } + .error { + color: var(--color-accent-red-1); + } + .input-error { + border-color: var(--color-accent-red-1) !important; + } + .header-step { + font-weight: bold; + } + .code-manual { + display: flex; + flex-direction: column; + gap:8px; + align-items: center; + position: relative; + z-index: 1; + + &::after { + content: ""; + position: absolute; + top: -8px; + bottom: -8px; + left: 0px; + right: 0px; + // width: 100%; + // height: 100%; + opacity: 0.1; + border-radius: 8px; + background-color: var(--color-primary); + z-index: -1; + } + } + + .form-input-container { + display: flex; + flex-direction: column; + width: 100%; + gap: 6px; + } + + .p-field-label { + color: var(--color-grey-black-1, #181818); + font-family: Inter, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 14px; + margin-bottom: 6px; + } + + .p-field-input { + width: 100%; + border-radius: 8px; + border: 1px solid var(--color-grey-3, #e1e7ef); + background: var(--color-grey-white, #fff); + outline: none !important; + box-shadow: unset !important; + padding: 12px 0 12px 16px; + align-items: center; + align-self: stretch; + transition: border 0.25s ease-in-out; + box-sizing: border-box; + resize: vertical; + + color: var(--color-grey-black-1, #181818); + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 16px; + } +} + +:host ::ng-deep .p-button:focus { + box-shadow: unset !important; +} + +.dialog-body { + height: auto; + overflow-y: auto; +} + +.action-buttons { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + margin-top: 24px; + .button { + width: 100%; + } +} + +// .dialog-footer { +// border-top: 1px solid var(--guardian-border-color); +// padding-top: 20px; +// } + +::ng-deep { + .p-dialog-title { + font-family: Poppins, sans-serif; + font-size: 24px !important; + font-style: normal; + font-weight: 600 !important; + } + + .p-dialog { + box-shadow: none; + } + + .p-dialog-header { + border-top-left-radius: 16px !important; + border-top-right-radius: 16px !important; + } + + .p-dialog-content { + border-bottom-left-radius: 16px; + border-bottom-right-radius: 16px; + } +} \ No newline at end of file diff --git a/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.ts b/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.ts new file mode 100644 index 0000000000..9219e8fa4c --- /dev/null +++ b/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.ts @@ -0,0 +1,64 @@ +import { Component, OnInit } from '@angular/core'; +import { UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; +import { AuthStateService } from 'src/app/services/auth-state.service'; +import { AuthService } from 'src/app/services/auth.service'; +import { noWhitespaceValidator } from 'src/app/validators/no-whitespace-validator'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; +import { ToastrService } from "ngx-toastr"; + + +/** + * Registration page. + */ +@Component({ + selector: 'app-config-otp-dialog', + templateUrl: './otp-config-dialog.component.html', + styleUrls: ['./otp-config-dialog.component.scss'] +}) +export class OtpConfigDialogComponent implements OnInit { + public form = new UntypedFormGroup({ + token: new UntypedFormControl('', [ + Validators.required, + Validators.minLength(6), + Validators.maxLength(6), + noWhitespaceValidator(), + ]), + }); + + public config: any; + public tokenError: boolean = false; + + constructor( + private dialogRef: DynamicDialogRef, + private dialogConfig: DynamicDialogConfig, + private auth: AuthService, + private toastr: ToastrService, + private authState: AuthStateService + ) { + this.config = this.dialogConfig.data?.config; + } + + ngOnInit() { + } + secretCopied() { + this.toastr.success('Secret copied'); + } + + onNoClick() { + this.dialogRef.close(null); + } + + onEnable() { + const token = this.form.value.token; + + this.auth.confirmOtpSecret(token).subscribe(result => { + if (!result.success) { + this.tokenError = true; + } + else { + this.tokenError = false; + this.dialogRef.close(result.backupCodes); + } + }); + } +} \ No newline at end of file diff --git a/frontend/src/app/views/login/otp-dialog/otp-dialog.component.html b/frontend/src/app/views/login/otp-dialog/otp-dialog.component.html new file mode 100644 index 0000000000..533d784ef3 --- /dev/null +++ b/frontend/src/app/views/login/otp-dialog/otp-dialog.component.html @@ -0,0 +1,19 @@ +
+
+
+
Enter the verification code generated by your mobile application.
+
+ +
+ + +
+
+
+ \ No newline at end of file diff --git a/frontend/src/app/views/login/otp-dialog/otp-dialog.component.scss b/frontend/src/app/views/login/otp-dialog/otp-dialog.component.scss new file mode 100644 index 0000000000..82a5a04434 --- /dev/null +++ b/frontend/src/app/views/login/otp-dialog/otp-dialog.component.scss @@ -0,0 +1,117 @@ +form { + width: 100%; + position: relative; +} + +.dialog-body { + position: relative; + + .form-input-container { + display: flex; + flex-direction: column; + width: 100%; + margin-bottom: 24px; + } + + .p-field-label { + color: var(--color-grey-black-1, #181818); + font-family: Inter, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 14px; + margin-bottom: 6px; + } + + .p-field-input { + width: 100%; + border-radius: 8px; + border: 1px solid var(--color-grey-3, #e1e7ef); + background: var(--color-grey-white, #fff); + outline: none !important; + box-shadow: unset !important; + padding: 12px 0 12px 16px; + align-items: center; + align-self: stretch; + transition: border 0.25s ease-in-out; + box-sizing: border-box; + resize: vertical; + + color: var(--color-grey-black-1, #181818); + font-family: Inter, sans-serif; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 16px; + } + + .primary-text-button { + color: var(--color-primary, #4169e2); + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 30px; + /* 214.286% */ + cursor: pointer; + } +} + +.primary-text-button { + color: var(--color-primary, #4169e2); + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 30px; + /* 214.286% */ + cursor: pointer; +} + +:host ::ng-deep .p-button:focus { + box-shadow: unset !important; +} + +.dialog-body { + height: auto; + overflow-y: auto; +} + +.action-buttons { + display: flex; + flex-direction: column; + gap: 8px; + align-items: center; + button { + width: 100%; + } +} + + +.dialog-footer { + //border-top: 1px solid var(--guardian-border-color); + padding-top: 20px; +} + +::ng-deep { + .p-dialog-title { + font-family: Poppins, sans-serif; + font-size: 24px !important; + font-style: normal; + font-weight: 600 !important; + } + + .p-dialog { + box-shadow: none; + } + + .p-dialog-header { + border-top-left-radius: 16px !important; + border-top-right-radius: 16px !important; + } + + .p-dialog-content { + border-bottom-left-radius: 16px; + border-bottom-right-radius: 16px; + } +} \ No newline at end of file diff --git a/frontend/src/app/views/login/otp-dialog/otp-dialog.component.ts b/frontend/src/app/views/login/otp-dialog/otp-dialog.component.ts new file mode 100644 index 0000000000..10bac7daf0 --- /dev/null +++ b/frontend/src/app/views/login/otp-dialog/otp-dialog.component.ts @@ -0,0 +1,50 @@ +import { Component, OnInit } from '@angular/core'; +import { UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { AuthStateService } from 'src/app/services/auth-state.service'; +import { AuthService } from 'src/app/services/auth.service'; +import { noWhitespaceValidator } from 'src/app/validators/no-whitespace-validator'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +/** + * Registration page. + */ +@Component({ + selector: 'app-otp-dialog', + templateUrl: './otp-dialog.component.html', + styleUrls: ['./otp-dialog.component.scss'] +}) +export class OtpDialogComponent implements OnInit { + public form = new UntypedFormGroup({ + token: new UntypedFormControl('', [ + Validators.required, + Validators.minLength(6), + Validators.maxLength(6), + noWhitespaceValidator(), + ]), + }); + + + constructor( + private dialogRef: DynamicDialogRef, + private dialogConfig: DynamicDialogConfig, + private auth: AuthService, + private router: Router, + private authState: AuthStateService + ) { + + } + + ngOnInit() { + + } + + onNoClick() { + this.dialogRef.close(null); + } + + onChange() { + const token = this.form.value.token; + this.dialogRef.close(token); + } +} \ No newline at end of file diff --git a/frontend/src/app/views/login/otp-disable-dialog/otp-disable-dialog.component.html b/frontend/src/app/views/login/otp-disable-dialog/otp-disable-dialog.component.html new file mode 100644 index 0000000000..a037255403 --- /dev/null +++ b/frontend/src/app/views/login/otp-disable-dialog/otp-disable-dialog.component.html @@ -0,0 +1,16 @@ +
+
Please confirm that you want to deactivate two-factor authentication.
+
+ Turning off 2-Step Verification will remove the extra security on your account, and you'll only use your + password to sign in.
+
+ \ No newline at end of file diff --git a/frontend/src/app/views/login/otp-disable-dialog/otp-disable-dialog.component.scss b/frontend/src/app/views/login/otp-disable-dialog/otp-disable-dialog.component.scss new file mode 100644 index 0000000000..1efaf3ca04 --- /dev/null +++ b/frontend/src/app/views/login/otp-disable-dialog/otp-disable-dialog.component.scss @@ -0,0 +1,122 @@ +form { + width: 100%; + position: relative; +} + +.button.red { + background-color: var(--color-accent-red-1) !important; + color: white !important; +} + +.dialog-body { + position: relative; + height: auto; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom:32px; + + .form-input-container { + display: flex; + flex-direction: column; + width: 100%; + margin-bottom: 24px; + } + + .p-field-label { + color: var(--color-grey-black-1, #181818); + font-family: Inter, sans-serif; + font-size: 12px; + font-style: normal; + font-weight: 500; + line-height: 14px; + margin-bottom: 6px; + } + + .p-field-input { + width: 100%; + border-radius: 8px; + border: 1px solid var(--color-grey-3, #e1e7ef); + background: var(--color-grey-white, #fff); + outline: none !important; + box-shadow: unset !important; + padding: 12px 0 12px 16px; + align-items: center; + align-self: stretch; + transition: border 0.25s ease-in-out; + box-sizing: border-box; + resize: vertical; + + color: var(--color-grey-black-1, #181818); + font-family: Inter, sans-serif; + font-size: 24px; + font-style: normal; + font-weight: 400; + line-height: 16px; + } + + .primary-text-button { + color: var(--color-primary, #4169e2); + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 30px; + /* 214.286% */ + cursor: pointer; + } +} + +.primary-text-button { + color: var(--color-primary, #4169e2); + font-family: Inter, sans-serif; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 30px; + /* 214.286% */ + cursor: pointer; +} + +:host ::ng-deep .p-button:focus { + box-shadow: unset !important; +} + +.action-buttons { + display: flex; + align-items: center; +} + +.cancel-button { + margin-left: auto; + margin-right: 15px; +} + +.dialog-footer { + border-top: 1px solid var(--guardian-border-color); + padding-top: 20px; +} + +::ng-deep { + .p-dialog-title { + font-family: Poppins, sans-serif; + font-size: 24px !important; + font-style: normal; + font-weight: 600 !important; + } + + .p-dialog { + box-shadow: none; + } + + .p-dialog-header { + border-top-left-radius: 16px !important; + border-top-right-radius: 16px !important; + } + + .p-dialog-content { + border-bottom-left-radius: 16px; + border-bottom-right-radius: 16px; + } +} \ No newline at end of file diff --git a/frontend/src/app/views/login/otp-disable-dialog/otp-disable-dialog.component.ts b/frontend/src/app/views/login/otp-disable-dialog/otp-disable-dialog.component.ts new file mode 100644 index 0000000000..cd7419c0e3 --- /dev/null +++ b/frontend/src/app/views/login/otp-disable-dialog/otp-disable-dialog.component.ts @@ -0,0 +1,36 @@ +import { Component, OnInit } from '@angular/core'; +import { UntypedFormControl, UntypedFormGroup, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +import { AuthStateService } from 'src/app/services/auth-state.service'; +import { AuthService } from 'src/app/services/auth.service'; +import { noWhitespaceValidator } from 'src/app/validators/no-whitespace-validator'; +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +/** + * Registration page. + */ +@Component({ + selector: 'app-otp-disable-dialog', + templateUrl: './otp-disable-dialog.component.html', + styleUrls: ['./otp-disable-dialog.component.scss'] +}) +export class OtpDisableDialogComponent implements OnInit { + + constructor( + private dialogRef: DynamicDialogRef, + private dialogConfig: DynamicDialogConfig, + ) { + + } + + ngOnInit() { + } + + onCancelClick() { + this.dialogRef.close(false); + } + + onDisableClick() { + this.dialogRef.close(true); + } +} \ No newline at end of file diff --git a/frontend/src/app/views/root-profile/root-profile.component.html b/frontend/src/app/views/root-profile/root-profile.component.html index 8e098adae1..fe8b7db349 100644 --- a/frontend/src/app/views/root-profile/root-profile.component.html +++ b/frontend/src/app/views/root-profile/root-profile.component.html @@ -54,6 +54,31 @@

Profile

+
Security Settings
+
+ Two-factor authentication +
+ Active +
+
+ Disabled +
+
+
+ When you sign in you'll be asked to use the security code provided + by your Authenticator + App. +
+
+
+ + Set up +
+
+ Deactivate +
+
+
Hedera
diff --git a/frontend/src/app/views/root-profile/root-profile.component.scss b/frontend/src/app/views/root-profile/root-profile.component.scss index 2b1ae0966c..089fde2653 100644 --- a/frontend/src/app/views/root-profile/root-profile.component.scss +++ b/frontend/src/app/views/root-profile/root-profile.component.scss @@ -762,4 +762,24 @@ mat-radio-button { :host ::ng-deep .p-tabview-nav-link { text-decoration: none !important; +} + +.otp-status { + &.disabled { + border-color: var(--color-grey-3) !important; + background-color: var(--color-grey-3); + + span { + color: var(--color-grey-6) !important; + } + } + + &.active { + border-color: var(--color-accent-green-2) !important; + background-color: var(--color-accent-green-2); + + span { + color: var(--color-accent-green-1) !important; + } + } } \ No newline at end of file diff --git a/frontend/src/app/views/root-profile/root-profile.component.ts b/frontend/src/app/views/root-profile/root-profile.component.ts index cd5bd350d3..1926e7be24 100644 --- a/frontend/src/app/views/root-profile/root-profile.component.ts +++ b/frontend/src/app/views/root-profile/root-profile.component.ts @@ -18,6 +18,9 @@ import { prepareVcData } from 'src/app/modules/common/models/prepare-vc-data'; import { RelayerAccountsService } from 'src/app/services/relayer-accounts.service'; import { NewRelayerAccountDialog } from 'src/app/components/new-relayer-account-dialog/new-relayer-account-dialog.component'; import { RelayerAccountDetailsDialog } from 'src/app/components/relayer-account-details-dialog/relayer-account-details-dialog.component'; +import { OtpCodesDialogComponent } from '../login/otp-codes-dialog/otp-codes-dialog.component'; +import { OtpConfigDialogComponent } from '../login/otp-config-dialog/otp-config-dialog.component'; +import { OtpDisableDialogComponent } from '../login/otp-disable-dialog/otp-disable-dialog.component'; import moment from 'moment'; enum OperationMode { @@ -59,6 +62,7 @@ export class RootProfileComponent implements OnInit, OnDestroy { public progress: number = 0; public userTopics: any[] = []; public schema!: Schema; + public is2faEnabled = false; public hederaForm = this.fb.group({ hederaAccountId: ['', Validators.required], hederaAccountKey: ['', Validators.required], @@ -182,6 +186,7 @@ export class RootProfileComponent implements OnInit, OnDestroy { ); this.loadProfile(); this.step = 'HEDERA'; + this.refreshOtpStatus(); } ngOnDestroy(): void { @@ -835,4 +840,45 @@ export class RootProfileComponent implements OnInit, OnDestroy { this.updateBalance(row); } } + + refreshOtpStatus() { + this.auth.getOtpStatus().subscribe((result) => { + this.is2faEnabled = result.enabled; + }); + } + + generate2fa() { + this.auth.generateOtpSecret().subscribe(config => { + + this.dialogService.open(OtpConfigDialogComponent, { + header: 'Enable two-factor authentication', + width: '50vw', + closable: false, + data: { config: config } + }).onClose.subscribe((codes) => { + this.refreshOtpStatus(); + if (codes && codes.length) { + this.dialogService.open(OtpCodesDialogComponent, { + header: "Save your recovery codes", + data: { codes: codes } + }); + } + }) + }); + } + + deactivate2fa() { + this.dialogService.open(OtpDisableDialogComponent, { + header: 'Deactivate two-factor authentication', + width: '50vw', + closable: false, + + }).onClose.subscribe(result => { + if (result == true) { + this.auth.deactivateOtp().subscribe(() => { + this.refreshOtpStatus(); + }); + } + }) + } } diff --git a/frontend/src/app/views/user-profile/user-profile.component.html b/frontend/src/app/views/user-profile/user-profile.component.html index f45e1835d1..a27dbac685 100644 --- a/frontend/src/app/views/user-profile/user-profile.component.html +++ b/frontend/src/app/views/user-profile/user-profile.component.html @@ -50,6 +50,31 @@

Profile

+
Security Settings
+
+ Two-factor authentication +
+ Active +
+
+ Disabled +
+
+
+ When you sign in you'll be asked to use the security code provided + by your Authenticator + App. +
+
+
+ + Set up +
+
+ Deactivate +
+
+
Hedera
Hedera Account ID diff --git a/frontend/src/app/views/user-profile/user-profile.component.scss b/frontend/src/app/views/user-profile/user-profile.component.scss index 5248103e98..beb7f32405 100644 --- a/frontend/src/app/views/user-profile/user-profile.component.scss +++ b/frontend/src/app/views/user-profile/user-profile.component.scss @@ -1243,4 +1243,21 @@ mat-radio-button { :host ::ng-deep .p-tabview-nav-link { text-decoration: none !important; +} + +.otp-status { + &.disabled { + border-color: var(--color-grey-3) !important; + background-color: var(--color-grey-3); + span { + color: var(--color-grey-6) !important; + } + } + &.active{ + border-color: var(--color-accent-green-2) !important; + background-color: var(--color-accent-green-2); + span { + color: var(--color-accent-green-1) !important; + } + } } \ No newline at end of file diff --git a/frontend/src/app/views/user-profile/user-profile.component.ts b/frontend/src/app/views/user-profile/user-profile.component.ts index d5c0e68986..82019ea34f 100644 --- a/frontend/src/app/views/user-profile/user-profile.component.ts +++ b/frontend/src/app/views/user-profile/user-profile.component.ts @@ -22,8 +22,12 @@ import { CustomConfirmDialogComponent } from 'src/app/modules/common/custom-conf import { RelayerAccountsService } from 'src/app/services/relayer-accounts.service'; import { NewRelayerAccountDialog } from 'src/app/components/new-relayer-account-dialog/new-relayer-account-dialog.component'; import { RelayerAccountDetailsDialog } from 'src/app/components/relayer-account-details-dialog/relayer-account-details-dialog.component'; +import { OtpConfigDialogComponent } from '../login/otp-config-dialog/otp-config-dialog.component'; +import { OtpDisableDialogComponent } from '../login/otp-disable-dialog/otp-disable-dialog.component'; +import { OtpCodesDialogComponent } from '../login/otp-codes-dialog/otp-codes-dialog.component'; import moment from 'moment'; + enum OperationMode { None, Generate, @@ -77,6 +81,8 @@ export class UserProfileComponent implements OnInit { public noFilterResults: boolean = false; + public is2faEnabled = false; + public get hasRegistries(): boolean { return this.standardRegistriesList.length > 0; } @@ -461,6 +467,7 @@ export class UserProfileComponent implements OnInit { ); this.loadDate(); this.update(); + this.refreshOtpStatus(); } ngOnDestroy(): void { @@ -1256,4 +1263,45 @@ export class UserProfileComponent implements OnInit { this.updateBalance(row); } } + + refreshOtpStatus() { + this.auth.getOtpStatus().subscribe((result) => { + this.is2faEnabled = result.enabled; + }); + } + + generate2fa() { + this.auth.generateOtpSecret().subscribe(config => { + + this.dialogService.open(OtpConfigDialogComponent, { + header: 'Enable two-factor authentication', + width: '50vw', + closable: false, + data: { config: config } + }).onClose.subscribe((codes) => { + this.refreshOtpStatus(); + if (codes && codes.length) { + this.dialogService.open(OtpCodesDialogComponent, { + header: "Save your recovery codes", + data: { codes: codes } + }); + } + }) + }); + } + + deactivate2fa() { + this.dialogService.open(OtpDisableDialogComponent, { + header: 'Deactivate two-factor authentication', + width: '50vw', + closable: false, + + }).onClose.subscribe(result => { + if (result == true) { + this.auth.deactivateOtp().subscribe(() => { + this.refreshOtpStatus(); + }); + } + }) + } } diff --git a/interfaces/src/type/messages/auth-events.ts b/interfaces/src/type/messages/auth-events.ts index 00f74705da..552375fa3a 100644 --- a/interfaces/src/type/messages/auth-events.ts +++ b/interfaces/src/type/messages/auth-events.ts @@ -50,5 +50,9 @@ export enum AuthEvents { GET_CURRENT_RELAYER_ACCOUNT = 'GET_CURRENT_RELAYER_ACCOUNT', CREATE_RELAYER_ACCOUNT = 'CREATE_RELAYER_ACCOUNT', GENERATE_RELAYER_ACCOUNT = 'GENERATE_RELAYER_ACCOUNT', - RELAYER_ACCOUNT_EXIST = 'RELAYER_ACCOUNT_EXIST' + RELAYER_ACCOUNT_EXIST = 'RELAYER_ACCOUNT_EXIST', + OTP_GENERATE_SECRET="OTP_GENERATE_SECRET", + OTP_CONFIRM_SECRET="OTP_CONFIRM_SECRET", + OTP_GET_STATUS="OTP_GET_STATUS", + OTP_DEACTIVATE="OTP_DEACTIVATE", } From e146f3d3ee0a59b97dab88c73973e4e6e27780a9 Mon Sep 17 00:00:00 2001 From: envision-ci-agent Date: Wed, 11 Mar 2026 14:28:04 +0000 Subject: [PATCH 02/10] [skip ci] Add swagger.yaml --- swagger.yaml | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/swagger.yaml b/swagger.yaml index 340fe87939..f752dd1809 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -233,6 +233,82 @@ paths: summary: Returns user's Hedera account balance. tags: - accounts + /accounts/otp/generate: + post: + operationId: AccountApi_generateOtp + parameters: [] + responses: + '401': + description: Unauthorized. + '403': + description: Forbidden. + '500': + description: Internal server error. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerErrorDTO' + security: + - bearer: [] + tags: + - accounts + /accounts/otp/confirm: + post: + operationId: AccountApi_confirmOtp + parameters: [] + responses: + '401': + description: Unauthorized. + '403': + description: Forbidden. + '500': + description: Internal server error. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerErrorDTO' + security: + - bearer: [] + tags: + - accounts + /accounts/otp/status: + get: + operationId: AccountApi_getOtpStatus + parameters: [] + responses: + '401': + description: Unauthorized. + '403': + description: Forbidden. + '500': + description: Internal server error. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerErrorDTO' + security: + - bearer: [] + tags: + - accounts + /accounts/otp/deactivate: + post: + operationId: AccountApi_deactivateOtp + parameters: [] + responses: + '401': + description: Unauthorized. + '403': + description: Forbidden. + '500': + description: Internal server error. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalServerErrorDTO' + security: + - bearer: [] + tags: + - accounts /analytics/search/policies: post: description: >- @@ -18754,9 +18830,12 @@ components: type: string password: type: string + otp: + type: string required: - username - password + - otp ChangePasswordDTO: type: object properties: From f82384c7910cb8719ccb0f455691afba0f1b2c8c Mon Sep 17 00:00:00 2001 From: Celiant Date: Tue, 17 Mar 2026 14:28:57 +0400 Subject: [PATCH 03/10] fix bug and add docs for swagger --- api-gateway/src/api/service/account.ts | 75 +++++++++-- .../src/middlewares/validation/examples.ts | 7 + .../validation/schemas/accounts.ts | 126 +++++++++++++++++- .../src/app/views/login/login.component.ts | 21 +-- 4 files changed, 207 insertions(+), 22 deletions(-) diff --git a/api-gateway/src/api/service/account.ts b/api-gateway/src/api/service/account.ts index 0e4a799130..fa2790eaf5 100644 --- a/api-gateway/src/api/service/account.ts +++ b/api-gateway/src/api/service/account.ts @@ -3,7 +3,7 @@ import { Permissions, PolicyStatus, SchemaEntity, UserRole } from '@guardian/int import { ClientProxy } from '@nestjs/microservices'; import { Body, Controller, Get, Headers, HttpCode, HttpException, HttpStatus, Inject, Post, Req } from '@nestjs/common'; import { ApiBearerAuth, ApiBody, ApiExtraModels, ApiInternalServerErrorResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { AccountsResponseDTO, AccountsSessionResponseDTO, AggregatedDTOItem, BalanceResponseDTO, ChangePasswordDTO, InternalServerErrorDTO, LoginUserDTO, RegisterUserDTO } from '#middlewares'; +import { AccountsResponseDTO, AccountsSessionResponseDTO, AggregatedDTOItem, BalanceResponseDTO, ChangePasswordDTO, InternalServerErrorDTO, LoginUserDTO, RegisterUserDTO, GenerateOPTResponseDTO, EmptyResponseDTO, OTPConfirmDTO, OTPConfirmResponseDTO, OTPStatusResponseDTO } from '#middlewares'; import { Auth, AuthUser, checkPermission } from '#auth'; import { EntityOwner, Guardians, InternalException, PolicyEngine, UseCache, Users } from '#helpers'; import { PolicyListResponse } from '../../entities/policy'; @@ -386,14 +386,26 @@ export class AccountApi { } } + /** + * Generate a OTP secret for 2FA setup + */ @Post('otp/generate') @Auth() + @ApiOperation({ + summary: 'Generate a OTP secret for 2FA setup.', + description: 'Generate a OTP secret for 2FA setup.', + }) + @ApiOkResponse({ + description: 'Successful operation.', + type: GenerateOPTResponseDTO, + }) @ApiInternalServerErrorResponse({ description: 'Internal server error.', type: InternalServerErrorDTO, }) + @HttpCode(HttpStatus.CREATED) async generateOtp(@AuthUser() user: IAuthUser,) { - const users = new Users(); + const users = new Users(); try { const code = await users.otpGenerateSecret(user.id); @@ -404,18 +416,35 @@ export class AccountApi { throw new HttpException(error.message, error.code || HttpStatus.INTERNAL_SERVER_ERROR); } } - + + /** + * Confirm OTP setup + */ @Post('otp/confirm') @Auth() + @ApiOperation({ + summary: 'Confirm OTP setup.', + description: 'Confirm OTP setup by OTP token.', + }) + @ApiBody({ + description: 'Configuration.', + type: OTPConfirmDTO, + required: true + }) + @ApiOkResponse({ + description: 'Successful operation.', + type: OTPConfirmResponseDTO, + }) @ApiInternalServerErrorResponse({ description: 'Internal server error.', type: InternalServerErrorDTO, }) + @HttpCode(HttpStatus.CREATED) async confirmOtp( @AuthUser() user: IAuthUser, - @Body() body, + @Body() body: OTPConfirmDTO ) { - const users = new Users(); + const users = new Users(); try { const token = body.token; const result = await users.otpConfirmSecret(user.id, token); @@ -428,17 +457,29 @@ export class AccountApi { } } + /** + * Get OTP status + */ @Get('otp/status') @Auth() + @ApiOperation({ + summary: 'Get OTP status.', + description: 'Get OTP status for current user.', + }) + @ApiOkResponse({ + description: 'Successful operation.', + type: OTPStatusResponseDTO + }) @ApiInternalServerErrorResponse({ description: 'Internal server error.', type: InternalServerErrorDTO, }) + @HttpCode(HttpStatus.OK) async getOtpStatus( @AuthUser() user: IAuthUser, - ){ - const users = new Users(); - try { + ) { + const users = new Users(); + try { const result = await users.otpGetStatus(user.id); return result; @@ -449,17 +490,29 @@ export class AccountApi { } } + /** + * Deactivate 2FA + */ @Post('otp/deactivate') @Auth() + @ApiOperation({ + summary: 'Deactivate 2FA.', + description: 'Deactivate 2FA.', + }) + @ApiOkResponse({ + description: 'Successful operation.', + type: EmptyResponseDTO, + }) @ApiInternalServerErrorResponse({ description: 'Internal server error.', type: InternalServerErrorDTO, }) + @HttpCode(HttpStatus.CREATED) async deactivateOtp( @AuthUser() user: IAuthUser, - ){ - const users = new Users(); - try { + ) { + const users = new Users(); + try { const result = await users.otpDeactivate(user.id); return result; diff --git a/api-gateway/src/middlewares/validation/examples.ts b/api-gateway/src/middlewares/validation/examples.ts index 189cae2832..963ee47d78 100644 --- a/api-gateway/src/middlewares/validation/examples.ts +++ b/api-gateway/src/middlewares/validation/examples.ts @@ -8,4 +8,11 @@ export enum Examples { COLOR = '#000000', DID = '#did:hedera:testnet:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0.0.0000001', HASH = 'GcDE9NsPJc7oCZvSVJySCZHxTxvjc3ZAMgtKozP1r1Eh', + OTPName = 'OS Guardian', + USER_NAME_SR_1 = 'StandardRegistry', + OTPSecret = 'AAA0AA0A0A00A000', + OTPAuthURL = 'otpauth://totp/OS%20Guardian:StandardRegistry?issuer=OS+Guardian&period=30&secret=XXX0XX0X0X00X000', + OTPAlgo = 'sha1', + Number = 111, + OTPCode = '111111', } \ No newline at end of file diff --git a/api-gateway/src/middlewares/validation/schemas/accounts.ts b/api-gateway/src/middlewares/validation/schemas/accounts.ts index c10e0626f8..33ceb37a3d 100644 --- a/api-gateway/src/middlewares/validation/schemas/accounts.ts +++ b/api-gateway/src/middlewares/validation/schemas/accounts.ts @@ -1,10 +1,11 @@ import * as yup from 'yup'; import fieldsValidation from '../fields-validation.js' -import { IsIn, IsNotEmpty, IsString, IsOptional } from 'class-validator'; +import { IsIn, IsNotEmpty, IsString, IsOptional, IsNumber, IsBoolean, IsArray } from 'class-validator'; import { UserRole } from '@guardian/interfaces'; -import { Expose } from 'class-transformer'; +import { Expose, Type } from 'class-transformer'; import { ApiProperty } from '@nestjs/swagger'; import { Match } from '../../../helpers/decorators/match.validator.js'; +import { Examples } from '../../../middlewares/validation/examples.js'; export class AccountsResponseDTO { @ApiProperty() @@ -70,7 +71,7 @@ export class LoginUserDTO { @ApiProperty() @IsString() - @IsOptional() + @IsOptional() otp: string; } @@ -143,6 +144,125 @@ export class BalanceResponseDTO { user: UserAccountDTO; } +export class OTPConfigDTO { + @ApiProperty({ + type: String, + required: true, + example: Examples.OTPAlgo + }) + @IsString() + algo: string; + + @ApiProperty({ + type: Number, + required: true, + example: Examples.Number + }) + @IsNumber() + digits: number; + + @ApiProperty({ + type: Number, + required: true, + example: Examples.Number + }) + @IsNumber() + period: number; + + @ApiProperty({ + type: Number, + required: true, + example: Examples.Number + }) + @IsNumber() + secretSize: number; +} + +export class GenerateOPTResponseDTO { + @ApiProperty({ + type: String, + required: true, + example: Examples.OTPName + }) + @IsString() + issuer: string; + + @ApiProperty({ + type: String, + required: true, + example: Examples.USER_NAME_SR_1 + }) + @IsString() + user: string; + + @ApiProperty({ + type: String, + required: true, + example: Examples.OTPSecret + }) + @IsString() + secret: string; + + @ApiProperty({ + type: String, + required: true, + example: Examples.OTPAuthURL + }) + @IsString() + url: string; + + @ApiProperty({ + type: OTPConfigDTO, + required: true, + }) + @Type(() => OTPConfigDTO) + config: OTPConfigDTO; +} + +export class OTPConfirmDTO { + @ApiProperty({ + type: String, + required: true, + example: Examples.OTPCode + }) + @IsString() + token: string; +} + +export class OTPConfirmResponseDTO { + @ApiProperty({ + type: Boolean, + required: true, + example: true + }) + @IsBoolean() + success: boolean; + + @ApiProperty({ + type: String, + required: true, + isArray: true, + example: ['000000', '111111', '222222', '333333', '444444', '555555', '666666', '777777', '888888', '999999'] + }) + @IsArray() + @IsString({ each: true }) + backupCodes: string[]; +} + +export class OTPStatusResponseDTO { + @ApiProperty({ + type: Boolean, + required: true, + example: true + }) + @IsBoolean() + enabled: boolean; +} + +export class EmptyResponseDTO { +} + + export const registerSchema = () => { const { username, password, password_confirmation, role } = fieldsValidation return yup.object({ diff --git a/frontend/src/app/views/login/login.component.ts b/frontend/src/app/views/login/login.component.ts index 7ef82bfe1d..fa47ae36d7 100644 --- a/frontend/src/app/views/login/login.component.ts +++ b/frontend/src/app/views/login/login.component.ts @@ -151,14 +151,6 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewChecked { this.wrongNameOrPassword = false; this.auth.login(login, password, otp) .subscribe((result) => { - this.auth.setRefreshToken(result.refreshToken); - this.auth.setUsername(login); - this.auth.updateAccessToken().subscribe(_result => { - this.authState.updateState(true); - const home = this.auth.home(result.role); - this.router.navigate([home]); - }); - if (result.otprequired) { this.dialogService.open(OtpDialogComponent, { header: 'Enter Verification Code', @@ -168,6 +160,18 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewChecked { if (token) { this.login(login, password, token); } + else { + this.loading = false; + } + }); + } + else { + this.auth.setRefreshToken(result.refreshToken); + this.auth.setUsername(login); + this.auth.updateAccessToken().subscribe(_result => { + this.authState.updateState(true); + const home = this.auth.home(result.role); + this.router.navigate([home]); }); } @@ -177,6 +181,7 @@ export class LoginComponent implements OnInit, OnDestroy, AfterViewChecked { 'Weak Password', ); } + }, (error) => { this.loading = false; this.errorMessage = error.message; From 370bb20d700e833483f4fc5adc4040b71ad111b4 Mon Sep 17 00:00:00 2001 From: envision-ci-agent Date: Tue, 17 Mar 2026 10:32:12 +0000 Subject: [PATCH 04/10] [skip ci] Add swagger.yaml --- swagger.yaml | 126 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) diff --git a/swagger.yaml b/swagger.yaml index f752dd1809..9e0f5cbbaf 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -235,9 +235,16 @@ paths: - accounts /accounts/otp/generate: post: + description: Generate a OTP secret for 2FA setup. operationId: AccountApi_generateOtp parameters: [] responses: + '200': + description: Successful operation. + content: + application/json: + schema: + $ref: '#/components/schemas/GenerateOPTResponseDTO' '401': description: Unauthorized. '403': @@ -250,13 +257,28 @@ paths: $ref: '#/components/schemas/InternalServerErrorDTO' security: - bearer: [] + summary: Generate a OTP secret for 2FA setup. tags: - accounts /accounts/otp/confirm: post: + description: Confirm OTP setup by OTP token. operationId: AccountApi_confirmOtp parameters: [] + requestBody: + required: true + description: Configuration. + content: + application/json: + schema: + $ref: '#/components/schemas/OTPConfirmDTO' responses: + '200': + description: Successful operation. + content: + application/json: + schema: + $ref: '#/components/schemas/OTPConfirmResponseDTO' '401': description: Unauthorized. '403': @@ -269,13 +291,21 @@ paths: $ref: '#/components/schemas/InternalServerErrorDTO' security: - bearer: [] + summary: Confirm OTP setup. tags: - accounts /accounts/otp/status: get: + description: Get OTP status for current user. operationId: AccountApi_getOtpStatus parameters: [] responses: + '200': + description: Successful operation. + content: + application/json: + schema: + $ref: '#/components/schemas/OTPStatusResponseDTO' '401': description: Unauthorized. '403': @@ -288,13 +318,21 @@ paths: $ref: '#/components/schemas/InternalServerErrorDTO' security: - bearer: [] + summary: Get OTP status. tags: - accounts /accounts/otp/deactivate: post: + description: Deactivate 2FA. operationId: AccountApi_deactivateOtp parameters: [] responses: + '200': + description: Successful operation. + content: + application/json: + schema: + $ref: '#/components/schemas/EmptyResponseDTO' '401': description: Unauthorized. '403': @@ -307,6 +345,7 @@ paths: $ref: '#/components/schemas/InternalServerErrorDTO' security: - bearer: [] + summary: Deactivate 2FA. tags: - accounts /analytics/search/policies: @@ -19306,6 +19345,93 @@ components: - balance - unit - user + OTPConfigDTO: + type: object + properties: + algo: + type: string + example: sha1 + digits: + type: number + example: 111 + period: + type: number + example: 111 + secretSize: + type: number + example: 111 + required: + - algo + - digits + - period + - secretSize + GenerateOPTResponseDTO: + type: object + properties: + issuer: + type: string + example: OS Guardian + user: + type: string + example: StandardRegistry + secret: + type: string + example: AAA0AA0A0A00A000 + url: + type: string + example: >- + otpauth://totp/OS%20Guardian:StandardRegistry?issuer=OS+Guardian&period=30&secret=XXX0XX0X0X00X000 + config: + $ref: '#/components/schemas/OTPConfigDTO' + required: + - issuer + - user + - secret + - url + - config + OTPConfirmDTO: + type: object + properties: + token: + type: string + example: '111111' + required: + - token + OTPConfirmResponseDTO: + type: object + properties: + success: + type: boolean + example: true + backupCodes: + example: + - '000000' + - '111111' + - '222222' + - '333333' + - '444444' + - '555555' + - '666666' + - '777777' + - '888888' + - '999999' + type: array + items: + type: string + required: + - success + - backupCodes + OTPStatusResponseDTO: + type: object + properties: + enabled: + type: boolean + example: true + required: + - enabled + EmptyResponseDTO: + type: object + properties: {} FilterSearchPoliciesDTO: type: object properties: From 363de14f2b43e5257fdb9f1b29d3b02768bc134a Mon Sep 17 00:00:00 2001 From: Celiant Date: Tue, 17 Mar 2026 14:49:46 +0400 Subject: [PATCH 05/10] fix linter issues --- interfaces/src/type/messages/auth-events.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interfaces/src/type/messages/auth-events.ts b/interfaces/src/type/messages/auth-events.ts index 552375fa3a..796c13ae0e 100644 --- a/interfaces/src/type/messages/auth-events.ts +++ b/interfaces/src/type/messages/auth-events.ts @@ -51,8 +51,8 @@ export enum AuthEvents { CREATE_RELAYER_ACCOUNT = 'CREATE_RELAYER_ACCOUNT', GENERATE_RELAYER_ACCOUNT = 'GENERATE_RELAYER_ACCOUNT', RELAYER_ACCOUNT_EXIST = 'RELAYER_ACCOUNT_EXIST', - OTP_GENERATE_SECRET="OTP_GENERATE_SECRET", - OTP_CONFIRM_SECRET="OTP_CONFIRM_SECRET", - OTP_GET_STATUS="OTP_GET_STATUS", - OTP_DEACTIVATE="OTP_DEACTIVATE", + OTP_GENERATE_SECRET = 'OTP_GENERATE_SECRET', + OTP_CONFIRM_SECRET = 'OTP_CONFIRM_SECRET', + OTP_GET_STATUS = 'OTP_GET_STATUS', + OTP_DEACTIVATE = 'OTP_DEACTIVATE', } From aa3841638223d5b76a32ed7fbea1c4be95f170d5 Mon Sep 17 00:00:00 2001 From: Celiant Date: Tue, 17 Mar 2026 16:13:39 +0400 Subject: [PATCH 06/10] fix linter issues and update swagger docs --- api-gateway/src/api/service/account.ts | 22 +++++- .../src/middlewares/validation/examples.ts | 30 ++++++-- .../validation/schemas/accounts.ts | 77 ++++++++++++++++--- auth-service/src/api/account-service.ts | 16 ++-- auth-service/src/entity/otp-secret.ts | 4 +- auth-service/src/helpers/otp-helper.ts | 69 ++++++++--------- 6 files changed, 156 insertions(+), 62 deletions(-) diff --git a/api-gateway/src/api/service/account.ts b/api-gateway/src/api/service/account.ts index fa2790eaf5..2d022106fb 100644 --- a/api-gateway/src/api/service/account.ts +++ b/api-gateway/src/api/service/account.ts @@ -2,8 +2,8 @@ import { IAuthUser, NotificationHelper, PinoLogger } from '@guardian/common'; import { Permissions, PolicyStatus, SchemaEntity, UserRole } from '@guardian/interfaces'; import { ClientProxy } from '@nestjs/microservices'; import { Body, Controller, Get, Headers, HttpCode, HttpException, HttpStatus, Inject, Post, Req } from '@nestjs/common'; -import { ApiBearerAuth, ApiBody, ApiExtraModels, ApiInternalServerErrorResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; -import { AccountsResponseDTO, AccountsSessionResponseDTO, AggregatedDTOItem, BalanceResponseDTO, ChangePasswordDTO, InternalServerErrorDTO, LoginUserDTO, RegisterUserDTO, GenerateOPTResponseDTO, EmptyResponseDTO, OTPConfirmDTO, OTPConfirmResponseDTO, OTPStatusResponseDTO } from '#middlewares'; +import { ApiBearerAuth, ApiBody, ApiExtraModels, ApiInternalServerErrorResponse, ApiOkResponse, ApiOperation, ApiTags, getSchemaPath } from '@nestjs/swagger'; +import { AccountsResponseDTO, AccountsSessionResponseDTO, AggregatedDTOItem, BalanceResponseDTO, ChangePasswordDTO, InternalServerErrorDTO, LoginUserDTO, RegisterUserDTO, GenerateOPTResponseDTO, EmptyResponseDTO, LoginSuccessResponseDTO, LoginOTPRequiredResponseDTO, OTPConfirmDTO, OTPConfirmResponseDTO, ObjectExamples, OTPStatusResponseDTO } from '#middlewares'; import { Auth, AuthUser, checkPermission } from '#auth'; import { EntityOwner, Guardians, InternalException, PolicyEngine, UseCache, Users } from '#helpers'; import { PolicyListResponse } from '../../entities/policy'; @@ -126,8 +126,22 @@ export class AccountApi { summary: 'Logs user into the system.', }) @ApiOkResponse({ - description: 'Successful operation.', - type: AccountsSessionResponseDTO + schema: { + oneOf: [ + { $ref: getSchemaPath(LoginSuccessResponseDTO) }, + { $ref: getSchemaPath(LoginOTPRequiredResponseDTO) } + ] + }, + examples: { + success: { + summary: 'Successful response', + value: ObjectExamples.LOGIN_SUCCESSFUL + }, + otpRequired: { + summary: 'OTP required', + value: ObjectExamples.OTP_REQUIRED_RESPONSE + } + } }) @ApiInternalServerErrorResponse({ description: 'Internal server error.', diff --git a/api-gateway/src/middlewares/validation/examples.ts b/api-gateway/src/middlewares/validation/examples.ts index 963ee47d78..1324cc4ac7 100644 --- a/api-gateway/src/middlewares/validation/examples.ts +++ b/api-gateway/src/middlewares/validation/examples.ts @@ -8,11 +8,29 @@ export enum Examples { COLOR = '#000000', DID = '#did:hedera:testnet:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0.0.0000001', HASH = 'GcDE9NsPJc7oCZvSVJySCZHxTxvjc3ZAMgtKozP1r1Eh', - OTPName = 'OS Guardian', + OTP_NAME = 'OS Guardian', USER_NAME_SR_1 = 'StandardRegistry', - OTPSecret = 'AAA0AA0A0A00A000', - OTPAuthURL = 'otpauth://totp/OS%20Guardian:StandardRegistry?issuer=OS+Guardian&period=30&secret=XXX0XX0X0X00X000', - OTPAlgo = 'sha1', - Number = 111, - OTPCode = '111111', + OTP_SECRET = 'AAA0AA0A0A00A000', + OTP_AUTH_URL = 'otpauth://totp/OS%20Guardian:StandardRegistry?issuer=OS+Guardian&period=30&secret=XXX0XX0X0X00X000', + OTP_ALGO = 'sha1', + NUMBER = 111, + OTP_CODE = '111111', + TOKEN = 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImIxM2E0NTkyLWU2YjQtNDg0OS1hMTkxLTAxOWVkODNkYzM5ZCIsIm5hbWUiOiJTdGFuZGFyZFJlZ2lzdHJ5IiwiZXhwaXJlQXQiOjE4MDUyODI4NjU3MDEsImlhdCI6MTc3Mzc0Njg2NX0.BsKje1bza0NEKKTAHFMRfwa3H-H-eRu7-KEDHKTftljXE3eQNmYCf_ftaPpw3DdsfsavBcEDfs5UQwlyeMsaTJPehEx_gl697rGQx6b8objGkqfFL2A7nWetMbWtxFFsIrbxs4mqHy1LM_4VVJuiXsH2DYQZkxOmw4HdyUshjE84', + ROLE_SR = 'STANDARD_REGISTRY', + ROLE_USER = 'USER' +} + +export const ObjectExamples = { + LOGIN_SUCCESSFUL: { + did: Examples.DID, + refreshToken: Examples.TOKEN, + role: Examples.ROLE_SR, + username: Examples.USER_NAME_SR_1, + weakPassword: false + }, + + OTP_REQUIRED_RESPONSE: { + success: false, + otprequired: true + } } \ No newline at end of file diff --git a/api-gateway/src/middlewares/validation/schemas/accounts.ts b/api-gateway/src/middlewares/validation/schemas/accounts.ts index 33ceb37a3d..c79359d83f 100644 --- a/api-gateway/src/middlewares/validation/schemas/accounts.ts +++ b/api-gateway/src/middlewares/validation/schemas/accounts.ts @@ -41,6 +41,66 @@ export class AccountsSessionResponseDTO { accessToken: string } +export class LoginSuccessResponseDTO { + @ApiProperty({ + type: String, + required: true, + example: Examples.DID + }) + @IsString() + did: string; + + @ApiProperty({ + type: String, + required: true, + example: Examples.TOKEN + }) + @IsString() + refreshToken: string; + + @ApiProperty({ + type: String, + required: true, + example: Examples.ROLE_SR + }) + @IsString() + role: string + + @ApiProperty({ + type: String, + required: true, + example: Examples.USER_NAME_SR_1 + }) + @IsString() + username: string + + @ApiProperty({ + type: String, + required: true, + example: false + }) + @IsString() + weakPassword: string +} + +export class LoginOTPRequiredResponseDTO { + @ApiProperty({ + type: Boolean, + required: true, + example: false + }) + @IsBoolean() + success: boolean; + + @ApiProperty({ + type: Boolean, + required: true, + example: true + }) + @IsBoolean() + otprequired: boolean; +} + export class ChangePasswordDTO { @ApiProperty() @IsString() @@ -148,7 +208,7 @@ export class OTPConfigDTO { @ApiProperty({ type: String, required: true, - example: Examples.OTPAlgo + example: Examples.OTP_ALGO }) @IsString() algo: string; @@ -156,7 +216,7 @@ export class OTPConfigDTO { @ApiProperty({ type: Number, required: true, - example: Examples.Number + example: Examples.NUMBER }) @IsNumber() digits: number; @@ -164,7 +224,7 @@ export class OTPConfigDTO { @ApiProperty({ type: Number, required: true, - example: Examples.Number + example: Examples.NUMBER }) @IsNumber() period: number; @@ -172,7 +232,7 @@ export class OTPConfigDTO { @ApiProperty({ type: Number, required: true, - example: Examples.Number + example: Examples.NUMBER }) @IsNumber() secretSize: number; @@ -182,7 +242,7 @@ export class GenerateOPTResponseDTO { @ApiProperty({ type: String, required: true, - example: Examples.OTPName + example: Examples.OTP_NAME }) @IsString() issuer: string; @@ -198,7 +258,7 @@ export class GenerateOPTResponseDTO { @ApiProperty({ type: String, required: true, - example: Examples.OTPSecret + example: Examples.OTP_SECRET }) @IsString() secret: string; @@ -206,7 +266,7 @@ export class GenerateOPTResponseDTO { @ApiProperty({ type: String, required: true, - example: Examples.OTPAuthURL + example: Examples.OTP_AUTH_URL }) @IsString() url: string; @@ -223,7 +283,7 @@ export class OTPConfirmDTO { @ApiProperty({ type: String, required: true, - example: Examples.OTPCode + example: Examples.OTP_CODE }) @IsString() token: string; @@ -262,7 +322,6 @@ export class OTPStatusResponseDTO { export class EmptyResponseDTO { } - export const registerSchema = () => { const { username, password, password_confirmation, role } = fieldsValidation return yup.object({ diff --git a/auth-service/src/api/account-service.ts b/auth-service/src/api/account-service.ts index 7be6bd528e..234641c3bf 100644 --- a/auth-service/src/api/account-service.ts +++ b/auth-service/src/api/account-service.ts @@ -551,8 +551,9 @@ export class AccountService extends NatsService { const { userId } = msg; const user = await new DataBaseHelper(User).findOne({ id: userId }); - if (!user) + if (!user) { return new MessageError('Invalid user'); + } const key = await OtpHelper.generateNewSecretFor(user); @@ -569,29 +570,31 @@ export class AccountService extends NatsService { const { userId, token } = msg; const user = await new DataBaseHelper(User).findOne({ id: userId }); - if (!user) + if (!user) { return new MessageError('Invalid user'); + } const result = await OtpHelper.confirmNewSecret(user, token); if (result) { const codes = await OtpHelper.generateBackupCodes(user); return new MessageResponse({ success: true, backupCodes: codes }); } - else + else { return new MessageResponse({ success: false }); + } } catch (error) { await logger.error(error, ['AUTH_SERVICE', 'OTP_GENERATE_SECRET']); return new MessageError(error); } }); - this.getMessages(AuthEvents.OTP_GET_STATUS, async (msg) => { try { const { userId } = msg; const user = await new DataBaseHelper(User).findOne({ id: userId }); - if (!user) + if (!user) { return new MessageError('Invalid user'); + } const result = await OtpHelper.isConfiguredFor(user); return new MessageResponse({ enabled: result }); @@ -606,8 +609,9 @@ export class AccountService extends NatsService { const { userId } = msg; const user = await new DataBaseHelper(User).findOne({ id: userId }); - if (!user) + if (!user) { return new MessageError('Invalid user'); + } const result = await OtpHelper.deactivate(user); return new MessageResponse({ enabled: result }); diff --git a/auth-service/src/entity/otp-secret.ts b/auth-service/src/entity/otp-secret.ts index d1a41810ce..4d6120c812 100644 --- a/auth-service/src/entity/otp-secret.ts +++ b/auth-service/src/entity/otp-secret.ts @@ -7,8 +7,8 @@ import { BaseEntity } from '@guardian/common'; @Entity() export class OtpSecret extends BaseEntity { /** - * User Id - */ + * User Id + */ @Property({ nullable: true }) userId: string; diff --git a/auth-service/src/helpers/otp-helper.ts b/auth-service/src/helpers/otp-helper.ts index d074cacd52..72aab41c1b 100644 --- a/auth-service/src/helpers/otp-helper.ts +++ b/auth-service/src/helpers/otp-helper.ts @@ -1,7 +1,7 @@ -import { Totp, generateBackupCodes } from "time2fa"; -import { OtpSecret } from "../entity/otp-secret.js"; -import { DataBaseHelper } from "@guardian/common"; -import { User } from "../entity/user.js"; +import { Totp, generateBackupCodes } from 'time2fa'; +import { OtpSecret } from '../entity/otp-secret.js'; +import { DataBaseHelper } from '@guardian/common'; +import { User } from '../entity/user.js'; export class OtpHelper { @@ -10,11 +10,11 @@ export class OtpHelper { } private static async deleteOtp(user: User): Promise { - await new DataBaseHelper(OtpSecret).delete({ ...this.getFilter(user), enabled: true }); + await new DataBaseHelper(OtpSecret).delete({ ...OtpHelper.getFilter(user), enabled: true }); } private static async getOtp(user: User): Promise { - return await new DataBaseHelper(OtpSecret).findOne({ ...this.getFilter(user), enabled: true }); + return await new DataBaseHelper(OtpSecret).findOne({ ...OtpHelper.getFilter(user), enabled: true }); } private static getAccountName(user: User): string { @@ -23,11 +23,11 @@ export class OtpHelper { public static async generateNewSecretFor(user: User) { - //Delete prev temp secret if exists - await new DataBaseHelper(OtpSecret).delete({ ...this.getFilter(user), enabled: false }); + //Delete prev temp secret if exists + await new DataBaseHelper(OtpSecret).delete({ ...OtpHelper.getFilter(user), enabled: false }); //Generate secret - const key = Totp.generateKey({ issuer: "OS Guardian", user: this.getAccountName(user) }); + const key = Totp.generateKey({ issuer: 'OS Guardian', user: OtpHelper.getAccountName(user) }); const entity = await new DataBaseHelper(OtpSecret).create({ userId: user.id, secret: key.secret, @@ -41,15 +41,15 @@ export class OtpHelper { } public static async confirmNewSecret(user: User, token: string): Promise { - const temp = await new DataBaseHelper(OtpSecret).findOne({ ...this.getFilter(user), enabled: false }); - if (!temp) + const temp = await new DataBaseHelper(OtpSecret).findOne({ ...OtpHelper.getFilter(user), enabled: false }); + if (!temp) { return false; + } try { const valid = Totp.validate({ passcode: token, secret: temp.secret }); - if (!valid) - return false; + if (!valid) { return false; } //Delete prev secret if exists - await this.deleteOtp(user); + await OtpHelper.deleteOtp(user); temp.enabled = true; await new DataBaseHelper(OtpSecret).save(temp); @@ -60,32 +60,30 @@ export class OtpHelper { } public static async generateBackupCodes(user: User): Promise { - const otp = await this.getOtp(user); - if (!otp) - return undefined; - if (otp.backupCodes && otp.backupCodes.length > 0) - throw new Error('Backup codes already cenerated'); + const otp = await OtpHelper.getOtp(user); + if (!otp) { return undefined; } + if (otp.backupCodes && otp.backupCodes.length > 0) { throw new Error('Backup codes already cenerated'); } otp.backupCodes = generateBackupCodes(); await new DataBaseHelper(OtpSecret).save(otp); return otp.backupCodes; } public static async isConfiguredFor(user: User): Promise { - const key = await this.getOtp(user); - if (key) - return true - else - return false + const key = await OtpHelper.getOtp(user); + if (key) { return true } + else { return false } } public static async isValidToken(user: User, token: string): Promise { - - const key = await this.getOtp(user); - if (!key) + + const key = await OtpHelper.getOtp(user); + if (!key) { return false;// not configured + } - if(!token) + if (!token) { return false;// configured but not provided + } const result = Totp.validate({ passcode: token, secret: key.secret }) return result; @@ -94,18 +92,19 @@ export class OtpHelper { /** * Check user otp if required - * @returns + * @returns */ public static async checkOtp(user: User, token: string): Promise { - const configured = await this.isConfiguredFor(user); - if (!configured) + const configured = await OtpHelper.isConfiguredFor(user); + if (!configured) { return true; //Not required - ignore - if (!token) + } + if (!token) { return false; //Required and empty otp - reject - + } //Validate otp try { - const isValid = this.isValidToken(user, token); + const isValid = OtpHelper.isValidToken(user, token); return isValid; } catch (e) { @@ -114,6 +113,6 @@ export class OtpHelper { } public static async deactivate(user: User): Promise { - await this.deleteOtp(user); + await OtpHelper.deleteOtp(user); } } \ No newline at end of file From b241734d2b3fbeb9a1f60bf768669f56184fbfd8 Mon Sep 17 00:00:00 2001 From: envision-ci-agent Date: Tue, 17 Mar 2026 12:17:16 +0000 Subject: [PATCH 07/10] [skip ci] Add swagger.yaml --- swagger.yaml | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/swagger.yaml b/swagger.yaml index 939831350f..147b03eeda 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -62,11 +62,29 @@ paths: $ref: '#/components/schemas/LoginUserDTO' responses: '200': - description: Successful operation. + description: '' content: application/json: schema: - $ref: '#/components/schemas/AccountsSessionResponseDTO' + oneOf: + - $ref: '#/components/schemas/LoginSuccessResponseDTO' + - $ref: '#/components/schemas/LoginOTPRequiredResponseDTO' + examples: + success: + summary: Successful response + value: + did: >- + #did:hedera:testnet:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0.0.0000001 + refreshToken: >- + eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImIxM2E0NTkyLWU2YjQtNDg0OS1hMTkxLTAxOWVkODNkYzM5ZCIsIm5hbWUiOiJTdGFuZGFyZFJlZ2lzdHJ5IiwiZXhwaXJlQXQiOjE4MDUyODI4NjU3MDEsImlhdCI6MTc3Mzc0Njg2NX0.BsKje1bza0NEKKTAHFMRfwa3H-H-eRu7-KEDHKTftljXE3eQNmYCf_ftaPpw3DdsfsavBcEDfs5UQwlyeMsaTJPehEx_gl697rGQx6b8objGkqfFL2A7nWetMbWtxFFsIrbxs4mqHy1LM_4VVJuiXsH2DYQZkxOmw4HdyUshjE84 + role: STANDARD_REGISTRY + username: StandardRegistry + weakPassword: false + otpRequired: + summary: OTP required + value: + success: false + otprequired: true '500': description: Internal server error. content: @@ -23813,3 +23831,41 @@ components: - '@context' - id - type + LoginOTPRequiredResponseDTO: + type: object + properties: + success: + type: boolean + example: false + otprequired: + type: boolean + example: true + required: + - success + - otprequired + LoginSuccessResponseDTO: + type: object + properties: + did: + type: string + example: >- + #did:hedera:testnet:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0.0.0000001 + refreshToken: + type: string + example: >- + eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImIxM2E0NTkyLWU2YjQtNDg0OS1hMTkxLTAxOWVkODNkYzM5ZCIsIm5hbWUiOiJTdGFuZGFyZFJlZ2lzdHJ5IiwiZXhwaXJlQXQiOjE4MDUyODI4NjU3MDEsImlhdCI6MTc3Mzc0Njg2NX0.BsKje1bza0NEKKTAHFMRfwa3H-H-eRu7-KEDHKTftljXE3eQNmYCf_ftaPpw3DdsfsavBcEDfs5UQwlyeMsaTJPehEx_gl697rGQx6b8objGkqfFL2A7nWetMbWtxFFsIrbxs4mqHy1LM_4VVJuiXsH2DYQZkxOmw4HdyUshjE84 + role: + type: string + example: STANDARD_REGISTRY + username: + type: string + example: StandardRegistry + weakPassword: + type: string + example: false + required: + - did + - refreshToken + - role + - username + - weakPassword From bfb3f42bc9453f8f19abbb2f15e1b6c47029417f Mon Sep 17 00:00:00 2001 From: Celiant Date: Fri, 27 Mar 2026 20:22:53 +0400 Subject: [PATCH 08/10] fix grammar --- api-gateway/src/api/service/account.ts | 6 +++--- .../otp-config-dialog/otp-config-dialog.component.html | 2 +- .../src/app/views/root-profile/root-profile.component.html | 2 +- .../src/app/views/user-profile/user-profile.component.html | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api-gateway/src/api/service/account.ts b/api-gateway/src/api/service/account.ts index 2d022106fb..8db1b549a5 100644 --- a/api-gateway/src/api/service/account.ts +++ b/api-gateway/src/api/service/account.ts @@ -406,8 +406,8 @@ export class AccountApi { @Post('otp/generate') @Auth() @ApiOperation({ - summary: 'Generate a OTP secret for 2FA setup.', - description: 'Generate a OTP secret for 2FA setup.', + summary: 'Generate an OTP secret for 2FA setup.', + description: 'Generate an OTP secret for 2FA setup.', }) @ApiOkResponse({ description: 'Successful operation.', @@ -478,7 +478,7 @@ export class AccountApi { @Auth() @ApiOperation({ summary: 'Get OTP status.', - description: 'Get OTP status for current user.', + description: 'Get OTP status for the current user.', }) @ApiOkResponse({ description: 'Successful operation.', diff --git a/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.html b/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.html index 0717c1f6c0..4ca609f0f3 100644 --- a/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.html +++ b/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.html @@ -23,7 +23,7 @@ -
Wrong or expired code! Please, enter code again to confirm setup +
Wrong or expired code! Please enter the code again to confirm setup.
diff --git a/frontend/src/app/views/root-profile/root-profile.component.html b/frontend/src/app/views/root-profile/root-profile.component.html index fe8b7db349..0601a22a70 100644 --- a/frontend/src/app/views/root-profile/root-profile.component.html +++ b/frontend/src/app/views/root-profile/root-profile.component.html @@ -65,7 +65,7 @@

Profile

- When you sign in you'll be asked to use the security code provided + When you sign in, you'll be asked to use the security code provided by your Authenticator App.
diff --git a/frontend/src/app/views/user-profile/user-profile.component.html b/frontend/src/app/views/user-profile/user-profile.component.html index a27dbac685..7e6185552c 100644 --- a/frontend/src/app/views/user-profile/user-profile.component.html +++ b/frontend/src/app/views/user-profile/user-profile.component.html @@ -61,7 +61,7 @@

Profile

- When you sign in you'll be asked to use the security code provided + When you sign in, you'll be asked to use the security code provided by your Authenticator App.
From 772c02e552fc2c289ca53b6f6ff381c33c3b63c7 Mon Sep 17 00:00:00 2001 From: envision-ci-agent Date: Fri, 27 Mar 2026 16:25:51 +0000 Subject: [PATCH 09/10] [skip ci] Add swagger.yaml --- swagger.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/swagger.yaml b/swagger.yaml index 8d32264523..74d24b476d 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -253,7 +253,7 @@ paths: - accounts /accounts/otp/generate: post: - description: Generate a OTP secret for 2FA setup. + description: Generate an OTP secret for 2FA setup. operationId: AccountApi_generateOtp parameters: [] responses: @@ -275,7 +275,7 @@ paths: $ref: '#/components/schemas/InternalServerErrorDTO' security: - bearer: [] - summary: Generate a OTP secret for 2FA setup. + summary: Generate an OTP secret for 2FA setup. tags: - accounts /accounts/otp/confirm: @@ -314,7 +314,7 @@ paths: - accounts /accounts/otp/status: get: - description: Get OTP status for current user. + description: Get OTP status for the current user. operationId: AccountApi_getOtpStatus parameters: [] responses: From d94dcf63b83e12877ea641fc7235fb9eab85603c Mon Sep 17 00:00:00 2001 From: Celiant Date: Fri, 27 Mar 2026 23:13:21 +0400 Subject: [PATCH 10/10] delete commented lines and fix grammar --- api-gateway/src/api/service/account.ts | 2 +- .../login/otp-codes-dialog/otp-codes-dialog.component.scss | 4 ---- .../otp-config-dialog/otp-config-dialog.component.scss | 7 ------- .../app/views/login/otp-dialog/otp-dialog.component.scss | 3 --- 4 files changed, 1 insertion(+), 15 deletions(-) diff --git a/api-gateway/src/api/service/account.ts b/api-gateway/src/api/service/account.ts index 8db1b549a5..3272813a96 100644 --- a/api-gateway/src/api/service/account.ts +++ b/api-gateway/src/api/service/account.ts @@ -401,7 +401,7 @@ export class AccountApi { } /** - * Generate a OTP secret for 2FA setup + * Generate an OTP secret for 2FA setup */ @Post('otp/generate') @Auth() diff --git a/frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.scss b/frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.scss index 616c93c864..3d0c196f85 100644 --- a/frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.scss +++ b/frontend/src/app/views/login/otp-codes-dialog/otp-codes-dialog.component.scss @@ -8,8 +8,6 @@ form { grid-template-columns: 1fr 1fr; gap: 12px; margin: 15px; - /* margin-left: auto; */ - /* margin-right: auto; */ font-size: 24px; & > div { text-align: center; @@ -82,7 +80,6 @@ button .pi { font-style: normal; font-weight: 500; line-height: 30px; - /* 214.286% */ cursor: pointer; } } @@ -94,7 +91,6 @@ button .pi { font-style: normal; font-weight: 500; line-height: 30px; - /* 214.286% */ cursor: pointer; } diff --git a/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.scss b/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.scss index ad2cfc058c..6c52d6943f 100644 --- a/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.scss +++ b/frontend/src/app/views/login/otp-config-dialog/otp-config-dialog.component.scss @@ -42,8 +42,6 @@ form { bottom: -8px; left: 0px; right: 0px; - // width: 100%; - // height: 100%; opacity: 0.1; border-radius: 8px; background-color: var(--color-primary); @@ -111,11 +109,6 @@ form { } } -// .dialog-footer { -// border-top: 1px solid var(--guardian-border-color); -// padding-top: 20px; -// } - ::ng-deep { .p-dialog-title { font-family: Poppins, sans-serif; diff --git a/frontend/src/app/views/login/otp-dialog/otp-dialog.component.scss b/frontend/src/app/views/login/otp-dialog/otp-dialog.component.scss index 82a5a04434..cee4c61242 100644 --- a/frontend/src/app/views/login/otp-dialog/otp-dialog.component.scss +++ b/frontend/src/app/views/login/otp-dialog/otp-dialog.component.scss @@ -52,7 +52,6 @@ form { font-style: normal; font-weight: 500; line-height: 30px; - /* 214.286% */ cursor: pointer; } } @@ -64,7 +63,6 @@ form { font-style: normal; font-weight: 500; line-height: 30px; - /* 214.286% */ cursor: pointer; } @@ -89,7 +87,6 @@ form { .dialog-footer { - //border-top: 1px solid var(--guardian-border-color); padding-top: 20px; }